From 8372a3c7ca951a92f16d2ce11b1fc928b9e6de1b Mon Sep 17 00:00:00 2001 From: Chris Staley Date: Wed, 1 Apr 2026 19:26:34 -0600 Subject: [PATCH 1/3] fix(translator): emit response.completed with total_tokens for Responses API clients MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hub-and-spoke flush path was broken for non-OpenAI providers when the client speaks OpenAI Responses API (e.g. Codex CLI). When claudeToOpenAIResponse(null) returned null, openaiToOpenAIResponsesResponse was never called, so response.completed (carrying total_tokens) was never emitted — causing "missing field total_tokens" errors in Codex. Two fixes: - Pass null through to Step 2 translator even when Step 1 produced no output during flush, so terminal events get emitted - Capture usage from any chunk carrying it (not just usage-only chunks) and normalize Chat Completions format to Responses API format --- open-sse/translator/index.ts | 9 +++++++++ .../translator/response/openai-responses.ts | 19 +++++++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/open-sse/translator/index.ts b/open-sse/translator/index.ts index 79be284b99..f00005c632 100644 --- a/open-sse/translator/index.ts +++ b/open-sse/translator/index.ts @@ -267,6 +267,15 @@ export function translateResponse(targetFormat, sourceFormat, chunk, state) { finalResults.push(...(Array.isArray(converted) ? converted : [converted])); } } + // Flush: pass null to source-format translator even when Step 1 produced no output. + // This is critical for formats like openai-responses that emit terminal events + // (e.g., response.completed with total_tokens) in their flush handler. + if (chunk === null && results.length === 0) { + const converted = fromOpenAI(null, state); + if (converted) { + finalResults.push(...(Array.isArray(converted) ? converted : [converted])); + } + } results = finalResults; } } diff --git a/open-sse/translator/response/openai-responses.ts b/open-sse/translator/response/openai-responses.ts index 40ec923bd4..5c40f60af0 100644 --- a/open-sse/translator/response/openai-responses.ts +++ b/open-sse/translator/response/openai-responses.ts @@ -14,11 +14,22 @@ export function openaiToOpenAIResponsesResponse(chunk, state) { return flushEvents(state); } - if (!chunk.choices?.length) { - // Capture usage from usage-only chunks (stream_options.include_usage) - if (chunk.usage) { - state.usage = chunk.usage; + // Capture usage from any chunk that carries it (usage-only chunks OR final chunks with finish_reason) + // Normalize Chat Completions format (prompt_tokens/completion_tokens) to Responses API format + // (input_tokens/output_tokens) so response.completed always has the fields Codex expects. + if (chunk.usage) { + const u = chunk.usage; + state.usage = { + input_tokens: u.input_tokens ?? u.prompt_tokens ?? 0, + output_tokens: u.output_tokens ?? u.completion_tokens ?? 0, + total_tokens: u.total_tokens ?? (u.input_tokens ?? u.prompt_tokens ?? 0) + (u.output_tokens ?? u.completion_tokens ?? 0), + }; + if (u.prompt_tokens_details?.cached_tokens) { + state.usage.input_tokens_details = { cached_tokens: u.prompt_tokens_details.cached_tokens }; } + } + + if (!chunk.choices?.length) { return []; } From 304664b31823e1f79d57faa737f54089c2d562bc Mon Sep 17 00:00:00 2001 From: Chris Staley Date: Wed, 1 Apr 2026 19:46:18 -0600 Subject: [PATCH 2/3] test: update usage field assertions to Responses API format The normalization to input_tokens/output_tokens/total_tokens changed the usage field names. Update test to assert on the correct fields. --- tests/unit/responses-translation-fixes.test.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/responses-translation-fixes.test.mjs b/tests/unit/responses-translation-fixes.test.mjs index 6d66f7ed73..61785b04c6 100644 --- a/tests/unit/responses-translation-fixes.test.mjs +++ b/tests/unit/responses-translation-fixes.test.mjs @@ -369,7 +369,9 @@ test("Chat→Responses streaming: usage-only chunk is captured (not dropped)", ( const completedEvent = finishEvents.find((e) => e.event === "response.completed"); assert.ok(completedEvent, "should have completed event"); assert.ok(completedEvent.data.response.usage, "completed event should include usage"); - assert.equal(completedEvent.data.response.usage.prompt_tokens, 10); + assert.equal(completedEvent.data.response.usage.input_tokens, 10); + assert.equal(completedEvent.data.response.usage.output_tokens, 5); + assert.equal(completedEvent.data.response.usage.total_tokens, 15); }); test("Chat→Responses streaming: completed event includes accumulated output", () => { From 9588c1ea3e1a8aa5fb0fda61f709c150a3df970f Mon Sep 17 00:00:00 2001 From: Chris Staley Date: Wed, 1 Apr 2026 20:03:31 -0600 Subject: [PATCH 3/3] refactor(translator): extract usage token constants for readability Extract input_tokens/output_tokens into local constants to avoid repeating nullish coalescing chains in the total_tokens calculation. --- open-sse/translator/response/openai-responses.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/open-sse/translator/response/openai-responses.ts b/open-sse/translator/response/openai-responses.ts index 5c40f60af0..25a4de58c9 100644 --- a/open-sse/translator/response/openai-responses.ts +++ b/open-sse/translator/response/openai-responses.ts @@ -19,10 +19,12 @@ export function openaiToOpenAIResponsesResponse(chunk, state) { // (input_tokens/output_tokens) so response.completed always has the fields Codex expects. if (chunk.usage) { const u = chunk.usage; + const input_tokens = u.input_tokens ?? u.prompt_tokens ?? 0; + const output_tokens = u.output_tokens ?? u.completion_tokens ?? 0; state.usage = { - input_tokens: u.input_tokens ?? u.prompt_tokens ?? 0, - output_tokens: u.output_tokens ?? u.completion_tokens ?? 0, - total_tokens: u.total_tokens ?? (u.input_tokens ?? u.prompt_tokens ?? 0) + (u.output_tokens ?? u.completion_tokens ?? 0), + input_tokens, + output_tokens, + total_tokens: u.total_tokens ?? input_tokens + output_tokens, }; if (u.prompt_tokens_details?.cached_tokens) { state.usage.input_tokens_details = { cached_tokens: u.prompt_tokens_details.cached_tokens };