diff --git a/open-sse/translator/request/openai-responses/toResponses.ts b/open-sse/translator/request/openai-responses/toResponses.ts index d52608496f..bda9113d08 100644 --- a/open-sse/translator/request/openai-responses/toResponses.ts +++ b/open-sse/translator/request/openai-responses/toResponses.ts @@ -17,6 +17,16 @@ import { normalizeResponsesReasoningEffort, } from "./helpers.ts"; +// A Chat-Completions client can only express reasoning via the top-level +// `reasoning_effort` hint; it has no way to request a reasoning summary. When we +// promote that hint to the Responses API's `reasoning.effort`, default the +// summary (plus the encrypted-content include that actually streams it) so a +// downstream chat client still sees a thinking stream instead of an empty +// summary. Mirrors the Codex executor's `ensureCodexReasoningSummary`. An +// explicit `reasoning` object from a Responses-shaped client is preserved as-is. +const DEFAULT_RESPONSES_REASONING_SUMMARY = "auto"; +const RESPONSES_REASONING_ENCRYPTED_CONTENT_INCLUDE = "reasoning.encrypted_content"; + // Chat Completions `response_format: { type: "json_schema" }` → Responses API `text.format`. // Merges into any existing `result.text` (e.g. verbosity) so structured-output schemas from // Chat clients survive the translation to the Responses/Codex upstream (#5933). @@ -329,11 +339,17 @@ export function openaiToOpenAIResponsesRequest( if (chatVerbosity) { result.text = { ...toRecord(result.text), verbosity: chatVerbosity }; } + let defaultedReasoningSummary = false; if (root.reasoning !== undefined) { result.reasoning = root.reasoning; } else if (root.reasoning_effort !== undefined) { const effort = normalizeResponsesReasoningEffort(root.reasoning_effort); - if (effort) { + if (effort && effort !== "none") { + // Effort-only chat request: default a reasoning summary so the upstream + // streams thinking back (see the constant's note above). + result.reasoning = { effort, summary: DEFAULT_RESPONSES_REASONING_SUMMARY }; + defaultedReasoningSummary = true; + } else if (effort) { result.reasoning = { effort }; } } @@ -345,6 +361,14 @@ export function openaiToOpenAIResponsesRequest( if (Array.isArray(root.include) && root.include.length > 0) { result.include = root.include; } + // When we defaulted a reasoning summary above, also request the encrypted + // reasoning content so the summary actually streams back to the chat client. + if (defaultedReasoningSummary) { + const include = Array.isArray(result.include) ? (result.include as unknown[]) : []; + if (!include.includes(RESPONSES_REASONING_ENCRYPTED_CONTENT_INCLUDE)) { + result.include = [...include, RESPONSES_REASONING_ENCRYPTED_CONTENT_INCLUDE]; + } + } if (storeEnabled) { if (root[RESPONSES_STORE_MARKER] !== undefined) { result.store = root[RESPONSES_STORE_MARKER]; diff --git a/tests/unit/openai-responses-reasoning-effort.test.ts b/tests/unit/openai-responses-reasoning-effort.test.ts index 9b8151bf15..a8a2927d64 100644 --- a/tests/unit/openai-responses-reasoning-effort.test.ts +++ b/tests/unit/openai-responses-reasoning-effort.test.ts @@ -68,5 +68,5 @@ test("Chat -> Responses already wraps reasoning_effort into reasoning.effort", ( {} ) ); - assert.deepEqual(out.reasoning, { effort: "high" }); + assert.deepEqual(out.reasoning, { effort: "high", summary: "auto" }); }); diff --git a/tests/unit/translator-openai-responses-req.test.ts b/tests/unit/translator-openai-responses-req.test.ts index 87e5c97f43..3fdda8c31a 100644 --- a/tests/unit/translator-openai-responses-req.test.ts +++ b/tests/unit/translator-openai-responses-req.test.ts @@ -599,11 +599,34 @@ test("Chat -> Responses maps reasoning_effort into Responses reasoning", () => { null ); - assert.deepEqual((result as any).reasoning, { effort: "low" }); + // Effort-only chat requests now default `summary: "auto"` + the encrypted + // reasoning include so Responses-API upstreams stream thinking back to the + // chat client (previously the summary was empty and no think was visible). + assert.deepEqual((result as any).reasoning, { effort: "low", summary: "auto" }); + assert.deepEqual((result as Record).include, ["reasoning.encrypted_content"]); assert.equal((result as any).reasoning_effort, undefined); assert.equal((result as any).store, false); }); +test("Chat -> Responses does not default a reasoning summary for reasoning_effort none", () => { + const result = openaiToOpenAIResponsesRequest( + "gpt-5.3-codex-spark", + { + messages: [{ role: "user", content: "Hello" }], + reasoning_effort: "none", + }, + false, + null + ); + + const record = result as Record; + const reasoning = record.reasoning as Record | undefined; + if (reasoning !== undefined) { + assert.equal(reasoning.summary, undefined); + } + assert.equal(record.include, undefined); +}); + test("Chat -> Responses normalizes reasoning_effort max to xhigh", () => { const result = openaiToOpenAIResponsesRequest( "gpt-5.5", @@ -615,7 +638,7 @@ test("Chat -> Responses normalizes reasoning_effort max to xhigh", () => { null ); - assert.deepEqual((result as any).reasoning, { effort: "xhigh" }); + assert.deepEqual((result as any).reasoning, { effort: "xhigh", summary: "auto" }); assert.equal((result as any).reasoning_effort, undefined); });