diff --git a/CHANGELOG.md b/CHANGELOG.md index 82ed6a04d1..1f70a9eb8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ _In development β€” bullets added per PR; finalized at release._ +### πŸ”§ Bug Fixes + +- **command-code:** omit `max_tokens` when the client omits it so the upstream applies the model's native default, fixing `400 "expected <=200000"` on `/alpha/generate` for high-cap models; an explicit oversized client value is clamped to the 200k endpoint ceiling (#5221 β€” thanks @adivekar-utexas) + --- ## [3.8.39] β€” 2026-06-28 diff --git a/open-sse/executors/commandCode.ts b/open-sse/executors/commandCode.ts index 76fd7af3b8..a18100780c 100644 --- a/open-sse/executors/commandCode.ts +++ b/open-sse/executors/commandCode.ts @@ -6,6 +6,12 @@ import { BaseExecutor, mergeUpstreamExtraHeaders, type ExecuteInput } from "./ba type JsonRecord = Record; export const COMMAND_CODE_VERSION = process.env.COMMAND_CODE_VERSION?.trim() || "0.33.2"; +// Hard server-side ceiling enforced by Command Code's /alpha/generate endpoint: +// any request with params.max_tokens > 200_000 is rejected with a 400 +// "Too big: expected number to be <=200000 at params.max_tokens". We only use +// this to clamp a CLIENT-SUPPLIED max_tokens down to a value the endpoint will +// accept; we never fabricate this number for requests that omit the field (see +// clampMaxTokens / buildCommandCodeBody). const MAX_COMMAND_CODE_TOKENS = 200_000; const encoder = new TextEncoder(); @@ -140,23 +146,16 @@ function convertMessages(messages: unknown): { system: string; messages: unknown return { system: system.join("\n\n"), messages: out }; } -function clampMaxTokens(value: unknown, cap: number = MAX_COMMAND_CODE_TOKENS): number { - const numeric = numberValue(value) ?? cap; - return Math.max(1, Math.min(Math.floor(numeric), cap)); -} - -// Resolve the per-model max_tokens cap for a given CommandCode model id. -// Falls back to MAX_COMMAND_CODE_TOKENS when the model isn't registered or -// when the registry entry omits maxOutputTokens. Without this, GLM-5.x -// requests get capped at 200_000 and rejected with "ι™εˆΆζ•°ε€ΌθŒƒε›΄[1,131072]". -function getModelMaxTokensCap(modelId: string): number { - const entry = REGISTRY["command-code"]; - if (!entry) return MAX_COMMAND_CODE_TOKENS; - const model = entry.models?.find((m: { id: string }) => m.id === modelId); - const registryCap = (model as { maxOutputTokens?: number } | undefined)?.maxOutputTokens; - return typeof registryCap === "number" && registryCap > 0 - ? registryCap - : MAX_COMMAND_CODE_TOKENS; +// Clamp a client-supplied max_tokens to the endpoint ceiling, mirroring the +// provider-driven clamp in antigravity.ts: we only intervene when the value is +// present AND would otherwise be rejected (> 200_000). A valid value is +// returned floored; anything absent or non-numeric returns undefined so the +// caller can OMIT the field entirely and let Command Code's upstream apply the +// model's own native default (rather than us inventing a number). +function clampMaxTokens(value: unknown): number | undefined { + const numeric = numberValue(value); + if (numeric === undefined) return undefined; + return Math.max(1, Math.min(Math.floor(numeric), MAX_COMMAND_CODE_TOKENS)); } // Reasoning/thinking fields that payload rules or clients may inject and that @@ -188,13 +187,20 @@ function buildCommandCodeBody(model: string, body: unknown, stream = false): Jso messages: converted.messages, tools: convertTools(input.tools), system, - max_tokens: clampMaxTokens( - input.max_tokens ?? input.max_completion_tokens, - getModelMaxTokensCap(resolvedModel) - ), stream: true, }; + // Only forward max_tokens when the client actually supplied one. Omitting it + // lets Command Code's upstream apply the model's own native default, so we + // never invent a value (the old behavior, which sent the wrong number and got + // DeepSeek V4 rejected with "Too big: expected number to be <=200000"). When + // present, it is clamped to the endpoint ceiling so an oversized client value + // degrades gracefully instead of 400ing. + const maxTokens = clampMaxTokens(input.max_tokens ?? input.max_completion_tokens); + if (maxTokens !== undefined) { + params.max_tokens = maxTokens; + } + for (const field of COMMAND_CODE_PASSTHROUGH_FIELDS) { const value = input[field]; if (value !== undefined && value !== null) { diff --git a/tests/unit/command-code-executor.test.ts b/tests/unit/command-code-executor.test.ts index a52f59b62b..35ad263257 100644 --- a/tests/unit/command-code-executor.test.ts +++ b/tests/unit/command-code-executor.test.ts @@ -294,40 +294,60 @@ test("Command Code executor surfaces upstream and streamed errors", async () => }, /boom/); }); -test("Command Code executor caps max_tokens to the registered per-model limit (GLM-5.x)", async () => { +test("Command Code executor omits max_tokens when the client does not supply one (GLM-5.x)", async () => { const calls: FetchCall[] = []; globalThis.fetch = async (url, init = {}) => { calls.push({ url: String(url), init, body: JSON.parse(String(init.body)) }); return commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]); }; - // GLM-5 and GLM-5.1 are registered with maxOutputTokens: 131072. - // Without per-model capping, the upstream rejects with - // "ι™εˆΆζ•°ε€ΌθŒƒε›΄[1,131072]". + // No client max_tokens: we must NOT fabricate one. Omitting the field lets + // Command Code's upstream apply the model's own native default. await getExecutor("command-code").execute({ model: "zai-org/GLM-5.1", stream: false, credentials: { apiKey: "cc_test_key" }, body: { messages: [{ role: "user", content: "Hi" }] }, }); - assert.equal(calls[0].body.params.max_tokens, 131072); + assert.ok(!("max_tokens" in calls[0].body.params)); }); -test("Command Code executor caps max_tokens to the registered per-model limit (DeepSeek v4)", async () => { +test("Command Code executor omits max_tokens for DeepSeek v4 when the client does not supply one", async () => { const calls: FetchCall[] = []; globalThis.fetch = async (url, init = {}) => { calls.push({ url: String(url), init, body: JSON.parse(String(init.body)) }); return commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]); }; - // DeepSeek v4 pro is registered with maxOutputTokens: 384000. + // Regression: previously the executor invented max_tokens from the registry + // (384000), which /alpha/generate rejects with a 400 + // "Too big: expected number to be <=200000". With no client value we now omit + // the field entirely, so the request succeeds and upstream picks the default. await getExecutor("command-code").execute({ model: "deepseek/deepseek-v4-pro", stream: false, credentials: { apiKey: "cc_test_key" }, body: { messages: [{ role: "user", content: "Hi" }] }, }); - assert.equal(calls[0].body.params.max_tokens, 384000); + assert.ok(!("max_tokens" in calls[0].body.params)); +}); + +test("Command Code executor clamps an oversized client-supplied max_tokens to the endpoint ceiling", async () => { + const calls: FetchCall[] = []; + globalThis.fetch = async (url, init = {}) => { + calls.push({ url: String(url), init, body: JSON.parse(String(init.body)) }); + return commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]); + }; + + // A client asking for more than the 200000 endpoint ceiling is clamped down + // (not 400'd), mirroring the provider-driven clamp in antigravity.ts. + await getExecutor("command-code").execute({ + model: "deepseek/deepseek-v4-pro", + stream: false, + credentials: { apiKey: "cc_test_key" }, + body: { messages: [{ role: "user", content: "Hi" }], max_tokens: 500000 }, + }); + assert.equal(calls[0].body.params.max_tokens, 200000); }); test("Command Code executor honors a smaller client-provided max_tokens under the per-model cap", async () => {