Compare commits

..

5 Commits

Author SHA1 Message Date
diegosouzapw
9478176782 fix(cache): include tool_choice/tools/response_format in semantic cache signature (#12734) 2026-09-10 15:25:21 -03:00
Diego Rodrigues de Sa e Souza
fd27ff08c7 chore(deps): drain the Dependabot queue — 10 of 13 alerts (#13213)
* chore(deps): drain the Dependabot queue — 7 of 10 alerts

Lockfile-only bumps; no manifest touched, so nothing changes for consumers.

Root package-lock.json:
  hono      4.13.0 -> 4.13.7  (#215 #216 #217, medium, patched 4.13.5)
  csv-parse 7.0.1  -> 7.0.2   (#213, medium)
  joi       18.2.3 -> 18.2.8  (#211 #212, low, patched 18.2.4/18.2.5)

@omniroute/opencode-plugin:
  toml      4.1.1  -> 4.3.0   (#209, HIGH, patched 4.1.2)

@omniroute/opencode-plugin-v2:
  esbuild   0.28.1 -> 0.28.2  (#210, low) — the direct copy only; see below.

The plugin-v2 diff looks large but is one package: esbuild ships 27 platform
binaries, each carrying version + resolved + integrity.

Three alerts stay open, deliberately:

  #218 extract-zip (HIGH) and #214 adm-zip (medium) have NO published patch.
  Both are dev-scope. Closing them needs an upstream release or a decision to
  replace the dependency — neither belongs in a lockfile bump.

  #210 esbuild is only half-closed. `node_modules/esbuild` is on 0.28.2, but
  `tsup` pins `esbuild: ^0.27.0`, so its nested copy stays at 0.27.7 — inside the
  vulnerable range (>= 0.27.3, < 0.28.1). Updating tsup does not move it (8.5.1
  is already current). Forcing it would take an `overrides` entry pushing a major
  of esbuild inside the bundler, which is exactly the change that breaks a build
  silently, for a LOW dev-only alert. Left for an upstream tsup release.

check:lockfile passes on all three, including the workspace lock/manifest
consistency check. check:tracked-artifacts OK.

* chore(deps): bump js-yaml to 4.3.2 (root + electron)

Two more HIGH alerts arrived after the first sweep:

  #220 js-yaml (root package-lock.json)     >= 4.0.0, < 4.3.2
  #219 js-yaml (electron/package-lock.json) >= 4.0.0, < 4.3.2

The root's own js-yaml was already on 5.4.1; the vulnerable copies were the ones
nested under @yarnpkg/parsers, lockfile-lint, xmlbuilder2 (root) and the direct
dependency in electron. All now 4.3.2. Four version lines, nothing else.

#221 smol-toml (HIGH, <= 1.7.0) is NOT closed here. The root is on 1.8.0; the
vulnerable 1.6.1 sits under @openai/codex-security, which pins it as an EXACT
version rather than a range, so `npm update` cannot move it. Bumping
codex-security itself (0.1.24 -> 0.1.26) does not help — 0.1.26 pins the same
1.6.1 — so that bump was reverted rather than carried along for no benefit.

Closing #221 needs an upstream codex-security release or an `overrides` entry,
the same trade already declined for #210/tsup: forcing a transitive pin from
outside is how a build breaks silently. Note that @openai/codex-security is also
the package carrying the unpatched extract-zip (#218), so one upstream release
would likely clear both.

* chore(deps): override smol-toml to 1.8.0 and raise the js-yaml floor

Closes #221 (smol-toml, HIGH, DoS via malformed TOML, vulnerable <= 1.7.0).

@openai/codex-security pins smol-toml at 1.6.1 as an EXACT version, so no
`npm update` reaches it. This repo already uses `overrides` as its standard tool
for exactly that situation — the block carries 20+ entries, including the
scoped-by-parent form and the `qs`/`fast-uri`/`ip-address` entries that back
earlier security bumps — so a scoped override is the idiomatic fix here, not a
new mechanism:

    "@openai/codex-security": { "smol-toml": "^1.8.0" }

The nested copy deduplicates to the root's existing 1.8.0, which two other
consumers (the root itself and knip) already run, so the version is proven in
this tree. The whole lockfile diff is the 14 lines of the removed 1.6.1 entry.

Also raised the `@yarnpkg/parsers` js-yaml floor from ^4.3.1 to ^4.3.2, so the
override documents the patched version rather than permitting the vulnerable one
it was written against.

Not fixed, and not fixable by version — verified against the npm registry rather
than trusting the advisory metadata:

  #218 extract-zip — latest published IS 2.0.1, the vulnerable version. Dev
       scope, via @openai/codex-security. No release to move to.
  #214 adm-zip — latest published IS 0.6.0, the top of the vulnerable range
       (>= 0.5.9, <= 0.6.0). RUNTIME scope, via onnxruntime-node's ^0.5.16, and
       the repo already overrides adm-zip to ^0.6.0. No release to move to.

Both need an upstream fix or a decision to replace the dependency; neither is a
lockfile change. adm-zip being runtime rather than dev makes it the one worth
tracking.

#210 esbuild stays open too. A flat `overrides: { esbuild: ^0.28.2 }` in
opencode-plugin-v2 does close it — npm then reports 0 vulnerabilities — but it
requires regenerating that lockfile from scratch: 823 lines, 96 packages moved,
for a LOW dev-only alert, and a major esbuild bump inside tsup cannot be
validated here without a real install of that package. Tried, measured,
reverted. Left for an upstream tsup release.

check:lockfile OK on all lockfiles including the workspace consistency check;
check:tracked-artifacts OK; prettier clean.
2026-09-10 13:37:55 -03:00
Diego Rodrigues de Sa e Souza
0549dcfc36 fix(api): scope batch bulk-delete to the calling API key (#13211)
GHSA-wvxc-jp3v-5mg5: `DELETE /api/v1/batches/delete-completed` deleted the
completed batches of EVERY api key on the instance and nulled the contents of
every file those batches referenced. Any ordinary inference key reached it —
including one with `scopes: []` — and no victim key, batch id or file id was
needed.

Two defects stacked in one endpoint:

  - `deleteCompletedBatches()` carried no `api_key_id` predicate. The file
    SELECT, the checkpoint DELETE and the batch DELETE were all instance-wide.
  - The route only checked that SOME key was present (`!scope.apiKeyId` → 401),
    never that the caller owned anything, and called the helper bare.

The helper now takes `apiKeyId` and scopes all three statements to it; the route
passes the caller's key and omits it only for session auth, so the operator's own
dashboard keeps its instance-wide cleanup and an API key clears only its own
completed batches.

None of this is a new pattern. `listBatches(apiKeyId?)` and
`countBatches(apiKeyId?)` in the same module already scope by `api_key_id`, and
`batches/[id]/route.ts` already gates per-record access with `scopeCheck` —
session auth sees everything, a key sees only its own. This one helper was the
one that never got it, which is why the fix reuses the shape instead of
inventing a second convention.

tests/unit/batch-delete-completed-ownership-wvxc.test.ts — 5 tests, 4 red before
the fix, including the two that prove the cross-tenant destruction (another
key's batch survives; another key's file content survives). It also pins the
instance-wide dashboard sweep so the fix cannot be "tightened" into breaking the
operator's own cleanup, and a source guard that the route never calls the helper
bare again.

Reported privately via GHSA-wvxc-jp3v-5mg5.

Closes GHSA-wvxc-jp3v-5mg5
2026-09-10 13:27:10 -03:00
Diego Rodrigues de Sa e Souza
393cfdd660 fix(test): make the ToS heading guard actually require the parentheses (#13228)
CodeQL js/useless-regexp-character-escape (#994-#997) on one line, and it is a
real defect rather than the usual query noise.

The assertion built its pattern in a TEMPLATE literal:

    new RegExp(`\(\s*${String(tos?.actual)}\s*\)`)

JavaScript resolves the escapes before RegExp ever sees the string: `\(` becomes
"(" and `\s` becomes the LETTER "s". The compiled pattern was `(s*16s*)` — a
capture group around optional "s" characters — so it matched any heading merely
CONTAINING the number. The literal parentheses this guard exists to require were
never checked, and it passed on exactly the headings it was written to reject:

    /(s*16s*)/.test("### Caution — clauses worth checking 16")   // true

Doubled the backslashes so they survive the template literal, and routed the
interpolated value through an `escapeRegExp` helper — the count is a number
today, but interpolating an unescaped value into a regex source is the same
class of bug one refactor away.

Added a second test that pins the behaviour rather than the spelling: the
pattern must REJECT a heading carrying the count without parentheses, and accept
it with them (including inner whitespace). Before this fix that test fails.

4/4 green against the real docs/reference/FREE_TIERS.md heading.
2026-09-10 13:27:02 -03:00
Diego Rodrigues de Sa e Souza
d86cf75aef fix(quality): register 4 drifted covering tests in stryker tap.testFiles (#13229)
`check:mutation-test-coverage --strict` has been failing Fast Quality Gates on
every open PR against release/v3.8.51. It grew from 2 missing entries to 5 in
roughly an hour, so it is drifting faster than PRs land.

Four test files cover a mutated module without being listed, so their mutant
kills do not count:

  open-sse/services/accountFallback.ts      <- openai-compatible-per-upstream-402-health
  src/sse/services/auth.ts                  <- openai-compatible-per-upstream-402-health
                                            <- quota-window-label
  src/shared/utils/circuitBreaker.ts        <- combo/execute-target-gates
  open-sse/services/combo/comboStructure.ts <- combo-pin-implicit-allowlist

Registration only — no test or module is touched, and no gate is weakened; the
listing is what makes those kills count in the first place.

Inserted in place, never through a JSON round-trip: re-serializing this file
reorders the ~10 curated entries that are already out of alphabetical order
(learned the hard way in #11438).

check:mutation-test-coverage now reports no drift. check:tracked-artifacts OK,
prettier clean.

Worth noting for whoever adds the next test: this gate fires whenever a NEW test
happens to cover one of the 31 mutated modules, which is easy to do without
realising. Registering it in the same commit is cheaper than a CI round-trip.
2026-09-10 13:26:53 -03:00
21 changed files with 615 additions and 269 deletions

View File

@@ -22,7 +22,7 @@
"node": ">=22.22.3"
},
"peerDependencies": {
"@opencode-ai/plugin": "*"
"@opencode-ai/plugin": ">=1.18.29 <2"
}
},
"node_modules/@ai-sdk/provider": {
@@ -39,9 +39,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
"integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
"cpu": [
"ppc64"
],
@@ -56,9 +56,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
"integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
"cpu": [
"arm"
],
@@ -73,9 +73,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
"integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
"cpu": [
"arm64"
],
@@ -90,9 +90,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
"integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
"cpu": [
"x64"
],
@@ -107,9 +107,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
"integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
"cpu": [
"arm64"
],
@@ -124,9 +124,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
"integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
"cpu": [
"x64"
],
@@ -141,9 +141,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
"integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
"cpu": [
"arm64"
],
@@ -158,9 +158,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
"integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
"cpu": [
"x64"
],
@@ -175,9 +175,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
"integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
"cpu": [
"arm"
],
@@ -192,9 +192,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
"integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
"cpu": [
"arm64"
],
@@ -209,9 +209,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
"integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
"cpu": [
"ia32"
],
@@ -226,9 +226,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
"integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
"cpu": [
"loong64"
],
@@ -243,9 +243,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
"integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
"cpu": [
"mips64el"
],
@@ -260,9 +260,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
"integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
"cpu": [
"ppc64"
],
@@ -277,9 +277,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
"integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
"cpu": [
"riscv64"
],
@@ -294,9 +294,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
"integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
"cpu": [
"s390x"
],
@@ -311,7 +311,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.28.1",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
"integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
"cpu": [
"x64"
],
@@ -326,9 +328,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
"integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
"cpu": [
"arm64"
],
@@ -343,9 +345,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
"integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
"cpu": [
"x64"
],
@@ -360,9 +362,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
"integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
"cpu": [
"arm64"
],
@@ -377,9 +379,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
"integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
"cpu": [
"x64"
],
@@ -394,9 +396,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
"integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
"cpu": [
"arm64"
],
@@ -411,9 +413,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
"integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
"cpu": [
"x64"
],
@@ -428,9 +430,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
"integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
"cpu": [
"arm64"
],
@@ -445,9 +447,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
"integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
"cpu": [
"ia32"
],
@@ -462,9 +464,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
"integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
"cpu": [
"x64"
],
@@ -1180,7 +1182,9 @@
}
},
"node_modules/esbuild": {
"version": "0.28.1",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
"integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -1191,32 +1195,32 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.1",
"@esbuild/android-arm": "0.28.1",
"@esbuild/android-arm64": "0.28.1",
"@esbuild/android-x64": "0.28.1",
"@esbuild/darwin-arm64": "0.28.1",
"@esbuild/darwin-x64": "0.28.1",
"@esbuild/freebsd-arm64": "0.28.1",
"@esbuild/freebsd-x64": "0.28.1",
"@esbuild/linux-arm": "0.28.1",
"@esbuild/linux-arm64": "0.28.1",
"@esbuild/linux-ia32": "0.28.1",
"@esbuild/linux-loong64": "0.28.1",
"@esbuild/linux-mips64el": "0.28.1",
"@esbuild/linux-ppc64": "0.28.1",
"@esbuild/linux-riscv64": "0.28.1",
"@esbuild/linux-s390x": "0.28.1",
"@esbuild/linux-x64": "0.28.1",
"@esbuild/netbsd-arm64": "0.28.1",
"@esbuild/netbsd-x64": "0.28.1",
"@esbuild/openbsd-arm64": "0.28.1",
"@esbuild/openbsd-x64": "0.28.1",
"@esbuild/openharmony-arm64": "0.28.1",
"@esbuild/sunos-x64": "0.28.1",
"@esbuild/win32-arm64": "0.28.1",
"@esbuild/win32-ia32": "0.28.1",
"@esbuild/win32-x64": "0.28.1"
"@esbuild/aix-ppc64": "0.28.2",
"@esbuild/android-arm": "0.28.2",
"@esbuild/android-arm64": "0.28.2",
"@esbuild/android-x64": "0.28.2",
"@esbuild/darwin-arm64": "0.28.2",
"@esbuild/darwin-x64": "0.28.2",
"@esbuild/freebsd-arm64": "0.28.2",
"@esbuild/freebsd-x64": "0.28.2",
"@esbuild/linux-arm": "0.28.2",
"@esbuild/linux-arm64": "0.28.2",
"@esbuild/linux-ia32": "0.28.2",
"@esbuild/linux-loong64": "0.28.2",
"@esbuild/linux-mips64el": "0.28.2",
"@esbuild/linux-ppc64": "0.28.2",
"@esbuild/linux-riscv64": "0.28.2",
"@esbuild/linux-s390x": "0.28.2",
"@esbuild/linux-x64": "0.28.2",
"@esbuild/netbsd-arm64": "0.28.2",
"@esbuild/netbsd-x64": "0.28.2",
"@esbuild/openbsd-arm64": "0.28.2",
"@esbuild/openbsd-x64": "0.28.2",
"@esbuild/openharmony-arm64": "0.28.2",
"@esbuild/sunos-x64": "0.28.2",
"@esbuild/win32-arm64": "0.28.2",
"@esbuild/win32-ia32": "0.28.2",
"@esbuild/win32-x64": "0.28.2"
}
},
"node_modules/fast-check": {

View File

@@ -1745,9 +1745,9 @@
}
},
"node_modules/toml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz",
"integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/toml/-/toml-4.3.0.tgz",
"integrity": "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A==",
"dev": true,
"license": "MIT",
"engines": {

View File

@@ -578,31 +578,14 @@ own dedicated branch, and you MUST confirm the base branch with the operator bef
# HARD LINKS (`cp -al`), never a symlink: ~5s for the whole tree and near-zero extra
# disk (the inodes are shared), and unlike a symlink it does not break the dev server.
cp -al "$(git -C <main_checkout> rev-parse --show-toplevel)/node_modules" node_modules
# `.husky/_` is gitignored, so a fresh worktree does NOT have it and
# `core.hooksPath=.husky/_` then points at a directory that does not exist —
# every pre-commit gate goes silently mute. Copy it too.
cp -a "$(git -C <main_checkout> rev-parse --show-toplevel)/.husky/_" .husky/_
```
`scripts/dev/new-worktree.sh <branch> [base]` does all of the above (canonical path,
hard-linked `node_modules`, `.husky/_`) and then **verifies** the hook is actually
executable, so prefer it over running the steps by hand.
**Never `ln -s` node_modules.** Turbopack rejects a symlink that resolves outside the
project root, so `npm run dev` dies with a FATAL panic (`Symlink [project]/node_modules
is invalid, it points out of the filesystem root`) while typecheck, lint and the test
runners all keep passing — the error names "filesystem root", not the worktree, so it
reads like a Next/build bug and costs real time to trace (incident 2026-07-31, #9043).
**A worktree without `.husky/_` runs NO pre-commit gate — and says nothing.** `git`
resolves `core.hooksPath` relative to the worktree top; when the directory is missing it
simply finds no hook and commits. Nothing is printed, the commit succeeds, and the
identity/lint/docs gates never ran. This is how 59 commits carrying a stale identity
override (name of a contributor + the maintainer's e-mail) got past
`scripts/check/check-git-identity.sh` between 2026-08-29 and 09-02 — they were all made in
`cp -al` worktrees. Verify with `ls .husky/_/pre-commit` inside a new worktree, or just use
`scripts/dev/new-worktree.sh`, which fails loudly when the hook is not executable.
3. **Work, commit, push, open the PR — all from inside the worktree.** Never `git checkout` a
different branch inside a worktree another session might share.
4. **Tear down only your own** worktree + branch when done, from the main checkout:

View File

@@ -0,0 +1 @@
- fix(cache): fold tool_choice/tools/response_format into the semantic cache signature so a cached tool_calls response can no longer be replayed for a request whose tool policy forbids it (#12734)

View File

@@ -2113,9 +2113,9 @@
}
},
"node_modules/js-yaml": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
"integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
"funding": [
{
"type": "github",

View File

@@ -29,7 +29,13 @@ export async function checkSemanticCache({
semanticCacheEnabled: boolean;
// Only the fields this read path actually touches are named; everything else
// on the request body stays `unknown` via the index signature.
body: Record<string, unknown> & { temperature?: number; top_p?: number };
body: Record<string, unknown> & {
temperature?: number;
top_p?: number;
tool_choice?: unknown;
tools?: unknown;
response_format?: unknown;
};
clientRawRequest: { headers?: unknown } | null;
model: string;
provider: string;
@@ -51,7 +57,8 @@ export async function checkSemanticCache({
body.messages ?? body.input,
body.temperature,
body.top_p,
apiKeyId ?? undefined
apiKeyId ?? undefined,
{ toolChoice: body.tool_choice, tools: body.tools, responseFormat: body.response_format }
);
const cached = getCachedResponse(signature);
if (cached) {

View File

@@ -22,6 +22,9 @@ type CacheBody = {
input?: unknown;
temperature?: number;
top_p?: number;
tool_choice?: unknown;
tools?: unknown;
response_format?: unknown;
};
type UsageLike = { prompt_tokens?: number; completion_tokens?: number } | null | undefined;
@@ -65,7 +68,12 @@ export function storeSemanticCacheResponse(
args.body.messages ?? args.body.input,
args.body.temperature,
args.body.top_p,
args.apiKeyId ?? undefined
args.apiKeyId ?? undefined,
{
toolChoice: args.body.tool_choice,
tools: args.body.tools,
responseFormat: args.body.response_format,
}
);
const tokensSaved = args.usage?.prompt_tokens + args.usage?.completion_tokens || 0;
deps.setCachedResponse(signature, args.model, args.translatedResponse, tokensSaved);

View File

@@ -23,6 +23,9 @@ type CacheBody = {
input?: unknown;
temperature?: number;
top_p?: number;
tool_choice?: unknown;
tools?: unknown;
response_format?: unknown;
};
export interface StreamingSemanticCacheStoreDeps {
@@ -69,7 +72,12 @@ function writeStreamingCacheEntry(
args.body.messages ?? args.body.input,
args.body.temperature,
args.body.top_p,
args.apiKeyId ?? undefined
args.apiKeyId ?? undefined,
{
toolChoice: args.body.tool_choice,
tools: args.body.tools,
responseFormat: args.body.response_format,
}
);
const tokensSaved = streamTokensSaved(args.streamUsage);
deps.setCachedResponse(sig, args.model, cleanBody, tokensSaved);

50
package-lock.json generated
View File

@@ -8687,20 +8687,6 @@
"license": "ISC",
"optional": true
},
"node_modules/@openai/codex-security/node_modules/smol-toml": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz",
"integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
"engines": {
"node": ">= 18"
},
"funding": {
"url": "https://github.com/sponsors/cyyynthia"
}
},
"node_modules/@openai/codex-security/node_modules/type-fest": {
"version": "5.9.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.9.0.tgz",
@@ -15217,9 +15203,9 @@
}
},
"node_modules/@yarnpkg/parsers/node_modules/js-yaml": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
"integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
"dev": true,
"funding": [
{
@@ -18535,9 +18521,9 @@
"license": "MIT"
},
"node_modules/csv-parse": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-7.0.1.tgz",
"integrity": "sha512-+2z7Ar0APQ7Uu6fX4cn+pitRmxjZ1WPBcGmZFKmA74FCyi7Et/XZx8cjNQ5CjbZ4HCOxXCOpRBYvYH08Qa003A==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-7.0.2.tgz",
"integrity": "sha512-uKZghv9UmPkMVLYy//KZ9HFAIJsl7wkhoEdIL0+rhuSY9pZQlhaeGEDPIe+/w7eh81MOql8Q/9+inAGWG6ZHYA==",
"dev": true,
"license": "MIT"
},
@@ -23603,9 +23589,9 @@
"license": "MIT"
},
"node_modules/hono": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.0.tgz",
"integrity": "sha512-jhunvfHWxd7J5EFfSgH4xsYJzSe/lfqbUCxiyyeaQasUsXeEHXtzVid+7EOGByc5JnFa23SSFL3Y2RV/z1T+eQ==",
"version": "4.13.7",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.7.tgz",
"integrity": "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
@@ -26116,9 +26102,9 @@
}
},
"node_modules/joi": {
"version": "18.2.3",
"resolved": "https://registry.npmjs.org/joi/-/joi-18.2.3.tgz",
"integrity": "sha512-N5A3KTWQpPWT4ExxxPlUx7WmykGXRzhNidWhV41d6Abu9YfI2NyWCJuxdPnslJCPWtbRpSVOWSnSS6GakLM/Rg==",
"version": "18.2.8",
"resolved": "https://registry.npmjs.org/joi/-/joi-18.2.8.tgz",
"integrity": "sha512-G2TX62h58ZHuwqetJgP2F4ualakqAmZtBYe3jWen7gxQRw5xApX6crnFtuB91WC0c3ESBnva+kGSnb3+6pIQDQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -27946,9 +27932,9 @@
}
},
"node_modules/lockfile-lint/node_modules/js-yaml": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
"integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
"dev": true,
"funding": [
{
@@ -39620,9 +39606,9 @@
}
},
"node_modules/xmlbuilder2/node_modules/js-yaml": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
"integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
"dev": true,
"funding": [
{

View File

@@ -486,7 +486,10 @@
"fast-uri": "^3.1.7",
"body-parser": "^2.3.0",
"@yarnpkg/parsers": {
"js-yaml": "^4.3.1"
"js-yaml": "^4.3.2"
},
"@openai/codex-security": {
"smol-toml": "^1.8.0"
},
"jsdom": {
"undici": "^7.29.0"

View File

@@ -1,92 +0,0 @@
#!/usr/bin/env sh
# Cria uma worktree isolada seguindo o protocolo obrigatório do AGENTS.md
# (Git Workflow → "Worktree isolation" / Hard Rule #19), incluindo os dois
# passos que são fáceis de esquecer e falham em silêncio:
#
# 1. node_modules por HARD LINK (`cp -al`), nunca symlink — um symlink que
# resolve fora da raiz mata o Turbopack com um FATAL que culpa a
# "filesystem root" e não a worktree (incidente 2026-07-31, #9043).
# 2. `.husky/_` copiado — é gitignored, então uma worktree nova NÃO o tem, e
# `core.hooksPath=.husky/_` aponta para um diretório inexistente: TODOS os
# hooks de pre-commit ficam mudos, sem aviso nenhum. Foi assim que 59
# commits com identidade trocada passaram pelo gate entre 29/08 e 02/09
# (ver .mailmap e scripts/check/check-git-identity.sh).
#
# Uso: scripts/dev/new-worktree.sh <branch> [base-branch]
# Ex.: scripts/dev/new-worktree.sh fix/12345-algo release/v3.8.51
set -e
BRANCH="$1"
BASE="$2"
if [ -z "$BRANCH" ]; then
echo "uso: scripts/dev/new-worktree.sh <branch> [base-branch]" >&2
echo " ex: scripts/dev/new-worktree.sh fix/12345-algo release/v3.8.51" >&2
exit 1
fi
# O checkout PRINCIPAL, mesmo quando este script roda de dentro de outra worktree:
# `--show-toplevel` devolveria a worktree atual, e a nova nasceria aninhada nela.
MAIN=$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")
cd "$MAIN"
# Sem base explícita, usa a release ativa (maior release/* por semver) — nunca
# `main` e nunca "a branch em que eu estou", conforme a Hard Rule #19.
if [ -z "$BASE" ]; then
BASE=$(git ls-remote --heads origin 'refs/heads/release/*' \
| sed 's#.*refs/heads/##' | sort -V | tail -1)
[ -z "$BASE" ] && { echo "não consegui resolver a release ativa; passe a base explicitamente" >&2; exit 1; }
echo "base não informada — usando a release ativa: $BASE"
fi
DIR=".claude/worktrees/${BRANCH##*/}"
[ -e "$DIR" ] && { echo "já existe: $DIR" >&2; exit 1; }
git fetch origin "$BASE" --quiet
git worktree add "$DIR" -b "$BRANCH" "origin/$BASE"
# `.husky/_` PRIMEIRO: é minúsculo e é o que decide se os gates locais rodam.
# Copiar node_modules antes seria arriscar abortar (set -e) numa árvore de ~10 GB
# e deixar a worktree sem hook nenhum — exatamente o defeito que este script existe
# para impedir.
if [ -d "$MAIN/.husky/_" ]; then
cp -a "$MAIN/.husky/_" "$DIR/.husky/_"
else
echo "AVISO: .husky/_ não existe no checkout principal — rode 'npm install' lá primeiro" >&2
fi
# node_modules: hard links, ~5s e disco quase zero (inodes compartilhados).
# Um `cp -al SRC DEST` com DEST já existente aninharia SRC DENTRO dele
# (node_modules/node_modules), então DEST não pode existir aqui.
if [ -d "$MAIN/node_modules/node_modules" ]; then
echo "AVISO: $MAIN/node_modules/node_modules existe — resíduo de um cp -al aninhado." >&2
echo " Ele infla a cópia e esgota o limite de hard links; convém removê-lo." >&2
fi
if [ -d "$MAIN/node_modules" ]; then
# Falha parcial (limite de hard links, disco) não pode derrubar a worktree inteira:
# os hooks já estão no lugar e o npm install continua sendo uma saída válida.
if cp -al "$MAIN/node_modules" "$DIR/node_modules" 2>"$DIR/.cp-node-modules.log"; then
echo "node_modules: $(ls "$DIR/node_modules" | wc -l) entradas (hard links)"
rm -f "$DIR/.cp-node-modules.log"
else
echo "AVISO: a cópia de node_modules falhou parcialmente (veja $DIR/.cp-node-modules.log)." >&2
echo " Primeiras linhas:" >&2
head -3 "$DIR/.cp-node-modules.log" >&2
fi
else
echo "AVISO: node_modules não existe no checkout principal — rode 'npm install' lá primeiro" >&2
fi
# Verificação: o hook precisa estar REALMENTE ativo, não apenas presente.
HOOKS_PATH=$(git -C "$DIR" config --get core.hooksPath || echo ".git/hooks")
if [ -x "$DIR/$HOOKS_PATH/pre-commit" ]; then
echo "hooks: ativos ($HOOKS_PATH/pre-commit)"
else
echo "AVISO: pre-commit NÃO está ativo em $DIR/$HOOKS_PATH — os gates locais não vão rodar" >&2
exit 1
fi
echo
echo "pronto: $DIR (branch $BRANCH, base $BASE)"
echo " cd $DIR"

View File

@@ -19,7 +19,10 @@ export async function DELETE(request: Request) {
);
}
const result = deleteCompletedBatches();
// Scope the sweep to the caller. Only the operator's own dashboard (session
// auth) may clear the whole instance; an API key clears only its own
// completed batches (GHSA-wvxc-jp3v-5mg5).
const result = deleteCompletedBatches(scope.isSessionAuth ? undefined : scope.apiKeyId);
return NextResponse.json(
{ deleted: true, deletedBatches: result.deletedBatches, deletedFiles: result.deletedFiles },

View File

@@ -411,15 +411,37 @@ export function deleteBatch(id: string): boolean {
return result.changes > 0;
}
export function deleteCompletedBatches(): { deletedBatches: number; deletedFiles: number } {
/**
* Bulk-delete completed batches and the files they reference.
*
* `apiKeyId` scopes EVERY statement to that owner. Omitting it keeps the
* instance-wide sweep, which is legitimate for the operator's own dashboard
* (session auth) and for nothing else: without the predicate, an ordinary
* inference key could wipe every tenant's completed batches and null out their
* file contents (GHSA-wvxc-jp3v-5mg5). Same ownership shape as `listBatches`
* and `countBatches` above.
*/
export function deleteCompletedBatches(apiKeyId?: string | null): {
deletedBatches: number;
deletedFiles: number;
} {
const db = getDbInstance();
const scoped = typeof apiKeyId === "string" && apiKeyId.length > 0;
// Collect unique file IDs from all completed batches
const rows = db
.prepare(
"SELECT input_file_id, output_file_id, error_file_id FROM batches WHERE status = 'completed'"
)
.all() as Array<{
// Collect unique file IDs from the completed batches in scope
const rows = (
scoped
? db
.prepare(
"SELECT input_file_id, output_file_id, error_file_id FROM batches WHERE status = 'completed' AND api_key_id = ?"
)
.all(apiKeyId)
: db
.prepare(
"SELECT input_file_id, output_file_id, error_file_id FROM batches WHERE status = 'completed'"
)
.all()
) as Array<{
input_file_id: string | null;
output_file_id: string | null;
error_file_id: string | null;
@@ -441,6 +463,16 @@ export function deleteCompletedBatches(): { deletedBatches: number; deletedFiles
}
}
if (scoped) {
db.prepare(
"DELETE FROM batch_item_checkpoints WHERE batch_id IN (SELECT id FROM batches WHERE status = 'completed' AND api_key_id = ?)"
).run(apiKeyId);
const result = db
.prepare("DELETE FROM batches WHERE status = 'completed' AND api_key_id = ?")
.run(apiKeyId);
return { deletedBatches: result.changes, deletedFiles };
}
db.prepare(
"DELETE FROM batch_item_checkpoints WHERE batch_id IN (SELECT id FROM batches WHERE status = 'completed')"
).run();

View File

@@ -137,6 +137,43 @@ export function clearMemoryCache(): void {
// ─── Signature Generation ─────────────────
/**
* Behavior-changing generation constraints that MUST be folded into the cache signature
* (#12734). Without these, a cached response produced under one `tool_choice`/`tools`/
* `response_format` could be replayed for a later request that forbids or changes that
* behavior (e.g. a cached `tool_calls` response served to a `tool_choice: "none"` request).
*/
export interface SignatureConstraints {
toolChoice?: unknown;
tools?: unknown;
responseFormat?: unknown;
}
/** Normalize a single tool definition, keeping only the fields that define its policy. */
function normalizeTool(tool: unknown): unknown {
const record = asRecord(tool);
const fn = asRecord(record.function);
if (Object.keys(fn).length === 0 && Object.keys(record).length === 0) return tool;
return {
type: typeof record.type === "string" ? record.type : "function",
function: {
name: fn.name,
description: fn.description,
parameters: fn.parameters,
},
};
}
/**
* Normalize `tools` for consistent hashing (mirrors `normalizeConversation` for messages):
* strips volatile/irrelevant fields while keeping name/description/parameters, which are
* what actually define the tool policy a cached response was generated under.
*/
function normalizeTools(tools: unknown): unknown {
if (!Array.isArray(tools) || tools.length === 0) return undefined;
return tools.map(normalizeTool);
}
/**
* Generate deterministic cache signature from request params.
* @param {string} model
@@ -144,6 +181,8 @@ export function clearMemoryCache(): void {
* @param {number} temperature
* @param {number} topP
* @param {string} [apiKeyId] - API key ID for per-key isolation (prevents cross-user cache hits)
* @param {SignatureConstraints} [constraints] - tool_choice/tools/response_format (#12734):
* these change model behavior and must not collide with a signature computed without them.
* @returns {string} hex signature
*/
export function generateSignature(
@@ -151,13 +190,17 @@ export function generateSignature(
conversation,
temperature = 0,
topP = 1,
apiKeyId?: string
apiKeyId?: string,
constraints?: SignatureConstraints
) {
const payload = JSON.stringify({
model,
messages: normalizeConversation(conversation),
temperature,
top_p: topP,
tool_choice: constraints?.toolChoice,
tools: normalizeTools(constraints?.tools),
response_format: constraints?.responseFormat,
});
const digest = crypto.createHash("sha256").update(payload).digest("hex");
// Per-key cache isolation (#3740) namespaces the signature with the apiKeyId as a

View File

@@ -58,6 +58,8 @@
"tests/unit/account-fallback-retry-after-json.test.ts",
"tests/unit/account-fallback-route-restriction-403.test.ts",
"tests/unit/account-fallback-service.test.ts",
"tests/unit/combo-pin-implicit-allowlist.test.ts",
"tests/unit/combo/execute-target-gates.test.ts",
"tests/unit/moonshot-quota-writeback.test.ts",
"tests/unit/accountfallback-ratelimit-400-4976.test.ts",
"tests/unit/adaptive-admission-route-matrix.test.ts",
@@ -75,6 +77,8 @@
"tests/unit/api-key-policy-noauth-allowed-connections.test.ts",
"tests/unit/api-key-rotator-health.test.ts",
"tests/unit/chat-routing-synced-inventory-11089.test.ts",
"tests/unit/openai-compatible-per-upstream-402-health.test.ts",
"tests/unit/quota-window-label.test.ts",
"tests/unit/repro-combo-persisted-cooldown-preskip.test.ts",
"tests/unit/repro-glm-iso-reset-24h-cap.test.ts",
"tests/unit/security-route-guard-tiers.test.ts",

View File

@@ -0,0 +1,114 @@
/**
* GHSA-wvxc-jp3v-5mg5 — `DELETE /api/v1/batches/delete-completed` deleted the
* completed batches of EVERY api key on the instance, and nulled the contents of
* every file those batches referenced.
*
* Two defects in one endpoint:
* 1. `deleteCompletedBatches()` carried no `api_key_id` predicate — the file
* SELECT, the checkpoint DELETE and the batch DELETE were all instance-wide.
* 2. The route only checked that SOME key was present (`!scope.apiKeyId` →
* 401), never that the caller owned anything. A key with `scopes: []` —
* an ordinary inference key — could wipe another tenant's batches.
*
* The ownership pattern this restores is not new: `listBatches(apiKeyId?)` and
* `countBatches(apiKeyId?)` in the same module already scope by `api_key_id`,
* and `batches/[id]/route.ts` already gates per-record access with `scopeCheck`
* (session auth sees everything, a key sees only its own). This helper was the
* one that never got it.
*
* Run with:
* node --import tsx/esm --test tests/unit/batch-delete-completed-ownership-wvxc.test.ts
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { createFile, getFile } from "@/lib/db/files";
import { createBatch, getBatch, deleteCompletedBatches } from "@/lib/db/batches";
const KEY_A = "key-wvxc-aaaa";
const KEY_B = "key-wvxc-bbbb";
function seedCompletedBatch(apiKeyId: string | null, tag: string) {
const file = createFile({
bytes: 10,
filename: `wvxc-${tag}.jsonl`,
purpose: "batch",
content: Buffer.from("{}"),
});
const batch = createBatch({
endpoint: "/v1/chat/completions",
completionWindow: "24h",
inputFileId: file.id,
status: "completed",
apiKeyId,
});
return { file, batch };
}
describe("deleteCompletedBatches — ownership scoping (GHSA-wvxc-jp3v-5mg5)", () => {
it("scoped to one key deletes ONLY that key's completed batches", () => {
const a = seedCompletedBatch(KEY_A, "a1");
const b = seedCompletedBatch(KEY_B, "b1");
const result = deleteCompletedBatches(KEY_A);
assert.equal(getBatch(a.batch.id), null, "the caller's own batch should be gone");
assert.ok(getBatch(b.batch.id), "another key's batch must survive");
assert.equal(result.deletedBatches, 1, "must report only what it actually deleted");
});
it("scoped deletion does not touch another key's file contents", () => {
const a = seedCompletedBatch(KEY_A, "a2");
const b = seedCompletedBatch(KEY_B, "b2");
deleteCompletedBatches(KEY_A);
assert.equal(getFile(a.file.id), null, "the caller's own file should be gone");
assert.ok(getFile(b.file.id), "another key's file must survive with its content intact");
});
it("a key with no completed batches deletes nothing at all", () => {
const b = seedCompletedBatch(KEY_B, "b3");
const result = deleteCompletedBatches("key-wvxc-with-nothing");
assert.equal(result.deletedBatches, 0);
assert.equal(result.deletedFiles, 0);
assert.ok(getBatch(b.batch.id), "an unrelated key's batch must survive");
});
it("unscoped (dashboard session) still clears the whole instance", () => {
// The operator's own dashboard legitimately cleans up everything; that is
// the ONLY caller allowed to omit the key. Preserved deliberately.
seedCompletedBatch(KEY_A, "a4");
seedCompletedBatch(KEY_B, "b4");
const result = deleteCompletedBatches();
assert.ok(
result.deletedBatches >= 2,
`expected an instance-wide sweep, got ${result.deletedBatches}`
);
});
});
describe("the route passes the caller's key through", () => {
it("delete-completed scopes by api key instead of calling the helper bare", async () => {
const { readFileSync } = await import("node:fs");
const { fileURLToPath } = await import("node:url");
const src = readFileSync(
fileURLToPath(
new URL("../../src/app/api/v1/batches/delete-completed/route.ts", import.meta.url)
),
"utf8"
);
assert.ok(
!/deleteCompletedBatches\(\s*\)/.test(src),
"the route still calls deleteCompletedBatches() with no owner — every tenant's batches go"
);
assert.ok(
/deleteCompletedBatches\(\s*scope\./.test(src),
"the route must pass the caller's scope into the helper"
);
});
});

View File

@@ -135,3 +135,38 @@ test("missing usage → tokensSaved coerces to 0 (NaN || 0)", () => {
storeSemanticCacheResponse(baseArgs({ usage: undefined }), deps);
assert.equal(stored[0].tokens, 0);
});
// #12734: tool_choice/tools/response_format must reach generateSignature so a cached
// tool_calls response cannot be replayed under a stricter tool policy.
test("signature is called with tool_choice/tools/response_format from body (#12734)", () => {
let captured: unknown[] = [];
const { deps } = makeDeps({
generateSignature: (...a: unknown[]) => {
captured = a;
return "sig";
},
});
const tools = [{ type: "function", function: { name: "get_weather" } }];
storeSemanticCacheResponse(
baseArgs({
body: {
messages: [{ role: "user", content: "hi" }],
temperature: 0,
top_p: 1,
tool_choice: "none",
tools,
response_format: { type: "json_object" },
},
}),
deps
);
// args: (model, messages ?? input, temperature, top_p, apiKeyId, constraints)
const constraints = captured[5] as {
toolChoice: unknown;
tools: unknown;
responseFormat: unknown;
};
assert.equal(constraints.toolChoice, "none");
assert.deepEqual(constraints.tools, tools);
assert.deepEqual(constraints.responseFormat, { type: "json_object" });
});

View File

@@ -180,12 +180,14 @@ function makeHitArgs(overrides: Record<string, unknown> = {}) {
// Seed the cache under the EXACT signature checkSemanticCache rebuilds for `args`.
function seedHit(args: ReturnType<typeof makeHitArgs>["args"], response: unknown) {
const body = args.body as Record<string, unknown>;
const signature = generateSignature(
args.model,
args.body.messages ?? (args.body as Record<string, unknown>).input,
body.messages ?? body.input,
args.body.temperature,
(args.body as Record<string, unknown>).top_p,
args.apiKeyId ?? undefined
body.top_p,
args.apiKeyId ?? undefined,
{ toolChoice: body.tool_choice, tools: body.tools, responseFormat: body.response_format }
);
setCachedResponse(signature, args.model, response);
return signature;
@@ -492,3 +494,76 @@ test("checkSemanticCache HIT includes X-OmniRoute-Cache-Latency: synthetic heade
"HIT response carries X-OmniRoute-Cache-Latency: synthetic marker"
);
});
// ─── tool_choice / tools / response_format must be part of the signature (#12734) ────────────
test("#12734: cached tool_calls response must NOT be replayed for tool_choice: 'none'", async () => {
clearCache();
const messages = [{ role: "user", content: "what is 2+2?" }];
const toolCallResponse = {
id: "chatcmpl-tool-calls",
choices: [
{
index: 0,
finish_reason: "tool_calls",
message: {
role: "assistant",
content: null,
tool_calls: [
{ id: "call_1", type: "function", function: { name: "memory_search", arguments: "{}" } },
],
},
},
],
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
};
// Stored under a body with NO tool_choice (mirrors the real pipeline: the cache check
// runs before memory/skill tool injection, so the signature it stores under never saw
// tool_choice at all).
const { args: storeArgs } = makeHitArgs({ body: { model: "gpt-4o", messages, temperature: 0 } });
seedHit(storeArgs, toolCallResponse);
const { args: forbidArgs } = makeHitArgs({
body: { model: "gpt-4o", messages, temperature: 0, tool_choice: "none" },
});
const result = await checkSemanticCache(forbidArgs as Parameters<typeof checkSemanticCache>[0]);
assert.equal(
result,
null,
"a tool_choice:'none' request must be a cache MISS against a tool_calls response cached without tool_choice"
);
});
test("#12734: identical tool_choice/tools/response_format across requests still HITs", async () => {
clearCache();
const messages = [{ role: "user", content: "what is the weather?" }];
const tools = [
{
type: "function",
function: { name: "get_weather", description: "Get the weather", parameters: { type: "object" } },
},
];
const cached = {
id: "chatcmpl-tool-config-hit",
choices: [
{ index: 0, message: { role: "assistant", content: "sunny" }, finish_reason: "stop" },
],
usage: { prompt_tokens: 8, completion_tokens: 2, total_tokens: 10 },
};
const body = {
model: "gpt-4o",
messages,
temperature: 0,
tool_choice: "auto",
tools,
response_format: { type: "json_object" },
};
const { args: storeArgs } = makeHitArgs({ body });
seedHit(storeArgs, cached);
const { args: readArgs } = makeHitArgs({ body: { ...body } });
const result = await checkSemanticCache(readArgs as Parameters<typeof checkSemanticCache>[0]);
assert.ok(result, "identical tool_choice/tools/response_format must still HIT");
});

View File

@@ -127,3 +127,38 @@ test("a throwing dep is swallowed (fail-open, non-critical)", () => {
});
assert.doesNotThrow(() => storeStreamingSemanticCacheResponse(baseArgs(), deps));
});
// #12734: tool_choice/tools/response_format must reach generateSignature so a cached
// tool_calls streaming response cannot be replayed under a stricter tool policy.
test("signature is called with tool_choice/tools/response_format from body (#12734)", () => {
let captured: unknown[] = [];
const { deps } = makeDeps({
generateSignature: (...a: unknown[]) => {
captured = a;
return "sig";
},
});
const tools = [{ type: "function", function: { name: "get_weather" } }];
storeStreamingSemanticCacheResponse(
baseArgs({
body: {
messages: [{ role: "user", content: "hi" }],
temperature: 0,
top_p: 1,
tool_choice: "none",
tools,
response_format: { type: "json_object" },
},
}),
deps
);
// args: (model, messages ?? input, temperature, top_p, apiKeyId, constraints)
const constraints = captured[5] as {
toolChoice: unknown;
tools: unknown;
responseFormat: unknown;
};
assert.equal(constraints.toolChoice, "none");
assert.deepEqual(constraints.tools, tools);
assert.deepEqual(constraints.responseFormat, { type: "json_object" });
});

View File

@@ -13,6 +13,11 @@ type Check = {
validate?: (content: string, claim?: string) => { ok: boolean; detail: string };
};
/** Escape a value that is interpolated into a RegExp source. */
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
describe("ToS caution heading count", () => {
it("the Caution heading carries the count the gate checks", () => {
const txt = readFileSync(join(process.cwd(), "docs/reference/FREE_TIERS.md"), "utf8");
@@ -21,7 +26,24 @@ describe("ToS caution heading count", () => {
const tos = (buildChecks() as Check[]).find((c) =>
String(c.docKey ?? "").includes("ToS caution")
);
assert.match(heading, new RegExp(`\(\s*${String(tos?.actual)}\s*\)`));
// The backslashes must survive the TEMPLATE LITERAL to reach the regex.
// Written as `\(\s*…` they did not: JS resolves `\(` to "(" and `\s` to the
// LETTER "s" before RegExp ever sees them, so the pattern compiled to
// `(s*16s*)` — a capture group around optional "s" characters. That matched
// any heading merely containing the number, with no literal parentheses
// required at all, so this guard passed on headings it was written to reject
// (CodeQL js/useless-regexp-character-escape #994-#997).
const count = escapeRegExp(String(tos?.actual));
assert.match(heading, new RegExp(`\\(\\s*${count}\\s*\\)`));
});
it("the heading guard actually requires the parentheses", () => {
// Pins the defect above: the pattern this test builds must REJECT a heading
// that carries the count without parentheses. Before the fix it accepted it.
const pattern = new RegExp(`\\(\\s*${escapeRegExp("16")}\\s*\\)`);
assert.equal(pattern.test("### Caution — clauses worth checking 16"), false);
assert.equal(pattern.test("### Caution — clauses worth checking (16)"), true);
assert.equal(pattern.test("### Caution — clauses worth checking ( 16 )"), true);
});
it("buildChecks exposes a soft ToS entry on FREE_TIERS.md with requireClaim", () => {
const checks = buildChecks() as Check[];

View File

@@ -107,6 +107,81 @@ describe("Semantic Cache", () => {
const sigKeyless = generateSignature("gpt-4o", messages, 0, 1, undefined);
assert.notEqual(sigKeyed, sigKeyless);
});
// #12734: tool_choice/tools/response_format change model behavior and must not be
// ignored by the signature — otherwise a cached tool_calls response can be replayed
// for a request whose tool policy forbids it.
describe("tool_choice / tools / response_format (#12734)", () => {
const messages = [{ role: "user", content: "what is 2+2?" }];
it("generates different signatures for different tool_choice ('auto' vs 'none')", () => {
const sig1 = generateSignature("gpt-4o", messages, 0, 1, undefined, {
toolChoice: "auto",
});
const sig2 = generateSignature("gpt-4o", messages, 0, 1, undefined, {
toolChoice: "none",
});
assert.notEqual(sig1, sig2);
});
it("generates different signatures for a forced-function tool_choice", () => {
const sig1 = generateSignature("gpt-4o", messages, 0, 1, undefined, {
toolChoice: "auto",
});
const sig2 = generateSignature("gpt-4o", messages, 0, 1, undefined, {
toolChoice: { type: "function", function: { name: "get_weather" } },
});
assert.notEqual(sig1, sig2);
});
it("generates different signatures for no tool_choice vs an explicit one (the #12734 collision)", () => {
const sigNoToolChoice = generateSignature("gpt-4o", messages, 0, 1);
const sigWithToolChoice = generateSignature("gpt-4o", messages, 0, 1, undefined, {
toolChoice: "none",
});
assert.notEqual(sigNoToolChoice, sigWithToolChoice);
});
it("generates different signatures for different tools arrays", () => {
const tools1 = [{ type: "function", function: { name: "get_weather", parameters: {} } }];
const tools2 = [{ type: "function", function: { name: "get_stock_price", parameters: {} } }];
const sig1 = generateSignature("gpt-4o", messages, 0, 1, undefined, { tools: tools1 });
const sig2 = generateSignature("gpt-4o", messages, 0, 1, undefined, { tools: tools2 });
assert.notEqual(sig1, sig2);
});
it("generates different signatures for different response_format", () => {
const sig1 = generateSignature("gpt-4o", messages, 0, 1, undefined, {
responseFormat: { type: "text" },
});
const sig2 = generateSignature("gpt-4o", messages, 0, 1, undefined, {
responseFormat: { type: "json_object" },
});
assert.notEqual(sig1, sig2);
});
it("generates identical signatures when constraints are identical (no hit-rate regression)", () => {
const tools = [{ type: "function", function: { name: "get_weather", parameters: {} } }];
const constraints = {
toolChoice: "auto",
tools,
responseFormat: { type: "json_object" },
};
const sig1 = generateSignature("gpt-4o", messages, 0, 1, undefined, constraints);
const sig2 = generateSignature("gpt-4o", messages, 0, 1, undefined, {
toolChoice: "auto",
tools: [{ type: "function", function: { name: "get_weather", parameters: {} } }],
responseFormat: { type: "json_object" },
});
assert.equal(sig1, sig2);
});
it("generates identical signatures for omitted constraints vs an explicitly empty constraints object", () => {
const sig1 = generateSignature("gpt-4o", messages, 0, 1, undefined);
const sig2 = generateSignature("gpt-4o", messages, 0, 1, undefined, {});
assert.equal(sig1, sig2);
});
});
});
describe("isCacheableForRead", () => {