diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 24532fda14..3f796d71c5 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -910,8 +910,21 @@ export async function handleChatCore({ } // ── Common input sanitization (runs for ALL paths including passthrough) ── - // #994: Normalize max_output_tokens to max_tokens for universal compatibility - if (body.max_output_tokens !== undefined) { + // #994: Normalize between max_output_tokens and max_tokens for universal compatibility. + // For Responses API targets, max_output_tokens is the canonical field. For others, + // max_tokens is preferred. We handle normalization here to support passthrough + // paths where the translator is skipped. + if (targetFormat === FORMATS.OPENAI_RESPONSES) { + if (body.max_output_tokens === undefined) { + if (body.max_completion_tokens !== undefined) { + body.max_output_tokens = body.max_completion_tokens; + delete body.max_completion_tokens; + } else if (body.max_tokens !== undefined) { + body.max_output_tokens = body.max_tokens; + delete body.max_tokens; + } + } + } else if (body.max_output_tokens !== undefined) { if (body.max_tokens === undefined) { body.max_tokens = body.max_output_tokens; } diff --git a/tests/unit/chatcore-sanitization.test.ts b/tests/unit/chatcore-sanitization.test.ts index 3f56eadda6..b4ebe8653a 100644 --- a/tests/unit/chatcore-sanitization.test.ts +++ b/tests/unit/chatcore-sanitization.test.ts @@ -179,6 +179,90 @@ test("chatCore sanitization normalizes max_output_tokens into max_tokens", async assert.equal("max_tokens" in untouched.call.body, false); }); +test("chatCore sanitization preserves max_output_tokens for openai-responses targets", async () => { + // When the target provider uses openai-responses format (e.g. Codex), + // max_output_tokens is the canonical field and must NOT be normalized to + // max_tokens. Normalizing it breaks Responses→Responses passthrough because + // the translator (which converts max_tokens back) is skipped for same-format. + const { call } = await invokeChatCore({ + endpoint: "/v1/responses", + body: { + model: "gpt-5.4", + max_output_tokens: 4096, + input: [{ role: "user", content: "hello" }], + }, + responseFactory: () => + new Response( + JSON.stringify({ + id: "resp_test", + object: "response", + status: "completed", + model: "gpt-5.4", + output: [ + { type: "message", role: "assistant", content: [{ type: "output_text", text: "ok" }] }, + ], + usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ), + }); + + // max_output_tokens should survive sanitization for Responses targets + assert.equal("max_tokens" in call.body, false, "max_tokens must not be injected for Responses targets"); + + // Reverse normalization: max_tokens → max_output_tokens for Responses targets + const fromMaxTokens = await invokeChatCore({ + endpoint: "/v1/responses", + body: { + model: "gpt-5.4", + max_tokens: 2048, + input: [{ role: "user", content: "hello" }], + }, + responseFactory: () => + new Response( + JSON.stringify({ + id: "resp_test2", + object: "response", + status: "completed", + model: "gpt-5.4", + output: [ + { type: "message", role: "assistant", content: [{ type: "output_text", text: "ok" }] }, + ], + usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ), + }); + + assert.equal("max_tokens" in fromMaxTokens.call.body, false, "max_tokens should be converted to max_output_tokens"); + + // Reverse normalization: max_completion_tokens → max_output_tokens for Responses targets + const fromMaxCompletion = await invokeChatCore({ + endpoint: "/v1/responses", + body: { + model: "gpt-5.4", + max_completion_tokens: 8192, + input: [{ role: "user", content: "hello" }], + }, + responseFactory: () => + new Response( + JSON.stringify({ + id: "resp_test3", + object: "response", + status: "completed", + model: "gpt-5.4", + output: [ + { type: "message", role: "assistant", content: [{ type: "output_text", text: "ok" }] }, + ], + usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, + }), + { status: 200, headers: { "Content-Type": "application/json" } } + ), + }); + + assert.equal("max_completion_tokens" in fromMaxCompletion.call.body, false, "max_completion_tokens should be converted to max_output_tokens"); +}); + test("chatCore sanitization strips empty message names and filters empty tool names", async () => { // Note: `input` field is tested separately because its presence triggers // Responses format detection (PR #1002), which changes message handling.