diff --git a/src/transforms.test.ts b/src/transforms.test.ts index 26fcbed..dcee4a6 100644 --- a/src/transforms.test.ts +++ b/src/transforms.test.ts @@ -329,6 +329,73 @@ describe("transforms", () => { ) }) + it("transformBody caps budget_tokens when equal to max_tokens", () => { + const input = JSON.stringify({ + model: "claude-opus-4-6", + max_tokens: 128000, + thinking: { type: "enabled", budget_tokens: 128000 }, + messages: [{ role: "user", content: "test" }], + }) + + const output = transformBody(input) + const parsed = JSON.parse(output as string) as { + max_tokens: number + thinking: { type: string; budget_tokens: number } + } + + assert.equal(parsed.max_tokens, 128000, "max_tokens should be unchanged") + assert.equal( + parsed.thinking.budget_tokens, + 102400, + "budget_tokens should be capped to 80% of max_tokens", + ) + assert.ok( + parsed.max_tokens > parsed.thinking.budget_tokens, + "max_tokens must be greater than budget_tokens", + ) + }) + + it("transformBody caps budget_tokens when greater than max_tokens", () => { + const input = JSON.stringify({ + model: "claude-opus-4-6", + max_tokens: 32000, + thinking: { type: "enabled", budget_tokens: 50000 }, + messages: [{ role: "user", content: "test" }], + }) + + const output = transformBody(input) + const parsed = JSON.parse(output as string) as { + max_tokens: number + thinking: { type: string; budget_tokens: number } + } + + assert.equal(parsed.max_tokens, 32000) + assert.equal(parsed.thinking.budget_tokens, 25600) + assert.ok(parsed.max_tokens > parsed.thinking.budget_tokens) + }) + + it("transformBody does not modify budget_tokens when already less than max_tokens", () => { + const input = JSON.stringify({ + model: "claude-opus-4-6", + max_tokens: 128000, + thinking: { type: "enabled", budget_tokens: 100000 }, + messages: [{ role: "user", content: "test" }], + }) + + const output = transformBody(input) + const parsed = JSON.parse(output as string) as { + max_tokens: number + thinking: { type: string; budget_tokens: number } + } + + assert.equal(parsed.max_tokens, 128000) + assert.equal( + parsed.thinking.budget_tokens, + 100000, + "budget_tokens should be unchanged when already valid", + ) + }) + it("transformBody preserves effort for non-haiku models", () => { const input = JSON.stringify({ model: "claude-opus-4-6", diff --git a/src/transforms.ts b/src/transforms.ts index 49350d0..3d1716b 100644 --- a/src/transforms.ts +++ b/src/transforms.ts @@ -80,6 +80,8 @@ export function transformBody( try { const parsed = JSON.parse(body) as { model?: string + // eslint-disable-next-line @typescript-eslint/naming-convention + max_tokens?: number system?: SystemEntry[] thinking?: Record // eslint-disable-next-line @typescript-eslint/naming-convention @@ -173,6 +175,18 @@ export function transformBody( } } + // Ensure max_tokens > thinking.budget_tokens (API requirement). + // OpenCode may set budget_tokens >= max_tokens for custom models that + // aren't in its built-in registry, causing a 400 error from the API. + if ( + parsed.thinking && + typeof parsed.thinking.budget_tokens === "number" && + typeof parsed.max_tokens === "number" && + parsed.max_tokens <= parsed.thinking.budget_tokens + ) { + parsed.thinking.budget_tokens = Math.floor(parsed.max_tokens * 0.8) + } + if (Array.isArray(parsed.tools)) { parsed.tools = parsed.tools.map((tool) => ({ ...tool,