fix: add reverse normalization max_tokens/max_completion_tokens → max_output_tokens for Responses targets

Per review feedback: when targeting openai-responses, also normalize
max_tokens and max_completion_tokens INTO max_output_tokens, not just
skip the outbound normalization. This covers the case where a Chat
Completions client sends max_tokens to a Responses endpoint via
passthrough.

Extends test to cover both reverse normalization paths.
This commit is contained in:
ivan_yakimkin
2026-04-16 13:34:44 +03:00
parent be6c3ac662
commit c7a11eea4c
2 changed files with 67 additions and 6 deletions

View File

@@ -910,12 +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.
// 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) {
// #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;
}

View File

@@ -209,6 +209,58 @@ test("chatCore sanitization preserves max_output_tokens for openai-responses tar
// 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 () => {