fix: preserve max_output_tokens for Responses API targets in chatCore sanitization

The common input sanitization in chatCore unconditionally normalizes
max_output_tokens to max_tokens (#994). This works for Chat Completions
targets but breaks Responses API passthrough (source and target both
openai-responses), because:

1. chatCore replaces max_output_tokens with max_tokens
2. Same-format requests skip the translator entirely
3. max_tokens reaches the upstream, which rejects it with
   'Unsupported parameter: max_tokens'

The fix makes the normalization conditional: skip it when the target
format is openai-responses, where max_output_tokens is the canonical
field. The existing openaiToOpenAIResponsesRequest translator (#1245)
still handles the openai → openai-responses path correctly.

Adds a test that verifies max_output_tokens is not normalized to
max_tokens when routing to a Responses API target.
This commit is contained in:
ivan_yakimkin
2026-04-16 13:26:49 +03:00
parent 9e45baae58
commit be6c3ac662
2 changed files with 38 additions and 2 deletions

View File

@@ -910,8 +910,12 @@ 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 max_output_tokens to max_tokens for universal compatibility.
// Skip for Responses API targets where max_output_tokens is the canonical field;
// normalizing it to max_tokens there causes "Unsupported parameter: max_tokens"
// errors because Responses→Responses passthrough skips the translator that would
// convert it back (see openaiToOpenAIResponsesRequest).
if (body.max_output_tokens !== undefined && targetFormat !== FORMATS.OPENAI_RESPONSES) {
if (body.max_tokens === undefined) {
body.max_tokens = body.max_output_tokens;
}

View File

@@ -179,6 +179,38 @@ 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");
});
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.