diff --git a/open-sse/translator/index.ts b/open-sse/translator/index.ts index cc31aca671..86e39f2554 100644 --- a/open-sse/translator/index.ts +++ b/open-sse/translator/index.ts @@ -31,6 +31,7 @@ import { recordReplay, requiresReasoningReplay, } from "../services/reasoningCache.ts"; +import { normalizeResponsesReasoningEffort } from "./request/openai-responses/helpers.ts"; bootstrapTranslatorRegistry(); export { register } from "./registry.ts"; @@ -61,10 +62,37 @@ function normalizeResponsesInputItem(item) { return item; } +// Promote a stray top-level Chat-Completions-shaped `reasoning_effort` into the +// Responses-shaped `reasoning:{effort}` object, in place, removing the top-level key. +// No-op when `reasoning` is already present (an explicit Responses-shaped value always +// wins) or when `reasoning_effort` is absent. +// +// This exists for the SAME-FORMAT lane (source === target === OPENAI_RESPONSES), where +// translateRequest's hub-and-spoke translation block is skipped entirely (#7631): a +// caller that lands a top-level `reasoning_effort` there — e.g. applyNoThinkingAlias +// on the OpenAI path, which runs upstream of model-format resolution and cannot know +// yet whether the target lane is Responses-native — would otherwise reach the upstream +// with BOTH an unrecognized top-level field AND no `reasoning.effort`, so suppression +// silently does not take effect. The cross-format path (openai -> openai-responses) +// already performs the equivalent promotion in toResponses.ts; this covers the lane +// that promotion never runs on. +function promoteStrayReasoningEffort(body) { + if (!body || typeof body !== "object") return body; + if (body.reasoning !== undefined) return body; + if (body.reasoning_effort === undefined) return body; + + const effort = normalizeResponsesReasoningEffort(body.reasoning_effort); + if (effort) { + body.reasoning = { effort }; + } + delete body.reasoning_effort; + return body; +} + function normalizeOpenAIResponsesRequest(body) { if (!body || typeof body !== "object") return body; - const normalized = { ...body }; + const normalized = promoteStrayReasoningEffort({ ...body }); if (typeof normalized.input === "string") { normalized.input = [ diff --git a/tests/unit/default-reasoning-effort-6879.test.ts b/tests/unit/default-reasoning-effort-6879.test.ts index 1cff3f7e54..3ed6a592bc 100644 --- a/tests/unit/default-reasoning-effort-6879.test.ts +++ b/tests/unit/default-reasoning-effort-6879.test.ts @@ -137,3 +137,70 @@ test("a lane known to reject reasoning_effort still drops it downstream (delete- const stripped = stripUnsupportedParams("github", "claude-3-5-sonnet", body); assert.equal("reasoning_effort" in stripped, false); }); + +// --------------------------------------------------------------------------- +// #7631: the same-format /v1/responses lane (source === target === OPENAI_RESPONSES) +// skips translateRequest's hub-and-spoke translation block entirely, so a stray +// top-level `reasoning_effort` (set upstream by applyNoThinkingAlias on the OpenAI +// path, before model-format resolution knows the target lane is Responses-native) +// must still be promoted into the Responses-shaped `reasoning.effort`, or thinking +// suppression silently does not take effect on that lane. +// --------------------------------------------------------------------------- + +test("7631: translateRequest promotes a stray top-level reasoning_effort into reasoning.effort on the same-format OPENAI_RESPONSES lane", async () => { + const { translateRequest } = await import("../../open-sse/translator/index.ts"); + const { FORMATS } = await import("../../open-sse/translator/formats.ts"); + + const body: Record = { + model: "gpt-5.1-codex", + input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }], + reasoning_effort: "none", + }; + + const result = translateRequest( + FORMATS.OPENAI_RESPONSES, + FORMATS.OPENAI_RESPONSES, + "gpt-5.1-codex", + body + ); + + assert.equal("reasoning_effort" in result, false); + assert.deepEqual(result.reasoning, { effort: "none" }); +}); + +test("7631: same-format OPENAI_RESPONSES lane leaves an explicit reasoning object untouched (no double-promotion)", async () => { + const { translateRequest } = await import("../../open-sse/translator/index.ts"); + const { FORMATS } = await import("../../open-sse/translator/formats.ts"); + + const body: Record = { + model: "gpt-5.1-codex", + input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }], + reasoning: { effort: "high", summary: "auto" }, + }; + + const result = translateRequest( + FORMATS.OPENAI_RESPONSES, + FORMATS.OPENAI_RESPONSES, + "gpt-5.1-codex", + body + ); + + assert.deepEqual(result.reasoning, { effort: "high", summary: "auto" }); + assert.equal("reasoning_effort" in result, false); +}); + +test("7631: cross-format openai -> openai-responses promotion is unchanged (no regression)", async () => { + const { translateRequest } = await import("../../open-sse/translator/index.ts"); + const { FORMATS } = await import("../../open-sse/translator/formats.ts"); + + const body: Record = { + model: "gpt-5.1-codex", + messages: [{ role: "user", content: "hi" }], + reasoning_effort: "none", + }; + + const result = translateRequest(FORMATS.OPENAI, FORMATS.OPENAI_RESPONSES, "gpt-5.1-codex", body); + + assert.equal("reasoning_effort" in result, false); + assert.deepEqual(result.reasoning, { effort: "none" }); +});