Skip to content

Commit f1b28d4

Browse files
committed
fix: improve Claude Code max output tokens implementation
- Extract hardcoded default value (8000) to CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS constant - Fix truthy check to use explicit !== undefined for handling 0 value - Add validation (min: 1, max: 200000) to claudeCodeMaxOutputTokens in zod schema - Restore missing test coverage for shouldUseReasoningBudget and shouldUseReasoningEffort - Update all references to use the new constant instead of hardcoded values
1 parent 73bb7cb commit f1b28d4

File tree

6 files changed

+218
-10
lines changed

6 files changed

+218
-10
lines changed

packages/types/src/provider-settings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ const anthropicSchema = apiModelIdProviderModelSchema.extend({
8080

8181
const claudeCodeSchema = apiModelIdProviderModelSchema.extend({
8282
claudeCodePath: z.string().optional(),
83-
claudeCodeMaxOutputTokens: z.number().optional(),
83+
claudeCodeMaxOutputTokens: z.number().int().min(1).max(200000).optional(),
8484
})
8585

8686
const glamaSchema = baseProviderSettingsSchema.extend({

packages/types/src/providers/claude-code.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { anthropicModels } from "./anthropic.js"
44
// Claude Code
55
export type ClaudeCodeModelId = keyof typeof claudeCodeModels
66
export const claudeCodeDefaultModelId: ClaudeCodeModelId = "claude-sonnet-4-20250514"
7+
export const CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS = 8000
78
export const claudeCodeModels = {
89
"claude-sonnet-4-20250514": {
910
...anthropicModels["claude-sonnet-4-20250514"],

src/api/providers/claude-code.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ export class ClaudeCodeHandler extends BaseProvider implements ApiHandler {
143143
const defaultModelInfo: ModelInfo = { ...claudeCodeModels[claudeCodeDefaultModelId] }
144144

145145
// Override maxTokens with the configured value if provided
146-
if (this.options.claudeCodeMaxOutputTokens) {
146+
if (this.options.claudeCodeMaxOutputTokens !== undefined) {
147147
defaultModelInfo.maxTokens = this.options.claudeCodeMaxOutputTokens
148148
}
149149

src/integrations/claude-code/run.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type Anthropic from "@anthropic-ai/sdk"
33
import { execa } from "execa"
44
import { ClaudeCodeMessage } from "./types"
55
import readline from "readline"
6+
import { CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS } from "@roo-code/types"
67

78
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
89

@@ -142,9 +143,11 @@ function runProcess({
142143
stderr: "pipe",
143144
env: {
144145
...process.env,
145-
// Use the configured value, or the environment variable, or default to 8000
146+
// Use the configured value, or the environment variable, or default to CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS
146147
CLAUDE_CODE_MAX_OUTPUT_TOKENS:
147-
maxOutputTokens?.toString() || process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS || "8000",
148+
maxOutputTokens?.toString() ||
149+
process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS ||
150+
CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS.toString(),
148151
},
149152
cwd,
150153
maxBuffer: 1024 * 1024 * 1000,

src/shared/__tests__/api.spec.ts

Lines changed: 202 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, test, expect } from "vitest"
2-
import { getModelMaxOutputTokens } from "../api"
2+
import { getModelMaxOutputTokens, shouldUseReasoningBudget, shouldUseReasoningEffort } from "../api"
33
import type { ModelInfo, ProviderSettings } from "@roo-code/types"
4+
import { CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS, ANTHROPIC_DEFAULT_MAX_TOKENS } from "@roo-code/types"
45

56
describe("getModelMaxOutputTokens", () => {
67
const mockModel: ModelInfo = {
@@ -38,7 +39,7 @@ describe("getModelMaxOutputTokens", () => {
3839
expect(result).toBe(8192)
3940
})
4041

41-
test("should return default 8000 when claude-code provider has no custom max tokens", () => {
42+
test("should return default CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS when claude-code provider has no custom max tokens", () => {
4243
const settings: ProviderSettings = {
4344
apiProvider: "claude-code",
4445
// No claudeCodeMaxOutputTokens set
@@ -50,7 +51,7 @@ describe("getModelMaxOutputTokens", () => {
5051
settings,
5152
})
5253

53-
expect(result).toBe(8000)
54+
expect(result).toBe(CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS)
5455
})
5556

5657
test("should handle reasoning budget models correctly", () => {
@@ -89,4 +90,202 @@ describe("getModelMaxOutputTokens", () => {
8990

9091
expect(result).toBe(20000) // 20% of 100000
9192
})
93+
94+
test("should return ANTHROPIC_DEFAULT_MAX_TOKENS for Anthropic models that support reasoning budget but aren't using it", () => {
95+
const anthropicModelId = "claude-sonnet-4-20250514"
96+
const model: ModelInfo = {
97+
contextWindow: 200_000,
98+
supportsPromptCache: true,
99+
supportsReasoningBudget: true,
100+
maxTokens: 64_000, // This should be ignored
101+
}
102+
103+
const settings: ProviderSettings = {
104+
apiProvider: "anthropic",
105+
enableReasoningEffort: false, // Not using reasoning
106+
}
107+
108+
const result = getModelMaxOutputTokens({ modelId: anthropicModelId, model, settings })
109+
expect(result).toBe(ANTHROPIC_DEFAULT_MAX_TOKENS) // Should be 8192, not 64_000
110+
})
111+
112+
test("should return model.maxTokens for non-Anthropic models that support reasoning budget but aren't using it", () => {
113+
const geminiModelId = "gemini-2.5-flash-preview-04-17"
114+
const model: ModelInfo = {
115+
contextWindow: 1_048_576,
116+
supportsPromptCache: false,
117+
supportsReasoningBudget: true,
118+
maxTokens: 65_535,
119+
}
120+
121+
const settings: ProviderSettings = {
122+
apiProvider: "gemini",
123+
enableReasoningEffort: false, // Not using reasoning
124+
}
125+
126+
const result = getModelMaxOutputTokens({ modelId: geminiModelId, model, settings })
127+
expect(result).toBe(65_535) // Should use model.maxTokens, not ANTHROPIC_DEFAULT_MAX_TOKENS
128+
})
129+
130+
test("should return modelMaxTokens from settings when reasoning budget is required", () => {
131+
const model: ModelInfo = {
132+
contextWindow: 200_000,
133+
supportsPromptCache: true,
134+
requiredReasoningBudget: true,
135+
maxTokens: 8000,
136+
}
137+
138+
const settings: ProviderSettings = {
139+
modelMaxTokens: 4000,
140+
}
141+
142+
expect(getModelMaxOutputTokens({ modelId: "test", model, settings })).toBe(4000)
143+
})
144+
145+
test("should return default 16_384 for reasoning budget models when modelMaxTokens not provided", () => {
146+
const model: ModelInfo = {
147+
contextWindow: 200_000,
148+
supportsPromptCache: true,
149+
requiredReasoningBudget: true,
150+
maxTokens: 8000,
151+
}
152+
153+
const settings = {}
154+
155+
expect(getModelMaxOutputTokens({ modelId: "test", model, settings })).toBe(16_384)
156+
})
157+
})
158+
159+
describe("shouldUseReasoningBudget", () => {
160+
test("should return true when model has requiredReasoningBudget", () => {
161+
const model: ModelInfo = {
162+
contextWindow: 200_000,
163+
supportsPromptCache: true,
164+
requiredReasoningBudget: true,
165+
}
166+
167+
// Should return true regardless of settings
168+
expect(shouldUseReasoningBudget({ model })).toBe(true)
169+
expect(shouldUseReasoningBudget({ model, settings: {} })).toBe(true)
170+
expect(shouldUseReasoningBudget({ model, settings: { enableReasoningEffort: false } })).toBe(true)
171+
})
172+
173+
test("should return true when model supports reasoning budget and settings enable reasoning effort", () => {
174+
const model: ModelInfo = {
175+
contextWindow: 200_000,
176+
supportsPromptCache: true,
177+
supportsReasoningBudget: true,
178+
}
179+
180+
const settings: ProviderSettings = {
181+
enableReasoningEffort: true,
182+
}
183+
184+
expect(shouldUseReasoningBudget({ model, settings })).toBe(true)
185+
})
186+
187+
test("should return false when model supports reasoning budget but settings don't enable reasoning effort", () => {
188+
const model: ModelInfo = {
189+
contextWindow: 200_000,
190+
supportsPromptCache: true,
191+
supportsReasoningBudget: true,
192+
}
193+
194+
const settings: ProviderSettings = {
195+
enableReasoningEffort: false,
196+
}
197+
198+
expect(shouldUseReasoningBudget({ model, settings })).toBe(false)
199+
expect(shouldUseReasoningBudget({ model, settings: {} })).toBe(false)
200+
expect(shouldUseReasoningBudget({ model })).toBe(false)
201+
})
202+
203+
test("should return false when model doesn't support reasoning budget", () => {
204+
const model: ModelInfo = {
205+
contextWindow: 200_000,
206+
supportsPromptCache: true,
207+
}
208+
209+
const settings: ProviderSettings = {
210+
enableReasoningEffort: true,
211+
}
212+
213+
expect(shouldUseReasoningBudget({ model, settings })).toBe(false)
214+
expect(shouldUseReasoningBudget({ model })).toBe(false)
215+
})
216+
})
217+
218+
describe("shouldUseReasoningEffort", () => {
219+
test("should return true when model has reasoningEffort property", () => {
220+
const model: ModelInfo = {
221+
contextWindow: 200_000,
222+
supportsPromptCache: true,
223+
reasoningEffort: "medium",
224+
}
225+
226+
// Should return true regardless of settings
227+
expect(shouldUseReasoningEffort({ model })).toBe(true)
228+
expect(shouldUseReasoningEffort({ model, settings: {} })).toBe(true)
229+
expect(shouldUseReasoningEffort({ model, settings: { reasoningEffort: undefined } })).toBe(true)
230+
})
231+
232+
test("should return true when model supports reasoning effort and settings provide reasoning effort", () => {
233+
const model: ModelInfo = {
234+
contextWindow: 200_000,
235+
supportsPromptCache: true,
236+
supportsReasoningEffort: true,
237+
}
238+
239+
const settings: ProviderSettings = {
240+
reasoningEffort: "high",
241+
}
242+
243+
expect(shouldUseReasoningEffort({ model, settings })).toBe(true)
244+
})
245+
246+
test("should return false when model supports reasoning effort but settings don't provide reasoning effort", () => {
247+
const model: ModelInfo = {
248+
contextWindow: 200_000,
249+
supportsPromptCache: true,
250+
supportsReasoningEffort: true,
251+
}
252+
253+
const settings: ProviderSettings = {
254+
reasoningEffort: undefined,
255+
}
256+
257+
expect(shouldUseReasoningEffort({ model, settings })).toBe(false)
258+
expect(shouldUseReasoningEffort({ model, settings: {} })).toBe(false)
259+
expect(shouldUseReasoningEffort({ model })).toBe(false)
260+
})
261+
262+
test("should return false when model doesn't support reasoning effort", () => {
263+
const model: ModelInfo = {
264+
contextWindow: 200_000,
265+
supportsPromptCache: true,
266+
}
267+
268+
const settings: ProviderSettings = {
269+
reasoningEffort: "high",
270+
}
271+
272+
expect(shouldUseReasoningEffort({ model, settings })).toBe(false)
273+
expect(shouldUseReasoningEffort({ model })).toBe(false)
274+
})
275+
276+
test("should handle different reasoning effort values", () => {
277+
const model: ModelInfo = {
278+
contextWindow: 200_000,
279+
supportsPromptCache: true,
280+
supportsReasoningEffort: true,
281+
}
282+
283+
const settingsLow: ProviderSettings = { reasoningEffort: "low" }
284+
const settingsMedium: ProviderSettings = { reasoningEffort: "medium" }
285+
const settingsHigh: ProviderSettings = { reasoningEffort: "high" }
286+
287+
expect(shouldUseReasoningEffort({ model, settings: settingsLow })).toBe(true)
288+
expect(shouldUseReasoningEffort({ model, settings: settingsMedium })).toBe(true)
289+
expect(shouldUseReasoningEffort({ model, settings: settingsHigh })).toBe(true)
290+
})
92291
})

src/shared/api.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { type ModelInfo, type ProviderSettings, ANTHROPIC_DEFAULT_MAX_TOKENS } from "@roo-code/types"
1+
import {
2+
type ModelInfo,
3+
type ProviderSettings,
4+
ANTHROPIC_DEFAULT_MAX_TOKENS,
5+
CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS,
6+
} from "@roo-code/types"
27

38
// ApiHandlerOptions
49

@@ -60,8 +65,8 @@ export const getModelMaxOutputTokens = ({
6065
}): number | undefined => {
6166
// Check for Claude Code specific max output tokens setting
6267
if (settings?.apiProvider === "claude-code") {
63-
// Return the configured value or default to 8000
64-
return settings.claudeCodeMaxOutputTokens || 8000
68+
// Return the configured value or default to CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS
69+
return settings.claudeCodeMaxOutputTokens || CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS
6570
}
6671

6772
if (shouldUseReasoningBudget({ model, settings })) {

0 commit comments

Comments
 (0)