Skip to content

Commit 3c3f4a5

Browse files
committed
feat(claude-code): add configurable max output tokens setting
- Add maxOutputTokens property to ClaudeCodeSettings interface - Implement UI slider component (range: 8k-64k, default: 8k) - Update provider to pass setting to Claude Code API - Add comprehensive test coverage - Add localization strings for new setting
1 parent 99448fc commit 3c3f4a5

File tree

8 files changed

+200
-369
lines changed

8 files changed

+200
-369
lines changed

packages/types/src/provider-settings.ts

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

8181
const claudeCodeSchema = apiModelIdProviderModelSchema.extend({
8282
claudeCodePath: z.string().optional(),
83+
claudeCodeMaxOutputTokens: z.number().optional(),
8384
})
8485

8586
const glamaSchema = baseProviderSettingsSchema.extend({

src/api/providers/__tests__/claude-code.spec.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,32 @@ describe("ClaudeCodeHandler", () => {
4848
expect(model.id).toBe("claude-sonnet-4-20250514") // default model
4949
})
5050

51+
test("should override maxTokens when claudeCodeMaxOutputTokens is provided", () => {
52+
const options: ApiHandlerOptions = {
53+
claudeCodePath: "claude",
54+
apiModelId: "claude-sonnet-4-20250514",
55+
claudeCodeMaxOutputTokens: 8000,
56+
}
57+
const handlerWithMaxTokens = new ClaudeCodeHandler(options)
58+
const model = handlerWithMaxTokens.getModel()
59+
60+
expect(model.id).toBe("claude-sonnet-4-20250514")
61+
expect(model.info.maxTokens).toBe(8000) // Should use the configured value, not the default 64000
62+
})
63+
64+
test("should override maxTokens for default model when claudeCodeMaxOutputTokens is provided", () => {
65+
const options: ApiHandlerOptions = {
66+
claudeCodePath: "claude",
67+
apiModelId: "invalid-model", // Will fall back to default
68+
claudeCodeMaxOutputTokens: 16384,
69+
}
70+
const handlerWithMaxTokens = new ClaudeCodeHandler(options)
71+
const model = handlerWithMaxTokens.getModel()
72+
73+
expect(model.id).toBe("claude-sonnet-4-20250514") // default model
74+
expect(model.info.maxTokens).toBe(16384) // Should use the configured value
75+
})
76+
5177
test("should filter messages and call runClaudeCode", async () => {
5278
const systemPrompt = "You are a helpful assistant"
5379
const messages = [{ role: "user" as const, content: "Hello" }]
@@ -76,6 +102,43 @@ describe("ClaudeCodeHandler", () => {
76102
messages: filteredMessages,
77103
path: "claude",
78104
modelId: "claude-3-5-sonnet-20241022",
105+
maxOutputTokens: undefined, // No maxOutputTokens configured in this test
106+
})
107+
})
108+
109+
test("should pass maxOutputTokens to runClaudeCode when configured", async () => {
110+
const options: ApiHandlerOptions = {
111+
claudeCodePath: "claude",
112+
apiModelId: "claude-3-5-sonnet-20241022",
113+
claudeCodeMaxOutputTokens: 16384,
114+
}
115+
const handlerWithMaxTokens = new ClaudeCodeHandler(options)
116+
117+
const systemPrompt = "You are a helpful assistant"
118+
const messages = [{ role: "user" as const, content: "Hello" }]
119+
const filteredMessages = [{ role: "user" as const, content: "Hello (filtered)" }]
120+
121+
mockFilterMessages.mockReturnValue(filteredMessages)
122+
123+
// Mock empty async generator
124+
const mockGenerator = async function* (): AsyncGenerator<ClaudeCodeMessage | string> {
125+
// Empty generator for basic test
126+
}
127+
mockRunClaudeCode.mockReturnValue(mockGenerator())
128+
129+
const stream = handlerWithMaxTokens.createMessage(systemPrompt, messages)
130+
131+
// Need to start iterating to trigger the call
132+
const iterator = stream[Symbol.asyncIterator]()
133+
await iterator.next()
134+
135+
// Verify runClaudeCode was called with maxOutputTokens
136+
expect(mockRunClaudeCode).toHaveBeenCalledWith({
137+
systemPrompt,
138+
messages: filteredMessages,
139+
path: "claude",
140+
modelId: "claude-3-5-sonnet-20241022",
141+
maxOutputTokens: 16384,
79142
})
80143
})
81144

src/api/providers/claude-code.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Anthropic } from "@anthropic-ai/sdk"
2-
import { claudeCodeDefaultModelId, type ClaudeCodeModelId, claudeCodeModels } from "@roo-code/types"
2+
import { claudeCodeDefaultModelId, type ClaudeCodeModelId, claudeCodeModels, type ModelInfo } from "@roo-code/types"
33
import { type ApiHandler } from ".."
44
import { ApiStreamUsageChunk, type ApiStream } from "../transform/stream"
55
import { runClaudeCode } from "../../integrations/claude-code/run"
@@ -25,6 +25,7 @@ export class ClaudeCodeHandler extends BaseProvider implements ApiHandler {
2525
messages: filteredMessages,
2626
path: this.options.claudeCodePath,
2727
modelId: this.getModel().id,
28+
maxOutputTokens: this.options.claudeCodeMaxOutputTokens,
2829
})
2930

3031
// Usage is included with assistant messages,
@@ -129,12 +130,26 @@ export class ClaudeCodeHandler extends BaseProvider implements ApiHandler {
129130
const modelId = this.options.apiModelId
130131
if (modelId && modelId in claudeCodeModels) {
131132
const id = modelId as ClaudeCodeModelId
132-
return { id, info: claudeCodeModels[id] }
133+
const modelInfo: ModelInfo = { ...claudeCodeModels[id] }
134+
135+
// Override maxTokens with the configured value if provided
136+
if (this.options.claudeCodeMaxOutputTokens) {
137+
modelInfo.maxTokens = this.options.claudeCodeMaxOutputTokens
138+
}
139+
140+
return { id, info: modelInfo }
141+
}
142+
143+
const defaultModelInfo: ModelInfo = { ...claudeCodeModels[claudeCodeDefaultModelId] }
144+
145+
// Override maxTokens with the configured value if provided
146+
if (this.options.claudeCodeMaxOutputTokens) {
147+
defaultModelInfo.maxTokens = this.options.claudeCodeMaxOutputTokens
133148
}
134149

135150
return {
136151
id: claudeCodeDefaultModelId,
137-
info: claudeCodeModels[claudeCodeDefaultModelId],
152+
info: defaultModelInfo,
138153
}
139154
}
140155

src/integrations/claude-code/run.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ type ProcessState = {
2020
exitCode: number | null
2121
}
2222

23-
export async function* runClaudeCode(options: ClaudeCodeOptions): AsyncGenerator<ClaudeCodeMessage | string> {
23+
export async function* runClaudeCode(
24+
options: ClaudeCodeOptions & { maxOutputTokens?: number },
25+
): AsyncGenerator<ClaudeCodeMessage | string> {
2426
const process = runProcess(options)
2527

2628
const rl = readline.createInterface({
@@ -107,7 +109,13 @@ const claudeCodeTools = [
107109

108110
const CLAUDE_CODE_TIMEOUT = 600000 // 10 minutes
109111

110-
function runProcess({ systemPrompt, messages, path, modelId }: ClaudeCodeOptions) {
112+
function runProcess({
113+
systemPrompt,
114+
messages,
115+
path,
116+
modelId,
117+
maxOutputTokens,
118+
}: ClaudeCodeOptions & { maxOutputTokens?: number }) {
111119
const claudePath = path || "claude"
112120

113121
const args = [
@@ -134,8 +142,9 @@ function runProcess({ systemPrompt, messages, path, modelId }: ClaudeCodeOptions
134142
stderr: "pipe",
135143
env: {
136144
...process.env,
137-
// The default is 32000. However, I've gotten larger responses, so we increase it unless the user specified it.
138-
CLAUDE_CODE_MAX_OUTPUT_TOKENS: process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS || "64000",
145+
// Use the configured value, or the environment variable, or default to 8192
146+
CLAUDE_CODE_MAX_OUTPUT_TOKENS:
147+
maxOutputTokens?.toString() || process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS || "8192",
139148
},
140149
cwd,
141150
maxBuffer: 1024 * 1024 * 1000,

0 commit comments

Comments
 (0)