Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/server/claude-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1125,8 +1125,9 @@ async function handleClaudeMessagesWithBudget(

let clientEncoder: ClientEncoderOption | undefined;
// Native ChatGPT passthrough (openai-responses forward) accepts only Codex-shaped
// bodies: it 400s on sampling params ("Unsupported parameter: max_output_tokens",
// verified live 2026-07-11). Strip them for that route; routed providers keep them.
// bodies: it 400s on sampling params (verified live 2026-07-11). Every openai-responses
// route drops temperature/top_p/stop/user below; max_output_tokens is left to
// stripUnsupportedForwardParams (canonical-forward.ts), which drops it only for that backend.
let settledRoute: RouteResult | undefined;
/**
* Which replayed thinking fields the send that actually happened will serialize.
Expand Down Expand Up @@ -1193,7 +1194,8 @@ async function handleClaudeMessagesWithBudget(
clientEncoder = { protocol: "messages", stream, model: requestedModel, inputTokenFloor: claudeRequestTokenFloor() };
}
if (route.provider.adapter === "openai-responses") {
delete internalBody.max_output_tokens;
// max_output_tokens is the caller's spend cap: the adapter strips it only for the canonical
// ChatGPT backend that rejects it (#5753), so other Responses gateways keep the cap.
delete internalBody.temperature;
delete internalBody.top_p;
delete internalBody.stop;
Expand Down
35 changes: 35 additions & 0 deletions tests/claude-integration/claude-messages-endpoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,41 @@ test("native openai-responses route carries prompt_cache_key + synthesized sessi
}
});

test("non-canonical openai-responses Claude route keeps the caller's max_tokens cap", async () => {
let body: Record<string, unknown> | undefined;
const upstream = Bun.serve({
port: 0,
async fetch(req) {
body = await req.json() as Record<string, unknown>;
return new Response([
`event: response.output_text.delta\ndata: ${JSON.stringify({ delta: "Hello" })}\n\n`,
`event: response.completed\ndata: ${JSON.stringify({ response: { status: "completed", usage: { input_tokens: 10, output_tokens: 2 } } })}\n\n`,
].join(""), { headers: { "Content-Type": "text/event-stream" } });
},
});
saveConfig({
port: 0,
defaultProvider: "gateway",
providers: {
gateway: { adapter: "openai-responses", baseUrl: `${upstream.url.toString().replace(/\/$/, "")}/v1`, apiKey: "k", allowPrivateNetwork: true },
},
} as OcxConfig);
const server = startServer(0);
try {
const response = await fetch(new URL("/v1/messages", server.url), {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ model: "gateway/gpt-test", max_tokens: 128, messages: [{ role: "user", content: "hi" }] }),
});
expect(response.status).toBe(200);
await response.text();
expect(body?.max_output_tokens).toBe(128);
} finally {
await server.stop(true);
upstream.stop(true);
}
});

test("native openai-responses Claude route logs cyber terminals as 400 cyber_policy", async () => {
clearRequestLogsForTests();
const upstream = Bun.serve({
Expand Down
Loading