Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion open-sse/translator/request/openai-responses/toResponses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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 };
}
}
Expand All @@ -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];
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/openai-responses-reasoning-effort.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
});
27 changes: 25 additions & 2 deletions tests/unit/translator-openai-responses-req.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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);
});

Expand Down
Loading