fix(sse): default reasoning summary for effort-only Responses requests (#6807)

A Chat-Completions client can only express reasoning via the top-level
reasoning_effort hint and has no way to request a reasoning summary. When
that hint is promoted to the Responses API's reasoning.effort, the
upstream returns an empty summary and downstream chat clients see no
thinking stream (encrypted reasoning only).

Default reasoning.summary "auto" plus include ["reasoning.encrypted_content"]
on the effort-only path so the summary actually streams back to the chat
client, mirroring the Codex executor's ensureCodexReasoningSummary. An
explicit reasoning object from a Responses-shaped client is preserved
untouched, and reasoning_effort "none" is left without a summary.

Adds regression tests for the effort-only default, the none case, and
keeps the existing explicit-reasoning-object behavior unchanged.

Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
Jade Guo
2026-07-12 13:21:38 +08:00
committed by GitHub
parent 78ce492576
commit 10807972db
3 changed files with 51 additions and 4 deletions

View File

@@ -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];

View File

@@ -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" });
});

View File

@@ -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<string, unknown>).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<string, unknown>;
const reasoning = record.reasoning as Record<string, unknown> | 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);
});