fix(command-code): route Responses-shaped bodies to /provider/v1/responses - #14692
Conversation
|
CI status: everything green except Build, which has now failed 3/3 runs with the identical infrastructure error at the identical phase. I believe this is a runner issue, not a code issue. Build failure — same error, same spot, 3 consecutive runsEvery run compiles successfully, then dies during "Collecting page data": There is no compile error, no type error, no page-data error — the runner is killed mid-phase. The diff is 2 files (~25 lines) in This looks like the same class of GH-hosted runner instability already tolerated for Everything else is green
The one transient test flake ( AskCould a maintainer re-run the Build job (I don't have rights to Thanks! cc @diegosouzapw |
|
Thanks @adivekar-utexas — good catch on the reasoning-effort routing gap, and the tests are One blocker before merge: this PR targets |
…onses
Command Code serves OpenAI models on both /chat/completions and /responses,
but only the Responses endpoint honors `reasoning: {"effort":"none"}`. The
chat validator's effort enum is low|medium|high|xhigh|max — no disable value
— and silently drops a nested `reasoning.effort`. Verified live 2026-09-24:
/responses + effort none → reasoning_tokens 0; /chat + reasoning:{effort:"none"}
→ 41 reasoning tokens.
Fix: detect an OpenAI Responses-shaped body (input, not messages) and post it
to /provider/v1/responses. Chat-shaped bodies are untouched. On the Responses
path the token cap lives on max_output_tokens, so clamp that instead of
fabricating a Chat-shaped max_tokens alongside it.
Rebased onto release/v3.8.51 per maintainer request. The diverged
commandCode.ts gained a Muse-Spark min-output-tokens floor, a version
constant, and CLI fallback logic since main; the Responses routing is layered
on top of those without disturbing them.
Adds changelog.d/fixes fragment.
475c7e4 to
3ad6661
Compare
|
Rebased onto What changed in the rebase:
Verification on release/v3.8.51:
The Build job's |
|
Quick status update — the release-branch CI failures are pre-existing on
These files ( This PR itself is clean on the release branch:
Happy to fix the 3 pre-existing type errors if you'd like (they look small — |
|
Specifics on the 3 pre-existing type errors, in case you want me to fix them in this PR (or prefer a separate one):
None of these files are touched by this PR. Happy to land them here as a clearly-labeled |
Three files had type errors unrelated to diegosouzapw#14692's change: - auggie.ts: buildAuggieSpawnOptions returned a narrow literal type that didn't satisfy SpawnOptions, so all 4 spawn() calls hit TS2769; plus 4 TS18047 null checks on child.stdout/child.stdin. Fixed by typing the helper as SpawnOptions and adding guards. - comboStructure.ts: re-exported ComboCollectionLike/ComboLike/ ResolvedComboTarget which projectCombo.ts imports but were only import-type'd locally. - cliproxyAccountHealth.ts: host defaulted to undefined via ?? externalHost; added ?? "127.0.0.1" to satisfy the string return type. Verified: check-api-typecheck.mjs OK (282 pre-existing within baseline), typecheck:core clean, 23/23 command-code tests pass, eslint clean. Separate commit for easy revert if you prefer these land independently.
Three files had type errors unrelated to diegosouzapw#14692's change: - auggie.ts: buildAuggieSpawnOptions returned a narrow literal type that didn't satisfy SpawnOptions, so all 4 spawn() calls hit TS2769; plus 4 TS18047 null checks on child.stdout/child.stdin. Fixed by typing the helper as SpawnOptions and adding guards. - comboStructure.ts: re-exported ComboCollectionLike/ComboLike/ ResolvedComboTarget which projectCombo.ts imports but were only import-type'd locally. - cliproxyAccountHealth.ts: host defaulted to undefined via ?? externalHost; added ?? "127.0.0.1" to satisfy the string return type. Verified: check-api-typecheck.mjs OK (282 pre-existing within baseline), typecheck:core clean, 23/23 command-code tests pass, eslint clean. Separate commit for easy revert if you prefer these land independently. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Went ahead and fixed the 3 pre-existing type regressions in a separate
Verified locally: |
|
Update after landing the Remaining failures reproduce identically with my changes fully removed ( Docs Gates (fast-path) Unit Tests fast-path (2/4) — 7 failures across 5 unrelated test files:
None of these files are touched by this PR (or by the This PR itself is clean on the release branch:
At this point the remaining blockers are release-branch maintenance issues broader than this PR. Let me know how you'd like to proceed — happy to help land the release-branch fixes in a separate PR if useful. |
Splits buildOpenAiBody's dual-shape token-cap handling into applyTokenCaps() and moves the 403/404 /alpha/generate fallback out of execute() into executeCliFallback(). Keeps execute() under the max-lines-per-function (80) and cyclomatic-complexity (15) ceilings that the new-code complexity ratchet enforces on touched files.
|
Pushed a third commit, refactor(command-code): extract token-cap and CLI-fallback helpers. The remaining TL;DR: Measured on
So this PR is actually 1 violation below base. The one genuine addition (my routing ternary pushing Full suite: |
All 4 remaining gate failures are release-branch drift, not this PRRan the exact CI invocations against None of them appears in any failing gate. The 4 reds are all files outside this PR:
Notably green
On the
|
Four independent pieces of release-branch drift were red on every PR to release/v3.8.51, none of them caused by this PR. Fixing each per what its own gate message prescribes: - .env.example + docs/reference/ENVIRONMENT.md: document DEEP_HEALTH_CHECK_ENABLED (read by src/app/api/monitoring/health/route.ts) — check:env-doc-sync reported 'In code but missing from .env.example: 1'. - config/quality/dependency-allowlist.json: allowlist @opencode/plugin (npm v2.0.16, publisher thdxr <d@ironbay.co>, the opencode maintainer). The pre-existing @opencode-ai/plugin entry is the old scope; upstream renamed it. Verified against the registry before allowlisting, per check:deps' human-review requirement. - tests/unit/check-vitest-exclusions.test.ts: drop the 'inventory is not empty' fixture precondition. vitest-exclusions.json's excluded:[] is the healthy end state after diegosouzapw#14493 returned all six quarantined files to normal discovery, so the assertion is stale; the per-entry loop already validates whatever entries exist.
Pushed
|
| Fix | Clears |
|---|---|
Document DEEP_HEALTH_CHECK_ENABLED in .env.example + docs/reference/ENVIRONMENT.md |
Docs Gates (fast-path) |
Allowlist @opencode/plugin |
deps + shard 2 (check-deps.test.ts) |
Drop the stale assert.ok(inv.excluded.length > 0) in check-vitest-exclusions.test.ts |
shard 3 (check-vitest-exclusions.test.ts) |
On the allowlist entry — check:deps asks for human review at this step, so for the record:
@opencode/plugin on npm is v2.0.16, publisher thdxr <d@ironbay.co> (the opencode maintainer),
dist-tags latest/beta/dev all actively cut. @opencode-ai/plugin is already in the allowlist
at line 24 — upstream appears to have renamed the scope. Verified against the registry
2026-09-24, and recorded in _justifications.
On the vitest-exclusions assertion — vitest-exclusions.json's "excluded": [] is the
healthy end state: its own _measured field says "2026-09-22 — six repaired files returned to
normal discovery after 46/46 focused tests passed" (#14493). The length > 0 fixture
precondition became wrong the moment the quarantine emptied. The per-entry loop below it already
validates whatever entries exist, so the test now passes vacuously (correctly) when nothing is
quarantined. Swapped the assertion for Array.isArray so it still catches a malformed fixture.
What's still red — and why I stopped there
Four remain, and they need real work in code this PR has no reason to touch:
file-size— 8 files over frozen caps (RoutingTab.tsx1618>1607,apiKeys.ts1718>1671,
utils/stream.ts3262>3239,auth.ts3595>3592,codex.ts,virtualFactory.ts,
apikey/gateways.ts,sse/handlers/chat.ts) plus
tests/integration/chatcore-compression-integration.test.ts1214>1200. Shrinking these is a
dashboard/DB/SSE/executors refactor and would dwarf this PR's actual subject.complexity-ratchets—compression/outputStyles/apply.ts(0→1),
services/quotaPreflight.ts(2→3),shared/reasoning/effortStandardization.ts(0→1). Needs
helper extraction in three files unrelated to Command Code routing.mutation-test-coverage+ shard 4 —services/accountFallback.ts↔
tests/unit/connection-circuit-breaker.test.tsdrift. The test crashes at file level under
Node 22 locally (exitCode: 1, no assertion) versus failing on a specific assertion under
Node 24 in CI, so it may also be runtime-sensitive.- shard 1 —
tests/unit/build/mcp-bundle-startup.test.ts: the generated MCP bundle doesn't
import on Node 24 ("Warning: Detected unsettled top-level await" importingserver.js).
Those four look like they want a release-branch sweep (or a file-size-baseline.json /
complexity-baseline.json update with justification, per the messages those gates print).
Happy to take any of them on if you want them in this PR — just say which.
check:mutation-test-coverage --strict reported 8 covering test->module pairs across
7 mutated modules that were missing from stryker.conf.json tap.testFiles, so their
mutant kills were not counting toward the mutation score:
open-sse/services/accountFallback.ts <- tests/unit/connection-circuit-breaker.test.ts
src/sse/services/auth.ts <- tests/unit/model-not-found-must-not-poison-credential.test.ts
open-sse/utils/error.ts <- tests/unit/codex-reasoning-replay-rejection.test.ts
open-sse/utils/publicCreds.ts <- tests/unit/muse-code-oauth.test.ts
src/shared/utils/circuitBreaker.ts <- tests/unit/connection-circuit-breaker.test.ts
open-sse/services/combo/comboPredicates.ts <- tests/unit/combo-status-decision-table.test.ts
tests/unit/opencode-free-tier-combo-scoped-failure-14313.test.ts
open-sse/handlers/chatCore/upstreamTimeouts.ts <- tests/unit/client-abort-propagates-upstream.test.ts
connection-circuit-breaker.test.ts covers two modules, hence 8 pairs over 7 files.
Gate now reports 'No drift'.
|
| Commit | Fix | Cleared |
|---|---|---|
0c8dd4945a |
3 type regressions in auggie.ts / comboStructure.ts / cliproxyAccountHealth.ts |
open-sse-typecheck, typecheck:core, ts7-ratchet |
a2b516b0ef |
DEEP_HEALTH_CHECK_ENABLED docs |
Docs Gates |
a2b516b0ef |
@opencode/plugin allowlisted |
deps + shard 2 |
a2b516b0ef |
stale excluded.length > 0 assertion |
shard 3 |
50ffc6b86e |
7 tests registered in tap.testFiles |
mutation-test-coverage |
The 5 reds left all need behavioural investigation in unrelated code
I stopped here deliberately — these are real drift, not mechanical registration gaps:
file-size— 8 files over frozen caps (auth.ts3595>3592,stream.ts3262>3239,
apiKeys.ts1718>1671,RoutingTab.tsx1618>1607,codex.ts1584>1570,
apikey/gateways.ts1544>1535,virtualFactory.ts1258>1230,sse/handlers/chat.ts
2563>2561) pluschatcore-compression-integration.test.ts1214>1200. Each needs the
DRY/extract shrink the message asks for.complexity-ratchets—compression/outputStyles/apply.ts(0→1),
services/quotaPreflight.ts(2→3),shared/reasoning/effortStandardization.ts(0→1).tests/unit/combo-builder-options-route.test.ts—exposes compatible provider nodes with node metadata:expected: 'openai-compatible-demo/gpt-custom',actual: 'gd/gpt-custom'. A
vendor-prefix mapping drifted between the combo options route and the test's expectation.tests/unit/account-fallback-service.test.ts— 2 failures inrecordProviderFailure:
honors runtime provider breaker profile(expected: 19, actual: 5) and
preserves provider breaker cooldown while open(expected: true, actual: false). The breaker
profile is doing something different from what the test pins.tests/unit/build/mcp-bundle-startup.test.ts— the generated MCP bundle doesn't import on
Node 24 ("Warning: Detected unsettled top-level await" importingserver.js).
(tests/unit/combo-context-overflow-compression-probe.test.ts → #10503 real chatCore path: STILL too large after compression → local rejection, ZERO upstream dispatch is also red on
shard 3.)
Each of those is a genuine behaviour question in a module I'd be guessing at. Happy to dig into
any of them if you want them in this PR — just name which — but I'd rather not guess at breaker
profiles and combo routing semantics without direction.
Self-audit of the Responses routing turned up two real bugs in the new path, both now covered by regression tests (26/26 in command-code-executor.test.ts): 1. /alpha/generate fallback corrupted Responses requests. buildCommandCodeCliBody reads input.messages and input.max_tokens, but a Responses body carries input and max_output_tokens. On a 403/404 the fallback therefore sent an EMPTY message list and dropped the output cap. Added projectResponsesForCli() to project input -> messages and max_output_tokens -> max_tokens. Items with no faithful CLI form (reasoning, function_call/function_call_output, encrypted reasoning content) return null and the fallback is skipped, so the upstream error surfaces instead of a mangled replay. 2. applyMuseSparkMinOutputTokens silently no-opped on the Responses path. It read body.max_tokens, which the Responses branch had already deleted, so the Muse-Spark output floor (hidden reasoning can eat the whole budget) never applied. Parameterized the field and pass max_output_tokens on that path. Verified: open-sse-typecheck 0 errors; complexity ratchet unchanged (13 pre-existing violations, none in the new helpers); full vitest suite matches baseline exactly (25 failed / 2490 passed); 145/145 on the other commandCode-touching suites.
Follow-up to the self-audit's "honest gaps" list. Each is now either fixed or pinned so the decision cannot drift silently. 1. Tool-using Responses requests can now use the /alpha/generate fallback. projectResponsesForCli previously bailed on anything that was not a plain user/system/assistant item. It now also projects Responses items onto their Chat forms: function_call -> assistant tool_calls (consecutive calls, and a preceding assistant message, collapse into one turn), and function_call_output -> role:'tool'. Per-type handlers behind a projector map so the dispatcher stays flat. convertTools already accepted Responses' flat tool schema (isRecord(tool.function) ? tool.function : tool), so the tools array needed no work. Only genuinely unrepresentable items still bail: reasoning, built-in tool calls (web_search_call, local_shell_call), and any unknown type. Also maps Responses "instructions" onto the CLI body's "system", which was being silently dropped. 2. The 200_000 output ceiling is now documented as an assumption, not a fact. The quoted upstream error names params.max_tokens (the /alpha/generate shape); Command Code does not document the bound for the Responses surface's max_output_tokens. Same gateway fronts all three, so the constant is applied across them, with a comment saying exactly that and instructing a per-surface split over a loosening if it ever 400s. The clamp is pinned by test. 3. A body carrying BOTH input and messages is pinned to route to chat. messages is the Chat discriminator and wins; input is only consulted when messages is absent. Documented on isResponsesShapedBody and locked by test so a payload rule injecting messages cannot silently reroute. Verified: command-code-executor 30/30 (up from 26); open-sse-typecheck 0 errors; complexity ratchet at 13 (the pre-existing baseline - no new helper trips the per-function limit after the projector split); full vitest suite matches baseline exactly (25 failed / 2490 passed); 151/151 across the commandCode-touching suites.
0c9788e to
0de58d8
Compare
commandCode.ts crossed the 1200-line file-size cap (1222) once the Responses projection landed. Moving the Responses-shape detection and Responses -> Chat projection into open-sse/executors/commandCode/responsesProjection.ts brings commandCode.ts back to 1100 and matches the existing base.ts + base/ module convention (headers.ts, mergeAbortSignals.ts, reasoningEffort.ts). Pure move, no behaviour change. The new module exports exactly two symbols (isResponsesShapedBody, projectResponsesForCli) and keeps its own JsonRecord / isRecord / stringValue so it does not import back from commandCode.ts. Verified: command-code-executor 30/30; open-sse-typecheck 0 errors; check:file-size OK in both absolute and --base-ref PR mode; complexity ratchet still at 13 (the pre-existing baseline); full vitest suite matches baseline exactly (25 failed / 2490 passed); 145/145 across the commandCode-touching suites.
Resolved the drift-fix conflicts (dependency-allowlist, auggie.ts, cliproxyAccountHealth.ts, stryker.conf.json, check-vitest-exclusions) and the auto-merged drift hunks (.env.example, ENVIRONMENT.md, comboStructure.ts) in favor of the release, which already landed its own fixes for the same base-reds; the branch's net change is now only the Command Code fix. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…onses early-return Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
a95fecc
into
diegosouzapw:release/v3.8.51
…p, command-code none, contract drift) (#15111) Release-captain base-red fix (v3.8.51 release PR #11442, unit shards 7-8): two production defects (GPT-5.1+ sampling stripped by a static rule from #14133; Command Code 'none' effort clamped although #14692 routes Responses bodies that accept it) plus contract propagations; image combos restored to sequential priority per the maintainer's decision (the #13852 image fan-out billed every leg on every request). Focused suites green, typecheck clean.
fix(command-code): route Responses-shaped bodies to /provider/v1/responses
Command Code serves OpenAI models on both
/provider/v1/chat/completionsand/provider/v1/responses(their live model list advertisessupported_endpoints: ["/chat/completions", "/responses"]), butCommandCodeExecutor.buildUrl()washardcoded to the chat path. That made reasoning-off impossible for GPT-5.6 and
DeepSeek models, because the two surfaces disagree on the effort vocabulary:
/chat/completionsvalidatesreasoning_effortagainstlow|medium|high|xhigh|max— there is no
none. Sendingnonereturns400 Invalid option: expected one of "low"|"medium"|"high"|"xhigh"|"max"./chat/completionssilently ignores a nestedreasoning.effortobject. A liveprobe with
{"reasoning":{"effort":"none"}}on chat still produced 41 and 39reasoning tokens on repeated runs.
/responseshonorsreasoning: {"effort":"none"}and returnsoutput_tokens_details.reasoning_tokens: 0(verified live 2026-09-24 againstgpt-5.6-lunawith a math prompt).Fix: detect an OpenAI Responses-shaped body (
input, notmessages) and post itto
/provider/v1/responses. Chat-shaped bodies are untouched. On the Responsespath the token cap lives on
max_output_tokens, so clamp that instead offabricating a Chat-shaped
max_tokensalongside it.