From 298ad0fd64954bcc9df532facd584cdae700fefb Mon Sep 17 00:00:00 2001 From: "Bob.Hou" Date: Mon, 31 Aug 2026 13:10:26 -0400 Subject: [PATCH] fix(translator): strip plaintext reasoning content for opaque responses backends (#12128) (#12171) Signed-off-by: Minxi Hou --- open-sse/services/reasoningInputPolicy.ts | 14 ++- open-sse/translator/index.ts | 9 ++ ...es-plaintext-reasoning-input-12128.test.ts | 88 +++++++++++++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 tests/unit/responses-plaintext-reasoning-input-12128.test.ts diff --git a/open-sse/services/reasoningInputPolicy.ts b/open-sse/services/reasoningInputPolicy.ts index e6c049f614..da170740e2 100644 --- a/open-sse/services/reasoningInputPolicy.ts +++ b/open-sse/services/reasoningInputPolicy.ts @@ -43,7 +43,19 @@ export function resolveReasoningTransport( ): ReasoningTransport { const normalized = typeof provider === "string" ? provider.trim().toLowerCase() : ""; const transport = REASONING_TRANSPORTS.get(normalized); - return transport ?? (preserveEncryptedReasoning ? "opaque" : "plaintext"); + if (transport) return transport; + // #12128: Generic Responses-protocol endpoints (e.g. openai-compatible-responses-*, + // custom-openai-responses, proxy backends) implement the OpenAI/Codex Responses API + // where reasoning input items cannot accept plaintext content (maxItems: 0). + if ( + normalized.startsWith("openai-compatible-responses") || + normalized.startsWith("custom-openai-responses") || + normalized.includes("codex") || + normalized.includes("responses") + ) { + return "opaque"; + } + return preserveEncryptedReasoning ? "opaque" : "plaintext"; } function asRecord(value: unknown): JsonRecord | null { diff --git a/open-sse/translator/index.ts b/open-sse/translator/index.ts index 64b3a93f0c..eda968bae1 100644 --- a/open-sse/translator/index.ts +++ b/open-sse/translator/index.ts @@ -40,6 +40,7 @@ import { normalizeResponsesReasoningEffort, RESPONSES_STORE_MARKER, } from "./request/openai-responses/helpers.ts"; +import { applyReasoningInputPolicy } from "../services/reasoningInputPolicy.ts"; bootstrapTranslatorRegistry(); export { register } from "./registry.ts"; @@ -575,6 +576,14 @@ export function translateRequest( // Normalize openai-responses input shape for providers that require list input. if (targetFormat === FORMATS.OPENAI_RESPONSES) { result = normalizeOpenAIResponsesRequest(result); + // #12128: Sanitize reasoning input items for Responses targets (strip plaintext content for opaque backends) + applyReasoningInputPolicy(result as Record, "responses", { + provider, + preserveEncryptedReasoning: + (credentials as { providerSpecificData?: { preserveEncryptedReasoning?: boolean } } | null) + ?.providerSpecificData?.preserveEncryptedReasoning === true, + onIncompatibleReasoning: "drop", + }); } // Second role normalization: only for OPENAI_RESPONSES. Here messages are built from input diff --git a/tests/unit/responses-plaintext-reasoning-input-12128.test.ts b/tests/unit/responses-plaintext-reasoning-input-12128.test.ts new file mode 100644 index 0000000000..efbf7ed01a --- /dev/null +++ b/tests/unit/responses-plaintext-reasoning-input-12128.test.ts @@ -0,0 +1,88 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { translateRequest } = await import("../../open-sse/translator/index.ts"); +const { resolveReasoningTransport, applyReasoningInputPolicy } = await import( + "../../open-sse/services/reasoningInputPolicy.ts" +); +const { FORMATS } = await import("../../open-sse/translator/formats.ts"); + +test("#12128: resolveReasoningTransport identifies openai-compatible-responses and custom responses variants as opaque transport", () => { + assert.equal(resolveReasoningTransport("openai-compatible-responses-codex"), "opaque"); + assert.equal(resolveReasoningTransport("custom-openai-responses"), "opaque"); + assert.equal(resolveReasoningTransport("codex-proxy"), "opaque"); + assert.equal(resolveReasoningTransport("openai"), "opaque"); + assert.equal(resolveReasoningTransport("codex"), "opaque"); +}); + +test("#12128: translateRequest to Responses target strips plaintext reasoning.content for opaque responses providers", () => { + const chatBody = { + model: "gpt-5.6-codex", + messages: [ + { role: "user", content: "hello" }, + { + role: "assistant", + content: "Hi there!", + reasoning_content: "Let me think about how to greet the user properly.", + }, + { role: "user", content: "what is 2+2?" }, + ], + }; + + const translated = translateRequest( + FORMATS.OPENAI, + FORMATS.OPENAI_RESPONSES, + "gpt-5.6-codex", + chatBody, + false, + null, + "openai-compatible-responses-codex" + ) as Record; + + assert.ok(Array.isArray(translated.input), "translated.input must be an array"); + + const input = translated.input as Record[]; + const reasoningItems = input.filter((item) => item.type === "reasoning"); + + // For opaque responses targets without encrypted continuation, orphaned plaintext reasoning items + // must either be stripped entirely or have content.length === 0, so strict Codex backends do not 400. + for (const r of reasoningItems) { + assert.ok( + !r.content || (Array.isArray(r.content) && r.content.length === 0), + "reasoning item must not carry non-empty content array to strict responses endpoints" + ); + } +}); + +test("#12128: applyReasoningInputPolicy directly sanitizes replayed plaintext reasoning for openai-compatible-responses", () => { + const body: Record = { + input: [ + { + type: "reasoning", + content: [{ type: "reasoning_text", text: "step 1 plan" }], + summary: [], + }, + { + type: "message", + role: "assistant", + content: [{ type: "output_text", text: "Done" }], + }, + ], + }; + + applyReasoningInputPolicy(body, "responses", { + provider: "openai-compatible-responses-vllm", + onIncompatibleReasoning: "drop", + }); + + const input = body.input as Record[]; + const reasoningItems = input.filter((item) => item.type === "reasoning"); + + for (const r of reasoningItems) { + assert.equal( + r.content, + undefined, + "plaintext reasoning content must be dropped for opaque responses providers" + ); + } +});