Просмотр исходного кода

feat(claude-code): add configurable max output tokens setting (#5610)

Co-authored-by: Copilot <[email protected]>
Co-authored-by: Daniel <[email protected]>
Co-authored-by: Daniel Riccio <[email protected]>
Hannes Rudolph 5 месяцев назад
Родитель
Сommit
f5dfbf1fc2

+ 1 - 0
packages/types/src/provider-settings.ts

@@ -80,6 +80,7 @@ const anthropicSchema = apiModelIdProviderModelSchema.extend({
 
 const claudeCodeSchema = apiModelIdProviderModelSchema.extend({
 	claudeCodePath: z.string().optional(),
+	claudeCodeMaxOutputTokens: z.number().int().min(1).max(200000).optional(),
 })
 
 const glamaSchema = baseProviderSettingsSchema.extend({

+ 1 - 0
packages/types/src/providers/claude-code.ts

@@ -4,6 +4,7 @@ import { anthropicModels } from "./anthropic.js"
 // Claude Code
 export type ClaudeCodeModelId = keyof typeof claudeCodeModels
 export const claudeCodeDefaultModelId: ClaudeCodeModelId = "claude-sonnet-4-20250514"
+export const CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS = 8000
 export const claudeCodeModels = {
 	"claude-sonnet-4-20250514": {
 		...anthropicModels["claude-sonnet-4-20250514"],

+ 63 - 0
src/api/providers/__tests__/claude-code.spec.ts

@@ -48,6 +48,32 @@ describe("ClaudeCodeHandler", () => {
 		expect(model.id).toBe("claude-sonnet-4-20250514") // default model
 	})
 
+	test("should override maxTokens when claudeCodeMaxOutputTokens is provided", () => {
+		const options: ApiHandlerOptions = {
+			claudeCodePath: "claude",
+			apiModelId: "claude-sonnet-4-20250514",
+			claudeCodeMaxOutputTokens: 8000,
+		}
+		const handlerWithMaxTokens = new ClaudeCodeHandler(options)
+		const model = handlerWithMaxTokens.getModel()
+
+		expect(model.id).toBe("claude-sonnet-4-20250514")
+		expect(model.info.maxTokens).toBe(8000) // Should use the configured value, not the default 64000
+	})
+
+	test("should override maxTokens for default model when claudeCodeMaxOutputTokens is provided", () => {
+		const options: ApiHandlerOptions = {
+			claudeCodePath: "claude",
+			apiModelId: "invalid-model", // Will fall back to default
+			claudeCodeMaxOutputTokens: 16384,
+		}
+		const handlerWithMaxTokens = new ClaudeCodeHandler(options)
+		const model = handlerWithMaxTokens.getModel()
+
+		expect(model.id).toBe("claude-sonnet-4-20250514") // default model
+		expect(model.info.maxTokens).toBe(16384) // Should use the configured value
+	})
+
 	test("should filter messages and call runClaudeCode", async () => {
 		const systemPrompt = "You are a helpful assistant"
 		const messages = [{ role: "user" as const, content: "Hello" }]
@@ -76,6 +102,43 @@ describe("ClaudeCodeHandler", () => {
 			messages: filteredMessages,
 			path: "claude",
 			modelId: "claude-3-5-sonnet-20241022",
+			maxOutputTokens: undefined, // No maxOutputTokens configured in this test
+		})
+	})
+
+	test("should pass maxOutputTokens to runClaudeCode when configured", async () => {
+		const options: ApiHandlerOptions = {
+			claudeCodePath: "claude",
+			apiModelId: "claude-3-5-sonnet-20241022",
+			claudeCodeMaxOutputTokens: 16384,
+		}
+		const handlerWithMaxTokens = new ClaudeCodeHandler(options)
+
+		const systemPrompt = "You are a helpful assistant"
+		const messages = [{ role: "user" as const, content: "Hello" }]
+		const filteredMessages = [{ role: "user" as const, content: "Hello (filtered)" }]
+
+		mockFilterMessages.mockReturnValue(filteredMessages)
+
+		// Mock empty async generator
+		const mockGenerator = async function* (): AsyncGenerator<ClaudeCodeMessage | string> {
+			// Empty generator for basic test
+		}
+		mockRunClaudeCode.mockReturnValue(mockGenerator())
+
+		const stream = handlerWithMaxTokens.createMessage(systemPrompt, messages)
+
+		// Need to start iterating to trigger the call
+		const iterator = stream[Symbol.asyncIterator]()
+		await iterator.next()
+
+		// Verify runClaudeCode was called with maxOutputTokens
+		expect(mockRunClaudeCode).toHaveBeenCalledWith({
+			systemPrompt,
+			messages: filteredMessages,
+			path: "claude",
+			modelId: "claude-3-5-sonnet-20241022",
+			maxOutputTokens: 16384,
 		})
 	})
 

+ 18 - 3
src/api/providers/claude-code.ts

@@ -1,5 +1,5 @@
 import type { Anthropic } from "@anthropic-ai/sdk"
-import { claudeCodeDefaultModelId, type ClaudeCodeModelId, claudeCodeModels } from "@roo-code/types"
+import { claudeCodeDefaultModelId, type ClaudeCodeModelId, claudeCodeModels, type ModelInfo } from "@roo-code/types"
 import { type ApiHandler } from ".."
 import { ApiStreamUsageChunk, type ApiStream } from "../transform/stream"
 import { runClaudeCode } from "../../integrations/claude-code/run"
@@ -25,6 +25,7 @@ export class ClaudeCodeHandler extends BaseProvider implements ApiHandler {
 			messages: filteredMessages,
 			path: this.options.claudeCodePath,
 			modelId: this.getModel().id,
+			maxOutputTokens: this.options.claudeCodeMaxOutputTokens,
 		})
 
 		// Usage is included with assistant messages,
@@ -129,12 +130,26 @@ export class ClaudeCodeHandler extends BaseProvider implements ApiHandler {
 		const modelId = this.options.apiModelId
 		if (modelId && modelId in claudeCodeModels) {
 			const id = modelId as ClaudeCodeModelId
-			return { id, info: claudeCodeModels[id] }
+			const modelInfo: ModelInfo = { ...claudeCodeModels[id] }
+
+			// Override maxTokens with the configured value if provided
+			if (this.options.claudeCodeMaxOutputTokens !== undefined) {
+				modelInfo.maxTokens = this.options.claudeCodeMaxOutputTokens
+			}
+
+			return { id, info: modelInfo }
+		}
+
+		const defaultModelInfo: ModelInfo = { ...claudeCodeModels[claudeCodeDefaultModelId] }
+
+		// Override maxTokens with the configured value if provided
+		if (this.options.claudeCodeMaxOutputTokens !== undefined) {
+			defaultModelInfo.maxTokens = this.options.claudeCodeMaxOutputTokens
 		}
 
 		return {
 			id: claudeCodeDefaultModelId,
-			info: claudeCodeModels[claudeCodeDefaultModelId],
+			info: defaultModelInfo,
 		}
 	}
 

+ 16 - 4
src/integrations/claude-code/run.ts

@@ -3,6 +3,7 @@ import type Anthropic from "@anthropic-ai/sdk"
 import { execa } from "execa"
 import { ClaudeCodeMessage } from "./types"
 import readline from "readline"
+import { CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS } from "@roo-code/types"
 
 const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
 
@@ -20,7 +21,9 @@ type ProcessState = {
 	exitCode: number | null
 }
 
-export async function* runClaudeCode(options: ClaudeCodeOptions): AsyncGenerator<ClaudeCodeMessage | string> {
+export async function* runClaudeCode(
+	options: ClaudeCodeOptions & { maxOutputTokens?: number },
+): AsyncGenerator<ClaudeCodeMessage | string> {
 	const process = runProcess(options)
 
 	const rl = readline.createInterface({
@@ -107,7 +110,13 @@ const claudeCodeTools = [
 
 const CLAUDE_CODE_TIMEOUT = 600000 // 10 minutes
 
-function runProcess({ systemPrompt, messages, path, modelId }: ClaudeCodeOptions) {
+function runProcess({
+	systemPrompt,
+	messages,
+	path,
+	modelId,
+	maxOutputTokens,
+}: ClaudeCodeOptions & { maxOutputTokens?: number }) {
 	const claudePath = path || "claude"
 
 	const args = [
@@ -134,8 +143,11 @@ function runProcess({ systemPrompt, messages, path, modelId }: ClaudeCodeOptions
 		stderr: "pipe",
 		env: {
 			...process.env,
-			// The default is 32000. However, I've gotten larger responses, so we increase it unless the user specified it.
-			CLAUDE_CODE_MAX_OUTPUT_TOKENS: process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS || "64000",
+			// Use the configured value, or the environment variable, or default to CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS
+			CLAUDE_CODE_MAX_OUTPUT_TOKENS:
+				maxOutputTokens?.toString() ||
+				process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS ||
+				CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS.toString(),
 		},
 		cwd,
 		maxBuffer: 1024 * 1024 * 1000,

+ 97 - 185
src/shared/__tests__/api.spec.ts

@@ -1,164 +1,97 @@
-// npx vitest run src/shared/__tests__/api.spec.ts
-
-import { type ModelInfo, type ProviderSettings, ANTHROPIC_DEFAULT_MAX_TOKENS } from "@roo-code/types"
-
+import { describe, test, expect } from "vitest"
 import { getModelMaxOutputTokens, shouldUseReasoningBudget, shouldUseReasoningEffort } from "../api"
+import type { ModelInfo, ProviderSettings } from "@roo-code/types"
+import { CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS, ANTHROPIC_DEFAULT_MAX_TOKENS } from "@roo-code/types"
 
-describe("getMaxTokensForModel", () => {
-	const modelId = "test"
-
-	/**
-	 * Testing the specific fix in commit cc79178f:
-	 * For thinking models, use apiConfig.modelMaxTokens if available,
-	 * otherwise fall back to 8192 (not modelInfo.maxTokens)
-	 */
-
-	it("should return apiConfig.modelMaxTokens for thinking models when provided", () => {
-		const model: ModelInfo = {
-			contextWindow: 200_000,
-			supportsPromptCache: true,
-			requiredReasoningBudget: true,
-			maxTokens: 8000,
-		}
+describe("getModelMaxOutputTokens", () => {
+	const mockModel: ModelInfo = {
+		maxTokens: 8192,
+		contextWindow: 200000,
+		supportsPromptCache: true,
+	}
 
+	test("should return claudeCodeMaxOutputTokens when using claude-code provider", () => {
 		const settings: ProviderSettings = {
-			modelMaxTokens: 4000,
-		}
-
-		expect(getModelMaxOutputTokens({ modelId, model, settings })).toBe(4000)
-	})
-
-	it("should return 16_384 for thinking models when modelMaxTokens not provided", () => {
-		const model: ModelInfo = {
-			contextWindow: 200_000,
-			supportsPromptCache: true,
-			requiredReasoningBudget: true,
-			maxTokens: 8000,
+			apiProvider: "claude-code",
+			claudeCodeMaxOutputTokens: 16384,
 		}
 
-		const settings = {}
-
-		expect(getModelMaxOutputTokens({ modelId, model, settings })).toBe(16_384)
-	})
-
-	it("should return 16_384 for thinking models when apiConfig is undefined", () => {
-		const model: ModelInfo = {
-			contextWindow: 200_000,
-			supportsPromptCache: true,
-			requiredReasoningBudget: true,
-			maxTokens: 8000,
-		}
+		const result = getModelMaxOutputTokens({
+			modelId: "claude-3-5-sonnet-20241022",
+			model: mockModel,
+			settings,
+		})
 
-		expect(getModelMaxOutputTokens({ modelId, model, settings: undefined })).toBe(16_384)
+		expect(result).toBe(16384)
 	})
 
-	it("should return modelInfo.maxTokens for non-thinking models", () => {
-		const model: ModelInfo = {
-			contextWindow: 200_000,
-			supportsPromptCache: true,
-			maxTokens: 8000,
-		}
-
+	test("should return model maxTokens when not using claude-code provider", () => {
 		const settings: ProviderSettings = {
-			modelMaxTokens: 4000,
+			apiProvider: "anthropic",
 		}
 
-		expect(getModelMaxOutputTokens({ modelId, model, settings })).toBe(8000)
-	})
-
-	it("should return 20% of context window for non-thinking models with undefined maxTokens", () => {
-		const model: ModelInfo = {
-			contextWindow: 200_000,
-			supportsPromptCache: true,
-		}
-
-		const settings: ProviderSettings = {
-			modelMaxTokens: 4000,
-		}
+		const result = getModelMaxOutputTokens({
+			modelId: "claude-3-5-sonnet-20241022",
+			model: mockModel,
+			settings,
+		})
 
-		// Should return 20% of context window when maxTokens is undefined
-		expect(getModelMaxOutputTokens({ modelId, model, settings })).toBe(40000)
+		expect(result).toBe(8192)
 	})
 
-	test("should return maxTokens from modelInfo when thinking is false", () => {
-		const model: ModelInfo = {
-			contextWindow: 200_000,
-			supportsPromptCache: true,
-			maxTokens: 2048,
-		}
-
+	test("should return default CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS when claude-code provider has no custom max tokens", () => {
 		const settings: ProviderSettings = {
-			modelMaxTokens: 4096,
+			apiProvider: "claude-code",
+			// No claudeCodeMaxOutputTokens set
 		}
 
-		const result = getModelMaxOutputTokens({ modelId, model, settings })
-		expect(result).toBe(2048)
+		const result = getModelMaxOutputTokens({
+			modelId: "claude-3-5-sonnet-20241022",
+			model: mockModel,
+			settings,
+		})
+
+		expect(result).toBe(CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS)
 	})
 
-	test("should return modelMaxTokens from apiConfig when thinking is true", () => {
-		const model: ModelInfo = {
-			contextWindow: 200_000,
-			supportsPromptCache: true,
-			maxTokens: 2048,
+	test("should handle reasoning budget models correctly", () => {
+		const reasoningModel: ModelInfo = {
+			...mockModel,
+			supportsReasoningBudget: true,
 			requiredReasoningBudget: true,
 		}
 
 		const settings: ProviderSettings = {
-			modelMaxTokens: 4096,
-		}
-
-		const result = getModelMaxOutputTokens({ modelId, model, settings })
-		expect(result).toBe(4096)
-	})
-
-	test("should fallback to DEFAULT_THINKING_MODEL_MAX_TOKENS when thinking is true but apiConfig.modelMaxTokens is not defined", () => {
-		const model: ModelInfo = {
-			contextWindow: 200_000,
-			supportsPromptCache: true,
-			maxTokens: 2048,
-			requiredReasoningBudget: true,
+			apiProvider: "anthropic",
+			enableReasoningEffort: true,
+			modelMaxTokens: 32000,
 		}
 
-		const settings: ProviderSettings = {}
-
-		const result = getModelMaxOutputTokens({ modelId, model, settings: undefined })
-		expect(result).toBe(16_384)
-	})
-
-	test("should handle undefined inputs gracefully", () => {
-		const modelInfoOnly: ModelInfo = {
-			contextWindow: 200_000,
-			supportsPromptCache: true,
-			maxTokens: 2048,
-		}
+		const result = getModelMaxOutputTokens({
+			modelId: "claude-3-7-sonnet-20250219",
+			model: reasoningModel,
+			settings,
+		})
 
-		expect(getModelMaxOutputTokens({ modelId, model: modelInfoOnly, settings: undefined })).toBe(2048)
+		expect(result).toBe(32000)
 	})
 
-	test("should handle missing properties gracefully", () => {
-		const modelInfoWithoutMaxTokens: ModelInfo = {
-			contextWindow: 200_000,
+	test("should return 20% of context window when maxTokens is undefined", () => {
+		const modelWithoutMaxTokens: ModelInfo = {
+			contextWindow: 100000,
 			supportsPromptCache: true,
-			requiredReasoningBudget: true,
 		}
 
-		const settings: ProviderSettings = {
-			modelMaxTokens: 4096,
-		}
-
-		expect(getModelMaxOutputTokens({ modelId, model: modelInfoWithoutMaxTokens, settings })).toBe(4096)
-
-		const modelInfoWithoutThinking: ModelInfo = {
-			contextWindow: 200_000,
-			supportsPromptCache: true,
-			maxTokens: 2048,
-		}
+		const result = getModelMaxOutputTokens({
+			modelId: "some-model",
+			model: modelWithoutMaxTokens,
+			settings: {},
+		})
 
-		expect(getModelMaxOutputTokens({ modelId, model: modelInfoWithoutThinking, settings: undefined })).toBe(2048)
+		expect(result).toBe(20000) // 20% of 100000
 	})
 
 	test("should return ANTHROPIC_DEFAULT_MAX_TOKENS for Anthropic models that support reasoning budget but aren't using it", () => {
-		// Test case for models that support reasoning budget but enableReasoningEffort is false
 		const anthropicModelId = "claude-sonnet-4-20250514"
 		const model: ModelInfo = {
 			contextWindow: 200_000,
@@ -168,6 +101,7 @@ describe("getMaxTokensForModel", () => {
 		}
 
 		const settings: ProviderSettings = {
+			apiProvider: "anthropic",
 			enableReasoningEffort: false, // Not using reasoning
 		}
 
@@ -176,7 +110,6 @@ describe("getMaxTokensForModel", () => {
 	})
 
 	test("should return model.maxTokens for non-Anthropic models that support reasoning budget but aren't using it", () => {
-		// Test case for non-Anthropic models - should still use model.maxTokens
 		const geminiModelId = "gemini-2.5-flash-preview-04-17"
 		const model: ModelInfo = {
 			contextWindow: 1_048_576,
@@ -186,16 +119,45 @@ describe("getMaxTokensForModel", () => {
 		}
 
 		const settings: ProviderSettings = {
+			apiProvider: "gemini",
 			enableReasoningEffort: false, // Not using reasoning
 		}
 
 		const result = getModelMaxOutputTokens({ modelId: geminiModelId, model, settings })
 		expect(result).toBe(65_535) // Should use model.maxTokens, not ANTHROPIC_DEFAULT_MAX_TOKENS
 	})
+
+	test("should return modelMaxTokens from settings when reasoning budget is required", () => {
+		const model: ModelInfo = {
+			contextWindow: 200_000,
+			supportsPromptCache: true,
+			requiredReasoningBudget: true,
+			maxTokens: 8000,
+		}
+
+		const settings: ProviderSettings = {
+			modelMaxTokens: 4000,
+		}
+
+		expect(getModelMaxOutputTokens({ modelId: "test", model, settings })).toBe(4000)
+	})
+
+	test("should return default 16_384 for reasoning budget models when modelMaxTokens not provided", () => {
+		const model: ModelInfo = {
+			contextWindow: 200_000,
+			supportsPromptCache: true,
+			requiredReasoningBudget: true,
+			maxTokens: 8000,
+		}
+
+		const settings = {}
+
+		expect(getModelMaxOutputTokens({ modelId: "test", model, settings })).toBe(16_384)
+	})
 })
 
 describe("shouldUseReasoningBudget", () => {
-	it("should return true when model has requiredReasoningBudget", () => {
+	test("should return true when model has requiredReasoningBudget", () => {
 		const model: ModelInfo = {
 			contextWindow: 200_000,
 			supportsPromptCache: true,
@@ -208,7 +170,7 @@ describe("shouldUseReasoningBudget", () => {
 		expect(shouldUseReasoningBudget({ model, settings: { enableReasoningEffort: false } })).toBe(true)
 	})
 
-	it("should return true when model supports reasoning budget and settings enable reasoning effort", () => {
+	test("should return true when model supports reasoning budget and settings enable reasoning effort", () => {
 		const model: ModelInfo = {
 			contextWindow: 200_000,
 			supportsPromptCache: true,
@@ -222,7 +184,7 @@ describe("shouldUseReasoningBudget", () => {
 		expect(shouldUseReasoningBudget({ model, settings })).toBe(true)
 	})
 
-	it("should return false when model supports reasoning budget but settings don't enable reasoning effort", () => {
+	test("should return false when model supports reasoning budget but settings don't enable reasoning effort", () => {
 		const model: ModelInfo = {
 			contextWindow: 200_000,
 			supportsPromptCache: true,
@@ -238,7 +200,7 @@ describe("shouldUseReasoningBudget", () => {
 		expect(shouldUseReasoningBudget({ model })).toBe(false)
 	})
 
-	it("should return false when model doesn't support reasoning budget", () => {
+	test("should return false when model doesn't support reasoning budget", () => {
 		const model: ModelInfo = {
 			contextWindow: 200_000,
 			supportsPromptCache: true,
@@ -251,27 +213,10 @@ describe("shouldUseReasoningBudget", () => {
 		expect(shouldUseReasoningBudget({ model, settings })).toBe(false)
 		expect(shouldUseReasoningBudget({ model })).toBe(false)
 	})
-
-	it("should handle undefined settings gracefully", () => {
-		const modelWithRequired: ModelInfo = {
-			contextWindow: 200_000,
-			supportsPromptCache: true,
-			requiredReasoningBudget: true,
-		}
-
-		const modelWithSupported: ModelInfo = {
-			contextWindow: 200_000,
-			supportsPromptCache: true,
-			supportsReasoningBudget: true,
-		}
-
-		expect(shouldUseReasoningBudget({ model: modelWithRequired, settings: undefined })).toBe(true)
-		expect(shouldUseReasoningBudget({ model: modelWithSupported, settings: undefined })).toBe(false)
-	})
 })
 
 describe("shouldUseReasoningEffort", () => {
-	it("should return true when model has reasoningEffort property", () => {
+	test("should return true when model has reasoningEffort property", () => {
 		const model: ModelInfo = {
 			contextWindow: 200_000,
 			supportsPromptCache: true,
@@ -284,7 +229,7 @@ describe("shouldUseReasoningEffort", () => {
 		expect(shouldUseReasoningEffort({ model, settings: { reasoningEffort: undefined } })).toBe(true)
 	})
 
-	it("should return true when model supports reasoning effort and settings provide reasoning effort", () => {
+	test("should return true when model supports reasoning effort and settings provide reasoning effort", () => {
 		const model: ModelInfo = {
 			contextWindow: 200_000,
 			supportsPromptCache: true,
@@ -298,7 +243,7 @@ describe("shouldUseReasoningEffort", () => {
 		expect(shouldUseReasoningEffort({ model, settings })).toBe(true)
 	})
 
-	it("should return false when model supports reasoning effort but settings don't provide reasoning effort", () => {
+	test("should return false when model supports reasoning effort but settings don't provide reasoning effort", () => {
 		const model: ModelInfo = {
 			contextWindow: 200_000,
 			supportsPromptCache: true,
@@ -314,7 +259,7 @@ describe("shouldUseReasoningEffort", () => {
 		expect(shouldUseReasoningEffort({ model })).toBe(false)
 	})
 
-	it("should return false when model doesn't support reasoning effort", () => {
+	test("should return false when model doesn't support reasoning effort", () => {
 		const model: ModelInfo = {
 			contextWindow: 200_000,
 			supportsPromptCache: true,
@@ -328,7 +273,7 @@ describe("shouldUseReasoningEffort", () => {
 		expect(shouldUseReasoningEffort({ model })).toBe(false)
 	})
 
-	it("should handle different reasoning effort values", () => {
+	test("should handle different reasoning effort values", () => {
 		const model: ModelInfo = {
 			contextWindow: 200_000,
 			supportsPromptCache: true,
@@ -343,37 +288,4 @@ describe("shouldUseReasoningEffort", () => {
 		expect(shouldUseReasoningEffort({ model, settings: settingsMedium })).toBe(true)
 		expect(shouldUseReasoningEffort({ model, settings: settingsHigh })).toBe(true)
 	})
-
-	it("should handle undefined settings gracefully", () => {
-		const modelWithReasoning: ModelInfo = {
-			contextWindow: 200_000,
-			supportsPromptCache: true,
-			reasoningEffort: "medium",
-		}
-
-		const modelWithSupported: ModelInfo = {
-			contextWindow: 200_000,
-			supportsPromptCache: true,
-			supportsReasoningEffort: true,
-		}
-
-		expect(shouldUseReasoningEffort({ model: modelWithReasoning, settings: undefined })).toBe(true)
-		expect(shouldUseReasoningEffort({ model: modelWithSupported, settings: undefined })).toBe(false)
-	})
-
-	it("should prioritize model reasoningEffort over settings", () => {
-		const model: ModelInfo = {
-			contextWindow: 200_000,
-			supportsPromptCache: true,
-			supportsReasoningEffort: true,
-			reasoningEffort: "low",
-		}
-
-		const settings: ProviderSettings = {
-			reasoningEffort: "high",
-		}
-
-		// Should return true because model.reasoningEffort exists, regardless of settings
-		expect(shouldUseReasoningEffort({ model, settings })).toBe(true)
-	})
 })

+ 12 - 1
src/shared/api.ts

@@ -1,4 +1,9 @@
-import { type ModelInfo, type ProviderSettings, ANTHROPIC_DEFAULT_MAX_TOKENS } from "@roo-code/types"
+import {
+	type ModelInfo,
+	type ProviderSettings,
+	ANTHROPIC_DEFAULT_MAX_TOKENS,
+	CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS,
+} from "@roo-code/types"
 
 // ApiHandlerOptions
 
@@ -58,6 +63,12 @@ export const getModelMaxOutputTokens = ({
 	model: ModelInfo
 	settings?: ProviderSettings
 }): number | undefined => {
+	// Check for Claude Code specific max output tokens setting
+	if (settings?.apiProvider === "claude-code") {
+		// Return the configured value or default to CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS
+		return settings.claudeCodeMaxOutputTokens || CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS
+	}
+
 	if (shouldUseReasoningBudget({ model, settings })) {
 		return settings?.modelMaxTokens || DEFAULT_HYBRID_REASONING_MODEL_MAX_TOKENS
 	}

+ 39 - 17
webview-ui/src/components/settings/providers/ClaudeCode.tsx

@@ -2,6 +2,7 @@ import React from "react"
 import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 import { type ProviderSettings } from "@roo-code/types"
 import { useAppTranslation } from "@src/i18n/TranslationContext"
+import { Slider } from "@src/components/ui"
 
 interface ClaudeCodeProps {
 	apiConfiguration: ProviderSettings
@@ -16,25 +17,46 @@ export const ClaudeCode: React.FC<ClaudeCodeProps> = ({ apiConfiguration, setApi
 		setApiConfigurationField("claudeCodePath", element.value)
 	}
 
+	const maxOutputTokens = apiConfiguration?.claudeCodeMaxOutputTokens || 8000
+
 	return (
-		<div>
-			<VSCodeTextField
-				value={apiConfiguration?.claudeCodePath || ""}
-				style={{ width: "100%", marginTop: 3 }}
-				type="text"
-				onInput={handleInputChange}
-				placeholder={t("settings:providers.claudeCode.placeholder")}>
-				{t("settings:providers.claudeCode.pathLabel")}
-			</VSCodeTextField>
+		<div className="flex flex-col gap-4">
+			<div>
+				<VSCodeTextField
+					value={apiConfiguration?.claudeCodePath || ""}
+					style={{ width: "100%", marginTop: 3 }}
+					type="text"
+					onInput={handleInputChange}
+					placeholder={t("settings:providers.claudeCode.placeholder")}>
+					{t("settings:providers.claudeCode.pathLabel")}
+				</VSCodeTextField>
+
+				<p
+					style={{
+						fontSize: "12px",
+						marginTop: 3,
+						color: "var(--vscode-descriptionForeground)",
+					}}>
+					{t("settings:providers.claudeCode.description")}
+				</p>
+			</div>
 
-			<p
-				style={{
-					fontSize: "12px",
-					marginTop: 3,
-					color: "var(--vscode-descriptionForeground)",
-				}}>
-				{t("settings:providers.claudeCode.description")}
-			</p>
+			<div className="flex flex-col gap-1">
+				<div className="font-medium">{t("settings:providers.claudeCode.maxTokensLabel")}</div>
+				<div className="flex items-center gap-1">
+					<Slider
+						min={8000}
+						max={64000}
+						step={1024}
+						value={[maxOutputTokens]}
+						onValueChange={([value]) => setApiConfigurationField("claudeCodeMaxOutputTokens", value)}
+					/>
+					<div className="w-16 text-sm text-center">{maxOutputTokens}</div>
+				</div>
+				<p className="text-sm text-vscode-descriptionForeground mt-1">
+					{t("settings:providers.claudeCode.maxTokensDescription")}
+				</p>
+			</div>
 		</div>
 	)
 }

+ 3 - 1
webview-ui/src/i18n/locales/ca/settings.json

@@ -373,7 +373,9 @@
 		"claudeCode": {
 			"pathLabel": "Ruta del Codi Claude",
 			"description": "Ruta opcional al teu CLI de Claude Code. Per defecte, 'claude' si no s'estableix.",
-			"placeholder": "Per defecte: claude"
+			"placeholder": "Per defecte: claude",
+			"maxTokensLabel": "Tokens màxims de sortida",
+			"maxTokensDescription": "Nombre màxim de tokens de sortida per a les respostes de Claude Code. El valor per defecte és 8000."
 		}
 	},
 	"browser": {

+ 3 - 1
webview-ui/src/i18n/locales/de/settings.json

@@ -373,7 +373,9 @@
 		"claudeCode": {
 			"pathLabel": "Claude-Code-Pfad",
 			"description": "Optionaler Pfad zu Ihrer Claude Code CLI. Standard ist 'claude', wenn nicht festgelegt.",
-			"placeholder": "Standard: claude"
+			"placeholder": "Standard: claude",
+			"maxTokensLabel": "Maximale Ausgabe-Tokens",
+			"maxTokensDescription": "Maximale Anzahl an Ausgabe-Tokens für Claude Code-Antworten. Standard ist 8000."
 		}
 	},
 	"browser": {

+ 3 - 1
webview-ui/src/i18n/locales/en/settings.json

@@ -373,7 +373,9 @@
 		"claudeCode": {
 			"pathLabel": "Claude Code Path",
 			"description": "Optional path to your Claude Code CLI. Defaults to 'claude' if not set.",
-			"placeholder": "Default: claude"
+			"placeholder": "Default: claude",
+			"maxTokensLabel": "Max Output Tokens",
+			"maxTokensDescription": "Maximum number of output tokens for Claude Code responses. Default is 8000."
 		}
 	},
 	"browser": {

+ 3 - 1
webview-ui/src/i18n/locales/es/settings.json

@@ -373,7 +373,9 @@
 		"claudeCode": {
 			"pathLabel": "Ruta de Claude Code",
 			"description": "Ruta opcional a su CLI de Claude Code. Por defecto, es 'claude' si no se establece.",
-			"placeholder": "Por defecto: claude"
+			"placeholder": "Por defecto: claude",
+			"maxTokensLabel": "Tokens máximos de salida",
+			"maxTokensDescription": "Número máximo de tokens de salida para las respuestas de Claude Code. El valor predeterminado es 8000."
 		}
 	},
 	"browser": {

+ 3 - 1
webview-ui/src/i18n/locales/fr/settings.json

@@ -373,7 +373,9 @@
 		"claudeCode": {
 			"pathLabel": "Chemin du code Claude",
 			"description": "Chemin facultatif vers votre CLI Claude Code. La valeur par défaut est 'claude' si non défini.",
-			"placeholder": "Défaut : claude"
+			"placeholder": "Défaut : claude",
+			"maxTokensLabel": "Jetons de sortie max",
+			"maxTokensDescription": "Nombre maximum de jetons de sortie pour les réponses de Claude Code. La valeur par défaut est 8000."
 		}
 	},
 	"browser": {

+ 3 - 1
webview-ui/src/i18n/locales/hi/settings.json

@@ -373,7 +373,9 @@
 		"claudeCode": {
 			"pathLabel": "क्लाउड कोड पथ",
 			"description": "आपके क्लाउड कोड सीएलआई का वैकल्पिक पथ। यदि सेट नहीं है तो डिफ़ॉल्ट 'claude' है।",
-			"placeholder": "डिफ़ॉल्ट: claude"
+			"placeholder": "डिफ़ॉल्ट: claude",
+			"maxTokensLabel": "अधिकतम आउटपुट टोकन",
+			"maxTokensDescription": "Claude Code प्रतिक्रियाओं के लिए आउटपुट टोकन की अधिकतम संख्या। डिफ़ॉल्ट 8000 है।"
 		}
 	},
 	"browser": {

+ 3 - 1
webview-ui/src/i18n/locales/id/settings.json

@@ -377,7 +377,9 @@
 		"claudeCode": {
 			"pathLabel": "Jalur Kode Claude",
 			"description": "Jalur opsional ke Claude Code CLI Anda. Defaultnya adalah 'claude' jika tidak diatur.",
-			"placeholder": "Default: claude"
+			"placeholder": "Default: claude",
+			"maxTokensLabel": "Token Output Maks",
+			"maxTokensDescription": "Jumlah maksimum token output untuk respons Claude Code. Default adalah 8000."
 		}
 	},
 	"browser": {

+ 3 - 1
webview-ui/src/i18n/locales/it/settings.json

@@ -373,7 +373,9 @@
 		"claudeCode": {
 			"pathLabel": "Percorso Claude Code",
 			"description": "Percorso facoltativo per la tua CLI Claude Code. Predefinito 'claude' se non impostato.",
-			"placeholder": "Predefinito: claude"
+			"placeholder": "Predefinito: claude",
+			"maxTokensLabel": "Token di output massimi",
+			"maxTokensDescription": "Numero massimo di token di output per le risposte di Claude Code. Il valore predefinito è 8000."
 		}
 	},
 	"browser": {

+ 3 - 1
webview-ui/src/i18n/locales/ja/settings.json

@@ -373,7 +373,9 @@
 		"claudeCode": {
 			"pathLabel": "クロードコードパス",
 			"description": "Claude Code CLIへのオプションパス。設定されていない場合、デフォルトは「claude」です。",
-			"placeholder": "デフォルト:claude"
+			"placeholder": "デフォルト:claude",
+			"maxTokensLabel": "最大出力トークン",
+			"maxTokensDescription": "Claude Codeレスポンスの最大出力トークン数。デフォルトは8000です。"
 		}
 	},
 	"browser": {

+ 3 - 1
webview-ui/src/i18n/locales/ko/settings.json

@@ -373,7 +373,9 @@
 		"claudeCode": {
 			"pathLabel": "클로드 코드 경로",
 			"description": "Claude Code CLI의 선택적 경로입니다. 설정하지 않으면 'claude'가 기본값입니다.",
-			"placeholder": "기본값: claude"
+			"placeholder": "기본값: claude",
+			"maxTokensLabel": "최대 출력 토큰",
+			"maxTokensDescription": "Claude Code 응답의 최대 출력 토큰 수. 기본값은 8000입니다."
 		}
 	},
 	"browser": {

+ 3 - 1
webview-ui/src/i18n/locales/nl/settings.json

@@ -373,7 +373,9 @@
 		"claudeCode": {
 			"pathLabel": "Claude Code Pad",
 			"description": "Optioneel pad naar uw Claude Code CLI. Standaard 'claude' als niet ingesteld.",
-			"placeholder": "Standaard: claude"
+			"placeholder": "Standaard: claude",
+			"maxTokensLabel": "Max Output Tokens",
+			"maxTokensDescription": "Maximaal aantal output-tokens voor Claude Code-reacties. Standaard is 8000."
 		}
 	},
 	"browser": {

+ 3 - 1
webview-ui/src/i18n/locales/pl/settings.json

@@ -373,7 +373,9 @@
 		"claudeCode": {
 			"pathLabel": "Ścieżka Claude Code",
 			"description": "Opcjonalna ścieżka do Twojego CLI Claude Code. Domyślnie 'claude', jeśli nie ustawiono.",
-			"placeholder": "Domyślnie: claude"
+			"placeholder": "Domyślnie: claude",
+			"maxTokensLabel": "Maksymalna liczba tokenów wyjściowych",
+			"maxTokensDescription": "Maksymalna liczba tokenów wyjściowych dla odpowiedzi Claude Code. Domyślnie 8000."
 		}
 	},
 	"browser": {

+ 3 - 1
webview-ui/src/i18n/locales/pt-BR/settings.json

@@ -373,7 +373,9 @@
 		"claudeCode": {
 			"pathLabel": "Caminho do Claude Code",
 			"description": "Caminho opcional para o seu Claude Code CLI. O padrão é 'claude' se não for definido.",
-			"placeholder": "Padrão: claude"
+			"placeholder": "Padrão: claude",
+			"maxTokensLabel": "Tokens de saída máximos",
+			"maxTokensDescription": "Número máximo de tokens de saída para respostas do Claude Code. O padrão é 8000."
 		}
 	},
 	"browser": {

+ 3 - 1
webview-ui/src/i18n/locales/ru/settings.json

@@ -373,7 +373,9 @@
 		"claudeCode": {
 			"pathLabel": "Путь к Claude Code",
 			"description": "Необязательный путь к вашему Claude Code CLI. По умолчанию используется 'claude', если не установлено.",
-			"placeholder": "По умолчанию: claude"
+			"placeholder": "По умолчанию: claude",
+			"maxTokensLabel": "Макс. выходных токенов",
+			"maxTokensDescription": "Максимальное количество выходных токенов для ответов Claude Code. По умолчанию 8000."
 		}
 	},
 	"browser": {

+ 3 - 1
webview-ui/src/i18n/locales/tr/settings.json

@@ -373,7 +373,9 @@
 		"claudeCode": {
 			"pathLabel": "Claude Code Yolu",
 			"description": "Claude Code CLI'nize isteğe bağlı yol. Ayarlanmazsa varsayılan olarak 'claude' kullanılır.",
-			"placeholder": "Varsayılan: claude"
+			"placeholder": "Varsayılan: claude",
+			"maxTokensLabel": "Maksimum Çıktı Token sayısı",
+			"maxTokensDescription": "Claude Code yanıtları için maksimum çıktı token sayısı. Varsayılan 8000'dir."
 		}
 	},
 	"browser": {

+ 3 - 1
webview-ui/src/i18n/locales/vi/settings.json

@@ -373,7 +373,9 @@
 		"claudeCode": {
 			"pathLabel": "Đường dẫn Claude Code",
 			"description": "Đường dẫn tùy chọn đến Claude Code CLI của bạn. Mặc định là 'claude' nếu không được đặt.",
-			"placeholder": "Mặc định: claude"
+			"placeholder": "Mặc định: claude",
+			"maxTokensLabel": "Số token đầu ra tối đa",
+			"maxTokensDescription": "Số lượng token đầu ra tối đa cho các phản hồi của Claude Code. Mặc định là 8000."
 		}
 	},
 	"browser": {

+ 3 - 1
webview-ui/src/i18n/locales/zh-CN/settings.json

@@ -373,7 +373,9 @@
 		"claudeCode": {
 			"pathLabel": "Claude Code 路径",
 			"description": "您的 Claude Code CLI 的可选路径。如果未设置,则默认为 “claude”。",
-			"placeholder": "默认:claude"
+			"placeholder": "默认:claude",
+			"maxTokensLabel": "最大输出 Token",
+			"maxTokensDescription": "Claude Code 响应的最大输出 Token 数量。默认为 8000。"
 		}
 	},
 	"browser": {

+ 3 - 1
webview-ui/src/i18n/locales/zh-TW/settings.json

@@ -373,7 +373,9 @@
 		"claudeCode": {
 			"pathLabel": "Claude Code 路徑",
 			"description": "可選的 Claude Code CLI 路徑。如果未設定,則預設為 'claude'。",
-			"placeholder": "預設:claude"
+			"placeholder": "預設:claude",
+			"maxTokensLabel": "最大輸出 Token",
+			"maxTokensDescription": "Claude Code 回應的最大輸出 Token 數量。預設為 8000。"
 		}
 	},
 	"browser": {