Skip to content

Responses adapter hygiene: reasoning drop, dedupe, indexer leak, tool-name codec, double SSE parse (CL-6912) - #566

Merged
TheGreatAxios merged 3 commits into
mainfrom
cl-6912-responses-adapter-hygiene
Aug 23, 2026
Merged

Responses adapter hygiene: reasoning drop, dedupe, indexer leak, tool-name codec, double SSE parse (CL-6912)#566
TheGreatAxios merged 3 commits into
mainfrom
cl-6912-responses-adapter-hygiene

Conversation

@TheGreatAxios

@TheGreatAxios TheGreatAxios commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

CL-6912. Grab-bag hygiene fixes across the three Responses adapters (codex-responses-adapter.ts, grok-responses-adapter.ts, openai-responses-adapter.ts) and openai-compatible-adapter.ts.

This touches a provider adapter inside the inference loop. Needs an eval matrix run before merge — do not auto-merge. needs-sawyer-review.

Per-defect verdict

# Defect Live on main? Fix Test
1 signatureForModel requires turn.model === requestModel; model is optional on the persisted turn schema, so turns saved without it lose reasoning on resume Live, and in two places signatureForModel in codex-responses-adapter.ts was the visible site but not the load-bearing one: every real request goes through withReplaySanitizer first (src/provider/inference-dependencies.ts), whose sanitizeReplayTurns (replay-sanitizer.ts) had the identical undefined-model-is-foreign bug in stripForeignBlocks's gate, deleting the signature before the adapter ever ran. Fixed both. The vendored transformMessages (in vendor/intx-inference, not editable) has the same same-model check baked in, so a model-less assistant turn is now stamped with the target model before either sanitizer stage runs, rather than trying to loosen the vendored check. Genuinely foreign turns (a real, different model) are untouched. Also added: a genuine model mismatch now drops the function_call item(s) that the dropped reasoning produced, instead of leaving them orphaned — the exact shape that degenerates reasoning models codex-responses-adapter.test.ts: signatureForModel + "orphaned function_call suppression" describe blocks (direct-adapter unit tests); replay-sanitizer.test.ts: "carries a reasoning signature through the real buildRequest path when the turn has no model" — goes through withReplaySanitizer end-to-end, the actual production path, and fails without the replay-sanitizer.ts fix even with the adapter fix in place
2 dedupeToolOutputs keeps the stale FIRST function_call_output on a duplicate call_id; duplicate function_call items aren't deduped at all Live (in grok-responses-adapter.ts and openai-responses-adapter.ts, the two files that have this function) Renamed to dedupeToolItems; keeps the LAST occurrence for both function_call and function_call_output grok-responses-adapter.test.ts: "keeps the latest function_call_output..." / "dedupes a duplicate function_call..."; updated the now-incorrect pre-existing assertion in tests/unit/grok-responses-adapter.test.ts that expected the stale-first behavior
3 The block indexer is created once per adapter instance and never reset across requests Correctness bug in isolation, but likely not reachable live: the adapter registry never memoizes (documented at vendor/intx-inference/src/adapter.ts:146) and the harness resolves a fresh adapter per generator invocation, so each HTTP round trip probably already gets a new adapter object in practice. Fixed anyway — defensively correct and removes the dependency on that memoization behavior Indexer is now recreated inside buildRequest, so each HTTP round trip starts a fresh indexer regardless of adapter lifetime codex-responses-adapter.test.ts: "block indexer reset" describe block
4 All three Responses adapters bypass the tool-name encode/decode codec the vendored adapters use Live toResponsesTools/toResponsesItems now call encodeToolName, and the shared parseResponse's response.output_item.added handler calls decodeToolName, using a shared RESPONSES_TOOL_NAME_LIMIT (64 chars, same OpenAI wire charset) codex-responses-adapter.test.ts: "tool-name codec" describe block
5 openai-compatible parses every SSE frame twice (JSON.parse → restringify → base parses again) Same caveat as #3 — real-world exposure likely limited by adapter freshness per invocation, but the double parse itself was unconditional in the code as written, independent of adapter lifetime, so it's fixed regardless The null-delta-field patch is DeepSeek/NIM-specific (same condition the request-body patch already used); a closure flag set in buildRequest now gates it, so every non-DeepSeek frame calls base.parseResponse directly with a single parse openai-compatible-adapter.test.ts: "SSE parse count" describe block

CL-6904 (prompt_cache_key) and CL-6905 (cross-provider signature tagging) already landed in this area and are untouched — this PR builds on top of the provider-tagged signature scheme CL-6905 introduced.

Did not pursue the CL-6811 grok-adapter fork-dedup consolidation mentioned as optional in the ticket — it would have widened this diff considerably without changing behavior, and increases the odds of an eval regression outside this ticket's scope.

Noticed but not touched (out of scope)

  • codex-responses-adapter.ts's buildRequest never calls a dedupe pass at all (unlike grok/openai-responses) — same duplicate-call_id class of bug could reach Codex, just via a different code path. Left alone since the ticket's defect list names the existing dedupeToolOutputs function specifically, which codex doesn't have.

- signatureForModel no longer drops reasoning when a persisted turn is
  missing its model field; a genuine model mismatch now also drops the
  function_call items that reasoning produced, avoiding the orphaned
  shape that degenerates reasoning models
- dedupeToolOutputs (renamed dedupeToolItems) keeps the latest
  function_call/function_call_output on a duplicate call_id instead of
  the stale first one, and now covers duplicate function_call items too
- the Responses block indexer is recreated per buildRequest instead of
  once per adapter instance, fixing an unbounded per-conversation leak
- all three Responses adapters route tool names through the shared
  encode/decode codec instead of sending raw package-qualified ids
- openai-compatible only re-parses SSE frames for DeepSeek/NIM models;
  every other frame hits the base parser once instead of twice
@linear-code

linear-code Bot commented Aug 23, 2026

Copy link
Copy Markdown

CL-6912

… too

sanitizeReplayTurns runs before the adapter's own buildRequest via
withReplaySanitizer, and its stripForeignBlocks gate had the identical
undefined-model-is-foreign bug as signatureForModel, deleting the
signature before the adapter ever saw it. The vendored transformMessages
has the same same-model check and is not ours to change, so a model-less
assistant turn is now stamped with the target model before either stage
runs, instead of loosening either stage's foreign check directly.

Genuine cross-provider turns (a turn with a real, different model) are
untouched -- still stripped, still fail closed.
@TheGreatAxios
TheGreatAxios merged commit c1ac433 into main Aug 23, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant