Răsfoiți Sursa

feat: remove Claude Code provider (#10883)

Daniel 3 săptămâni în urmă
părinte
comite
7f854c0dd7
33 a modificat fișierele cu 46 adăugiri și 4107 ștergeri
  1. 0 5
      packages/types/src/__tests__/provider-settings.test.ts
  2. 1 9
      packages/types/src/provider-settings.ts
  3. 0 46
      packages/types/src/providers/__tests__/claude-code.spec.ts
  4. 0 154
      packages/types/src/providers/claude-code.ts
  5. 0 4
      packages/types/src/providers/index.ts
  6. 0 5
      packages/types/src/vscode-extension-host.ts
  7. 0 3
      src/api/index.ts
  8. 0 169
      src/api/providers/__tests__/claude-code-caching.spec.ts
  9. 0 606
      src/api/providers/__tests__/claude-code.spec.ts
  10. 0 378
      src/api/providers/claude-code.ts
  11. 0 1
      src/api/providers/index.ts
  12. 2 1
      src/core/config/ProviderSettingsManager.ts
  13. 25 35
      src/core/config/__tests__/importExport.spec.ts
  14. 0 8
      src/core/webview/ClineProvider.ts
  15. 0 70
      src/core/webview/webviewMessageHandler.ts
  16. 0 4
      src/extension.ts
  17. 0 235
      src/integrations/claude-code/__tests__/oauth.spec.ts
  18. 0 585
      src/integrations/claude-code/__tests__/streaming-client.spec.ts
  19. 0 638
      src/integrations/claude-code/oauth.ts
  20. 0 759
      src/integrations/claude-code/streaming-client.ts
  21. 1 1
      src/shared/__tests__/api.spec.ts
  22. 0 7
      src/shared/__tests__/checkExistApiConfig.spec.ts
  23. 2 5
      src/shared/checkExistApiConfig.ts
  24. 14 26
      webview-ui/src/components/chat/ChatRow.tsx
  25. 1 14
      webview-ui/src/components/settings/ApiOptions.tsx
  26. 0 3
      webview-ui/src/components/settings/constants.ts
  27. 0 71
      webview-ui/src/components/settings/providers/ClaudeCode.tsx
  28. 0 181
      webview-ui/src/components/settings/providers/ClaudeCodeRateLimitDashboard.tsx
  29. 0 1
      webview-ui/src/components/settings/providers/index.ts
  30. 0 1
      webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts
  31. 0 1
      webview-ui/src/components/settings/utils/providerModelConfig.ts
  32. 0 71
      webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts
  33. 0 10
      webview-ui/src/components/ui/hooks/useSelectedModel.ts

+ 0 - 5
packages/types/src/__tests__/provider-settings.test.ts

@@ -7,11 +7,6 @@ describe("getApiProtocol", () => {
 			expect(getApiProtocol("anthropic", "gpt-4")).toBe("anthropic")
 		})
 
-		it("should return 'anthropic' for claude-code provider", () => {
-			expect(getApiProtocol("claude-code")).toBe("anthropic")
-			expect(getApiProtocol("claude-code", "some-model")).toBe("anthropic")
-		})
-
 		it("should return 'anthropic' for bedrock provider", () => {
 			expect(getApiProtocol("bedrock")).toBe("anthropic")
 			expect(getApiProtocol("bedrock", "gpt-4")).toBe("anthropic")

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

@@ -7,7 +7,6 @@ import {
 	basetenModels,
 	bedrockModels,
 	cerebrasModels,
-	claudeCodeModels,
 	deepSeekModels,
 	doubaoModels,
 	featherlessModels,
@@ -123,7 +122,6 @@ export const providerNames = [
 	"bedrock",
 	"baseten",
 	"cerebras",
-	"claude-code",
 	"doubao",
 	"deepseek",
 	"featherless",
@@ -199,8 +197,6 @@ const anthropicSchema = apiModelIdProviderModelSchema.extend({
 	anthropicBeta1MContext: z.boolean().optional(), // Enable 'context-1m-2025-08-07' beta for 1M context window.
 })
 
-const claudeCodeSchema = apiModelIdProviderModelSchema.extend({})
-
 const openRouterSchema = baseProviderSettingsSchema.extend({
 	openRouterApiKey: z.string().optional(),
 	openRouterModelId: z.string().optional(),
@@ -429,7 +425,6 @@ const defaultSchema = z.object({
 
 export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProvider", [
 	anthropicSchema.merge(z.object({ apiProvider: z.literal("anthropic") })),
-	claudeCodeSchema.merge(z.object({ apiProvider: z.literal("claude-code") })),
 	openRouterSchema.merge(z.object({ apiProvider: z.literal("openrouter") })),
 	bedrockSchema.merge(z.object({ apiProvider: z.literal("bedrock") })),
 	vertexSchema.merge(z.object({ apiProvider: z.literal("vertex") })),
@@ -471,7 +466,6 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
 export const providerSettingsSchema = z.object({
 	apiProvider: providerNamesSchema.optional(),
 	...anthropicSchema.shape,
-	...claudeCodeSchema.shape,
 	...openRouterSchema.shape,
 	...bedrockSchema.shape,
 	...vertexSchema.shape,
@@ -560,7 +554,6 @@ export const isTypicalProvider = (key: unknown): key is TypicalProvider =>
 
 export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
 	anthropic: "apiModelId",
-	"claude-code": "apiModelId",
 	openrouter: "openRouterModelId",
 	bedrock: "apiModelId",
 	vertex: "apiModelId",
@@ -600,7 +593,7 @@ export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
  */
 
 // Providers that use Anthropic-style API protocol.
-export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "claude-code", "bedrock", "minimax"]
+export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "bedrock", "minimax"]
 
 export const getApiProtocol = (provider: ProviderName | undefined, modelId?: string): "anthropic" | "openai" => {
 	if (provider && ANTHROPIC_STYLE_PROVIDERS.includes(provider)) {
@@ -647,7 +640,6 @@ export const MODELS_BY_PROVIDER: Record<
 		label: "Cerebras",
 		models: Object.keys(cerebrasModels),
 	},
-	"claude-code": { id: "claude-code", label: "Claude Code", models: Object.keys(claudeCodeModels) },
 	deepseek: {
 		id: "deepseek",
 		label: "DeepSeek",

+ 0 - 46
packages/types/src/providers/__tests__/claude-code.spec.ts

@@ -1,46 +0,0 @@
-import { normalizeClaudeCodeModelId } from "../claude-code.js"
-
-describe("normalizeClaudeCodeModelId", () => {
-	test("should return valid model IDs unchanged", () => {
-		expect(normalizeClaudeCodeModelId("claude-sonnet-4-5")).toBe("claude-sonnet-4-5")
-		expect(normalizeClaudeCodeModelId("claude-opus-4-5")).toBe("claude-opus-4-5")
-		expect(normalizeClaudeCodeModelId("claude-haiku-4-5")).toBe("claude-haiku-4-5")
-	})
-
-	test("should normalize sonnet models with date suffix to claude-sonnet-4-5", () => {
-		// Sonnet 4.5 with date
-		expect(normalizeClaudeCodeModelId("claude-sonnet-4-5-20250929")).toBe("claude-sonnet-4-5")
-		// Sonnet 4 (legacy)
-		expect(normalizeClaudeCodeModelId("claude-sonnet-4-20250514")).toBe("claude-sonnet-4-5")
-		// Claude 3.7 Sonnet
-		expect(normalizeClaudeCodeModelId("claude-3-7-sonnet-20250219")).toBe("claude-sonnet-4-5")
-		// Claude 3.5 Sonnet
-		expect(normalizeClaudeCodeModelId("claude-3-5-sonnet-20241022")).toBe("claude-sonnet-4-5")
-	})
-
-	test("should normalize opus models with date suffix to claude-opus-4-5", () => {
-		// Opus 4.5 with date
-		expect(normalizeClaudeCodeModelId("claude-opus-4-5-20251101")).toBe("claude-opus-4-5")
-		// Opus 4.1 (legacy)
-		expect(normalizeClaudeCodeModelId("claude-opus-4-1-20250805")).toBe("claude-opus-4-5")
-		// Opus 4 (legacy)
-		expect(normalizeClaudeCodeModelId("claude-opus-4-20250514")).toBe("claude-opus-4-5")
-	})
-
-	test("should normalize haiku models with date suffix to claude-haiku-4-5", () => {
-		// Haiku 4.5 with date
-		expect(normalizeClaudeCodeModelId("claude-haiku-4-5-20251001")).toBe("claude-haiku-4-5")
-		// Claude 3.5 Haiku
-		expect(normalizeClaudeCodeModelId("claude-3-5-haiku-20241022")).toBe("claude-haiku-4-5")
-	})
-
-	test("should handle case-insensitive model family matching", () => {
-		expect(normalizeClaudeCodeModelId("Claude-Sonnet-4-5-20250929")).toBe("claude-sonnet-4-5")
-		expect(normalizeClaudeCodeModelId("CLAUDE-OPUS-4-5-20251101")).toBe("claude-opus-4-5")
-	})
-
-	test("should fallback to default for unrecognized models", () => {
-		expect(normalizeClaudeCodeModelId("unknown-model")).toBe("claude-sonnet-4-5")
-		expect(normalizeClaudeCodeModelId("gpt-4")).toBe("claude-sonnet-4-5")
-	})
-})

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

@@ -1,154 +0,0 @@
-import type { ModelInfo } from "../model.js"
-
-/**
- * Rate limit information from Claude Code API
- */
-export interface ClaudeCodeRateLimitInfo {
-	// 5-hour limit info
-	fiveHour: {
-		status: string
-		utilization: number
-		resetTime: number // Unix timestamp
-	}
-	// 7-day (weekly) limit info (Sonnet-specific)
-	weekly?: {
-		status: string
-		utilization: number
-		resetTime: number // Unix timestamp
-	}
-	// 7-day unified limit info
-	weeklyUnified?: {
-		status: string
-		utilization: number
-		resetTime: number // Unix timestamp
-	}
-	// Representative claim type
-	representativeClaim?: string
-	// Overage status
-	overage?: {
-		status: string
-		disabledReason?: string
-	}
-	// Fallback percentage
-	fallbackPercentage?: number
-	// Organization ID
-	organizationId?: string
-	// Timestamp when this was fetched
-	fetchedAt: number
-}
-
-// Regex pattern to strip date suffix from model names
-const DATE_SUFFIX_PATTERN = /-\d{8}$/
-
-// Models that work with Claude Code OAuth tokens
-// See: https://docs.anthropic.com/en/docs/claude-code
-// NOTE: Claude Code is subscription-based with no per-token cost - pricing fields are 0
-export const claudeCodeModels = {
-	"claude-haiku-4-5": {
-		maxTokens: 32768,
-		contextWindow: 200_000,
-		supportsImages: true,
-		supportsPromptCache: true,
-		supportsReasoningEffort: ["disable", "low", "medium", "high"],
-		reasoningEffort: "medium",
-		description: "Claude Haiku 4.5 - Fast and efficient with thinking",
-	},
-	"claude-sonnet-4-5": {
-		maxTokens: 32768,
-		contextWindow: 200_000,
-		supportsImages: true,
-		supportsPromptCache: true,
-		supportsReasoningEffort: ["disable", "low", "medium", "high"],
-		reasoningEffort: "medium",
-		description: "Claude Sonnet 4.5 - Balanced performance with thinking",
-	},
-	"claude-opus-4-5": {
-		maxTokens: 32768,
-		contextWindow: 200_000,
-		supportsImages: true,
-		supportsPromptCache: true,
-		supportsReasoningEffort: ["disable", "low", "medium", "high"],
-		reasoningEffort: "medium",
-		description: "Claude Opus 4.5 - Most capable with thinking",
-	},
-} as const satisfies Record<string, ModelInfo>
-
-// Claude Code - Only models that work with Claude Code OAuth tokens
-export type ClaudeCodeModelId = keyof typeof claudeCodeModels
-export const claudeCodeDefaultModelId: ClaudeCodeModelId = "claude-sonnet-4-5"
-
-/**
- * Model family patterns for normalization.
- * Maps regex patterns to their canonical Claude Code model IDs.
- *
- * Order matters - more specific patterns should come first.
- */
-const MODEL_FAMILY_PATTERNS: Array<{ pattern: RegExp; target: ClaudeCodeModelId }> = [
-	// Opus models (any version) → claude-opus-4-5
-	{ pattern: /opus/i, target: "claude-opus-4-5" },
-	// Haiku models (any version) → claude-haiku-4-5
-	{ pattern: /haiku/i, target: "claude-haiku-4-5" },
-	// Sonnet models (any version) → claude-sonnet-4-5
-	{ pattern: /sonnet/i, target: "claude-sonnet-4-5" },
-]
-
-/**
- * Normalizes a Claude model ID to a valid Claude Code model ID.
- *
- * This function handles backward compatibility for legacy model names
- * that may include version numbers or date suffixes. It maps:
- * - claude-sonnet-4-5-20250929, claude-sonnet-4-20250514, claude-3-7-sonnet-20250219, claude-3-5-sonnet-20241022 → claude-sonnet-4-5
- * - claude-opus-4-5-20251101, claude-opus-4-1-20250805, claude-opus-4-20250514 → claude-opus-4-5
- * - claude-haiku-4-5-20251001, claude-3-5-haiku-20241022 → claude-haiku-4-5
- *
- * @param modelId - The model ID to normalize (may be a legacy format)
- * @returns A valid ClaudeCodeModelId, or the original ID if already valid
- *
- * @example
- * normalizeClaudeCodeModelId("claude-sonnet-4-5") // returns "claude-sonnet-4-5"
- * normalizeClaudeCodeModelId("claude-3-5-sonnet-20241022") // returns "claude-sonnet-4-5"
- * normalizeClaudeCodeModelId("claude-opus-4-1-20250805") // returns "claude-opus-4-5"
- */
-export function normalizeClaudeCodeModelId(modelId: string): ClaudeCodeModelId {
-	// If already a valid model ID, return as-is
-	// Use Object.hasOwn() instead of 'in' operator to avoid matching inherited properties like 'toString'
-	if (Object.hasOwn(claudeCodeModels, modelId)) {
-		return modelId as ClaudeCodeModelId
-	}
-
-	// Strip date suffix if present (e.g., -20250514)
-	const withoutDate = modelId.replace(DATE_SUFFIX_PATTERN, "")
-
-	// Check if stripping the date makes it valid
-	if (Object.hasOwn(claudeCodeModels, withoutDate)) {
-		return withoutDate as ClaudeCodeModelId
-	}
-
-	// Match by model family
-	for (const { pattern, target } of MODEL_FAMILY_PATTERNS) {
-		if (pattern.test(modelId)) {
-			return target
-		}
-	}
-
-	// Fallback to default if no match (shouldn't happen with valid Claude models)
-	return claudeCodeDefaultModelId
-}
-
-/**
- * Reasoning effort configuration for Claude Code thinking mode.
- * Maps reasoning effort level to budget_tokens for the thinking process.
- *
- * Note: With interleaved thinking (enabled via beta header), budget_tokens
- * can exceed max_tokens as the token limit becomes the entire context window.
- * The max_tokens is drawn from the model's maxTokens definition.
- *
- * @see https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#interleaved-thinking
- */
-export const claudeCodeReasoningConfig = {
-	low: { budgetTokens: 16_000 },
-	medium: { budgetTokens: 32_000 },
-	high: { budgetTokens: 64_000 },
-} as const
-
-export type ClaudeCodeReasoningLevel = keyof typeof claudeCodeReasoningConfig

+ 0 - 4
packages/types/src/providers/index.ts

@@ -3,7 +3,6 @@ export * from "./baseten.js"
 export * from "./bedrock.js"
 export * from "./cerebras.js"
 export * from "./chutes.js"
-export * from "./claude-code.js"
 export * from "./deepseek.js"
 export * from "./doubao.js"
 export * from "./featherless.js"
@@ -39,7 +38,6 @@ import { basetenDefaultModelId } from "./baseten.js"
 import { bedrockDefaultModelId } from "./bedrock.js"
 import { cerebrasDefaultModelId } from "./cerebras.js"
 import { chutesDefaultModelId } from "./chutes.js"
-import { claudeCodeDefaultModelId } from "./claude-code.js"
 import { deepSeekDefaultModelId } from "./deepseek.js"
 import { doubaoDefaultModelId } from "./doubao.js"
 import { featherlessDefaultModelId } from "./featherless.js"
@@ -128,8 +126,6 @@ export function getProviderDefaultModelId(
 			return deepInfraDefaultModelId
 		case "vscode-lm":
 			return vscodeLlmDefaultModelId
-		case "claude-code":
-			return claudeCodeDefaultModelId
 		case "cerebras":
 			return cerebrasDefaultModelId
 		case "sambanova":

+ 0 - 5
packages/types/src/vscode-extension-host.ts

@@ -95,7 +95,6 @@ export interface ExtensionMessage {
 		| "interactionRequired"
 		| "browserSessionUpdate"
 		| "browserSessionNavigate"
-		| "claudeCodeRateLimits"
 		| "customToolsResult"
 		| "modes"
 		| "taskWithAggregatedCosts"
@@ -406,7 +405,6 @@ export type ExtensionState = Pick<
 	remoteControlEnabled: boolean
 	taskSyncEnabled: boolean
 	featureRoomoteControlEnabled: boolean
-	claudeCodeIsAuthenticated?: boolean
 	openAiCodexIsAuthenticated?: boolean
 	debug?: boolean
 }
@@ -535,8 +533,6 @@ export interface WebviewMessage {
 		| "cloudLandingPageSignIn"
 		| "rooCloudSignOut"
 		| "rooCloudManualUrl"
-		| "claudeCodeSignIn"
-		| "claudeCodeSignOut"
 		| "openAiCodexSignIn"
 		| "openAiCodexSignOut"
 		| "switchOrganization"
@@ -591,7 +587,6 @@ export interface WebviewMessage {
 		| "openDebugApiHistory"
 		| "openDebugUiHistory"
 		| "downloadErrorDiagnostics"
-		| "requestClaudeCodeRateLimits"
 		| "requestOpenAiCodexRateLimits"
 		| "refreshCustomTools"
 		| "requestModes"

+ 0 - 3
src/api/index.ts

@@ -29,7 +29,6 @@ import {
 	HuggingFaceHandler,
 	ChutesHandler,
 	LiteLLMHandler,
-	ClaudeCodeHandler,
 	QwenCodeHandler,
 	SambaNovaHandler,
 	IOIntelligenceHandler,
@@ -127,8 +126,6 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
 	switch (apiProvider) {
 		case "anthropic":
 			return new AnthropicHandler(options)
-		case "claude-code":
-			return new ClaudeCodeHandler(options)
 		case "openrouter":
 			return new OpenRouterHandler(options)
 		case "bedrock":

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

@@ -1,169 +0,0 @@
-import { ClaudeCodeHandler } from "../claude-code"
-import type { ApiHandlerOptions } from "../../../shared/api"
-import type { StreamChunk } from "../../../integrations/claude-code/streaming-client"
-import type { ApiStreamUsageChunk } from "../../transform/stream"
-
-// Mock the OAuth manager
-vi.mock("../../../integrations/claude-code/oauth", () => ({
-	claudeCodeOAuthManager: {
-		getAccessToken: vi.fn(),
-		getEmail: vi.fn(),
-		loadCredentials: vi.fn(),
-		saveCredentials: vi.fn(),
-		clearCredentials: vi.fn(),
-		isAuthenticated: vi.fn(),
-	},
-	generateUserId: vi.fn(() => "user_abc123_account_def456_session_ghi789"),
-}))
-
-// Mock the streaming client
-vi.mock("../../../integrations/claude-code/streaming-client", () => ({
-	createStreamingMessage: vi.fn(),
-}))
-
-const { claudeCodeOAuthManager } = await import("../../../integrations/claude-code/oauth")
-const { createStreamingMessage } = await import("../../../integrations/claude-code/streaming-client")
-
-const mockGetAccessToken = vi.mocked(claudeCodeOAuthManager.getAccessToken)
-const mockCreateStreamingMessage = vi.mocked(createStreamingMessage)
-
-describe("ClaudeCodeHandler - Caching Support", () => {
-	let handler: ClaudeCodeHandler
-	const mockOptions: ApiHandlerOptions = {
-		apiModelId: "claude-sonnet-4-5",
-	}
-
-	beforeEach(() => {
-		handler = new ClaudeCodeHandler(mockOptions)
-		vi.clearAllMocks()
-		mockGetAccessToken.mockResolvedValue("test-access-token")
-	})
-
-	it("should collect cache read tokens from API response", async () => {
-		const mockStream = async function* (): AsyncGenerator<StreamChunk> {
-			yield { type: "text", text: "Hello!" }
-			yield {
-				type: "usage",
-				inputTokens: 100,
-				outputTokens: 50,
-				cacheReadTokens: 80,
-				cacheWriteTokens: 20,
-			}
-		}
-
-		mockCreateStreamingMessage.mockReturnValue(mockStream())
-
-		const stream = handler.createMessage("System prompt", [{ role: "user", content: "Hello" }])
-
-		const chunks = []
-		for await (const chunk of stream) {
-			chunks.push(chunk)
-		}
-
-		// Find the usage chunk
-		const usageChunk = chunks.find((c) => c.type === "usage" && "totalCost" in c) as ApiStreamUsageChunk | undefined
-		expect(usageChunk).toBeDefined()
-		expect(usageChunk!.inputTokens).toBe(100)
-		expect(usageChunk!.outputTokens).toBe(50)
-		expect(usageChunk!.cacheReadTokens).toBe(80)
-		expect(usageChunk!.cacheWriteTokens).toBe(20)
-	})
-
-	it("should accumulate cache tokens across multiple messages", async () => {
-		// Note: The streaming client handles accumulation internally.
-		// Each usage chunk represents the accumulated totals for that point in the stream.
-		// This test verifies that we correctly pass through the accumulated values.
-		const mockStream = async function* (): AsyncGenerator<StreamChunk> {
-			yield { type: "text", text: "Part 1" }
-			yield {
-				type: "usage",
-				inputTokens: 50,
-				outputTokens: 25,
-				cacheReadTokens: 40,
-				cacheWriteTokens: 10,
-			}
-			yield { type: "text", text: "Part 2" }
-			yield {
-				type: "usage",
-				inputTokens: 100, // Accumulated: 50 + 50
-				outputTokens: 50, // Accumulated: 25 + 25
-				cacheReadTokens: 70, // Accumulated: 40 + 30
-				cacheWriteTokens: 30, // Accumulated: 10 + 20
-			}
-		}
-
-		mockCreateStreamingMessage.mockReturnValue(mockStream())
-
-		const stream = handler.createMessage("System prompt", [{ role: "user", content: "Hello" }])
-
-		const chunks = []
-		for await (const chunk of stream) {
-			chunks.push(chunk)
-		}
-
-		// Get the last usage chunk which should have accumulated totals
-		const usageChunks = chunks.filter((c) => c.type === "usage" && "totalCost" in c) as ApiStreamUsageChunk[]
-		expect(usageChunks.length).toBe(2)
-
-		const lastUsageChunk = usageChunks[usageChunks.length - 1]
-		expect(lastUsageChunk.inputTokens).toBe(100) // 50 + 50
-		expect(lastUsageChunk.outputTokens).toBe(50) // 25 + 25
-		expect(lastUsageChunk.cacheReadTokens).toBe(70) // 40 + 30
-		expect(lastUsageChunk.cacheWriteTokens).toBe(30) // 10 + 20
-	})
-
-	it("should handle missing cache token fields gracefully", async () => {
-		const mockStream = async function* (): AsyncGenerator<StreamChunk> {
-			yield { type: "text", text: "Hello!" }
-			yield {
-				type: "usage",
-				inputTokens: 100,
-				outputTokens: 50,
-				// No cache tokens provided
-			}
-		}
-
-		mockCreateStreamingMessage.mockReturnValue(mockStream())
-
-		const stream = handler.createMessage("System prompt", [{ role: "user", content: "Hello" }])
-
-		const chunks = []
-		for await (const chunk of stream) {
-			chunks.push(chunk)
-		}
-
-		const usageChunk = chunks.find((c) => c.type === "usage" && "totalCost" in c) as ApiStreamUsageChunk | undefined
-		expect(usageChunk).toBeDefined()
-		expect(usageChunk!.inputTokens).toBe(100)
-		expect(usageChunk!.outputTokens).toBe(50)
-		expect(usageChunk!.cacheReadTokens).toBeUndefined()
-		expect(usageChunk!.cacheWriteTokens).toBeUndefined()
-	})
-
-	it("should report zero cost for subscription usage", async () => {
-		// Claude Code is always subscription-based, cost should always be 0
-		const mockStream = async function* (): AsyncGenerator<StreamChunk> {
-			yield { type: "text", text: "Hello!" }
-			yield {
-				type: "usage",
-				inputTokens: 100,
-				outputTokens: 50,
-				cacheReadTokens: 80,
-				cacheWriteTokens: 20,
-			}
-		}
-
-		mockCreateStreamingMessage.mockReturnValue(mockStream())
-
-		const stream = handler.createMessage("System prompt", [{ role: "user", content: "Hello" }])
-
-		const chunks = []
-		for await (const chunk of stream) {
-			chunks.push(chunk)
-		}
-
-		const usageChunk = chunks.find((c) => c.type === "usage" && "totalCost" in c) as ApiStreamUsageChunk | undefined
-		expect(usageChunk).toBeDefined()
-		expect(usageChunk!.totalCost).toBe(0) // Should always be 0 for Claude Code (subscription-based)
-	})
-})

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

@@ -1,606 +0,0 @@
-import { ClaudeCodeHandler } from "../claude-code"
-import { ApiHandlerOptions } from "../../../shared/api"
-import type { StreamChunk } from "../../../integrations/claude-code/streaming-client"
-
-// Mock the OAuth manager
-vi.mock("../../../integrations/claude-code/oauth", () => ({
-	claudeCodeOAuthManager: {
-		getAccessToken: vi.fn(),
-		getEmail: vi.fn(),
-		loadCredentials: vi.fn(),
-		saveCredentials: vi.fn(),
-		clearCredentials: vi.fn(),
-		isAuthenticated: vi.fn(),
-	},
-	generateUserId: vi.fn(() => "user_abc123_account_def456_session_ghi789"),
-}))
-
-// Mock the streaming client
-vi.mock("../../../integrations/claude-code/streaming-client", () => ({
-	createStreamingMessage: vi.fn(),
-}))
-
-const { claudeCodeOAuthManager } = await import("../../../integrations/claude-code/oauth")
-const { createStreamingMessage } = await import("../../../integrations/claude-code/streaming-client")
-
-const mockGetAccessToken = vi.mocked(claudeCodeOAuthManager.getAccessToken)
-const mockGetEmail = vi.mocked(claudeCodeOAuthManager.getEmail)
-const mockCreateStreamingMessage = vi.mocked(createStreamingMessage)
-
-describe("ClaudeCodeHandler", () => {
-	let handler: ClaudeCodeHandler
-
-	beforeEach(() => {
-		vi.clearAllMocks()
-		const options: ApiHandlerOptions = {
-			apiModelId: "claude-sonnet-4-5",
-		}
-		handler = new ClaudeCodeHandler(options)
-	})
-
-	test("should create handler with correct model configuration", () => {
-		const model = handler.getModel()
-		expect(model.id).toBe("claude-sonnet-4-5")
-		expect(model.info.supportsImages).toBe(true)
-		expect(model.info.supportsPromptCache).toBe(true)
-	})
-
-	test("should use default model when invalid model provided", () => {
-		const options: ApiHandlerOptions = {
-			apiModelId: "invalid-model",
-		}
-		const handlerWithInvalidModel = new ClaudeCodeHandler(options)
-		const model = handlerWithInvalidModel.getModel()
-
-		expect(model.id).toBe("claude-sonnet-4-5") // default model
-	})
-
-	test("should return model maxTokens from model definition", () => {
-		const options: ApiHandlerOptions = {
-			apiModelId: "claude-opus-4-5",
-		}
-		const handlerWithModel = new ClaudeCodeHandler(options)
-		const model = handlerWithModel.getModel()
-
-		expect(model.id).toBe("claude-opus-4-5")
-		// Model maxTokens is 32768 as defined in claudeCodeModels for opus
-		expect(model.info.maxTokens).toBe(32768)
-	})
-
-	test("should support reasoning effort configuration", () => {
-		const options: ApiHandlerOptions = {
-			apiModelId: "claude-sonnet-4-5",
-		}
-		const handler = new ClaudeCodeHandler(options)
-		const model = handler.getModel()
-
-		// Default model has supportsReasoningEffort
-		expect(model.info.supportsReasoningEffort).toEqual(["disable", "low", "medium", "high"])
-		expect(model.info.reasoningEffort).toBe("medium")
-	})
-
-	test("should throw error when not authenticated", async () => {
-		const systemPrompt = "You are a helpful assistant"
-		const messages = [{ role: "user" as const, content: "Hello" }]
-
-		mockGetAccessToken.mockResolvedValue(null)
-
-		const stream = handler.createMessage(systemPrompt, messages)
-		const iterator = stream[Symbol.asyncIterator]()
-
-		await expect(iterator.next()).rejects.toThrow(/not authenticated/i)
-	})
-
-	test("should call createStreamingMessage with thinking enabled by default", async () => {
-		const systemPrompt = "You are a helpful assistant"
-		const messages = [{ role: "user" as const, content: "Hello" }]
-
-		mockGetAccessToken.mockResolvedValue("test-access-token")
-
-		// Mock empty async generator
-		const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
-			// Empty generator for basic test
-		}
-		mockCreateStreamingMessage.mockReturnValue(mockGenerator())
-
-		const stream = handler.createMessage(systemPrompt, messages)
-
-		// Need to start iterating to trigger the call
-		const iterator = stream[Symbol.asyncIterator]()
-		await iterator.next()
-
-		// Verify createStreamingMessage was called with correct parameters
-		// Default model has reasoning effort of "medium" so thinking should be enabled
-		// With interleaved thinking, maxTokens comes from model definition (32768 for claude-sonnet-4-5)
-		expect(mockCreateStreamingMessage).toHaveBeenCalledWith(
-			expect.objectContaining({
-				accessToken: "test-access-token",
-				model: "claude-sonnet-4-5",
-				systemPrompt,
-				messages,
-				maxTokens: 32768, // model's maxTokens from claudeCodeModels definition
-				thinking: {
-					type: "enabled",
-					budget_tokens: 32000, // medium reasoning budget_tokens
-				},
-				// Tools are now always present (minimum 6 from ALWAYS_AVAILABLE_TOOLS)
-				tools: expect.any(Array),
-				toolChoice: expect.any(Object),
-				metadata: {
-					user_id: "user_abc123_account_def456_session_ghi789",
-				},
-			}),
-		)
-	})
-
-	test("should disable thinking when reasoningEffort is set to disable", async () => {
-		const options: ApiHandlerOptions = {
-			apiModelId: "claude-sonnet-4-5",
-			reasoningEffort: "disable",
-		}
-		const handlerNoThinking = new ClaudeCodeHandler(options)
-
-		const systemPrompt = "You are a helpful assistant"
-		const messages = [{ role: "user" as const, content: "Hello" }]
-
-		mockGetAccessToken.mockResolvedValue("test-access-token")
-
-		// Mock empty async generator
-		const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
-			// Empty generator for basic test
-		}
-		mockCreateStreamingMessage.mockReturnValue(mockGenerator())
-
-		const stream = handlerNoThinking.createMessage(systemPrompt, messages)
-
-		// Need to start iterating to trigger the call
-		const iterator = stream[Symbol.asyncIterator]()
-		await iterator.next()
-
-		// Verify createStreamingMessage was called with thinking disabled
-		expect(mockCreateStreamingMessage).toHaveBeenCalledWith(
-			expect.objectContaining({
-				accessToken: "test-access-token",
-				model: "claude-sonnet-4-5",
-				systemPrompt,
-				messages,
-				maxTokens: 32768, // model maxTokens from claudeCodeModels definition
-				thinking: { type: "disabled" },
-				// Tools are now always present (minimum 6 from ALWAYS_AVAILABLE_TOOLS)
-				tools: expect.any(Array),
-				toolChoice: expect.any(Object),
-				metadata: {
-					user_id: "user_abc123_account_def456_session_ghi789",
-				},
-			}),
-		)
-	})
-
-	test("should use high reasoning config when reasoningEffort is high", async () => {
-		const options: ApiHandlerOptions = {
-			apiModelId: "claude-sonnet-4-5",
-			reasoningEffort: "high",
-		}
-		const handlerHighThinking = new ClaudeCodeHandler(options)
-
-		const systemPrompt = "You are a helpful assistant"
-		const messages = [{ role: "user" as const, content: "Hello" }]
-
-		mockGetAccessToken.mockResolvedValue("test-access-token")
-
-		// Mock empty async generator
-		const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
-			// Empty generator for basic test
-		}
-		mockCreateStreamingMessage.mockReturnValue(mockGenerator())
-
-		const stream = handlerHighThinking.createMessage(systemPrompt, messages)
-
-		// Need to start iterating to trigger the call
-		const iterator = stream[Symbol.asyncIterator]()
-		await iterator.next()
-
-		// Verify createStreamingMessage was called with high thinking config
-		// With interleaved thinking, maxTokens comes from model definition (32768 for claude-sonnet-4-5)
-		expect(mockCreateStreamingMessage).toHaveBeenCalledWith(
-			expect.objectContaining({
-				accessToken: "test-access-token",
-				model: "claude-sonnet-4-5",
-				systemPrompt,
-				messages,
-				maxTokens: 32768, // model's maxTokens from claudeCodeModels definition
-				thinking: {
-					type: "enabled",
-					budget_tokens: 64000, // high reasoning budget_tokens
-				},
-				// Tools are now always present (minimum 6 from ALWAYS_AVAILABLE_TOOLS)
-				tools: expect.any(Array),
-				toolChoice: expect.any(Object),
-				metadata: {
-					user_id: "user_abc123_account_def456_session_ghi789",
-				},
-			}),
-		)
-	})
-
-	test("should handle text content from streaming", async () => {
-		const systemPrompt = "You are a helpful assistant"
-		const messages = [{ role: "user" as const, content: "Hello" }]
-
-		mockGetAccessToken.mockResolvedValue("test-access-token")
-
-		// Mock async generator that yields text chunks
-		const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
-			yield { type: "text", text: "Hello " }
-			yield { type: "text", text: "there!" }
-		}
-
-		mockCreateStreamingMessage.mockReturnValue(mockGenerator())
-
-		const stream = handler.createMessage(systemPrompt, messages)
-		const results = []
-
-		for await (const chunk of stream) {
-			results.push(chunk)
-		}
-
-		expect(results).toHaveLength(2)
-		expect(results[0]).toEqual({
-			type: "text",
-			text: "Hello ",
-		})
-		expect(results[1]).toEqual({
-			type: "text",
-			text: "there!",
-		})
-	})
-
-	test("should handle reasoning content from streaming", async () => {
-		const systemPrompt = "You are a helpful assistant"
-		const messages = [{ role: "user" as const, content: "Hello" }]
-
-		mockGetAccessToken.mockResolvedValue("test-access-token")
-
-		// Mock async generator that yields reasoning chunks
-		const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
-			yield { type: "reasoning", text: "I need to think about this carefully..." }
-		}
-
-		mockCreateStreamingMessage.mockReturnValue(mockGenerator())
-
-		const stream = handler.createMessage(systemPrompt, messages)
-		const results = []
-
-		for await (const chunk of stream) {
-			results.push(chunk)
-		}
-
-		expect(results).toHaveLength(1)
-		expect(results[0]).toEqual({
-			type: "reasoning",
-			text: "I need to think about this carefully...",
-		})
-	})
-
-	test("should handle mixed content types from streaming", async () => {
-		const systemPrompt = "You are a helpful assistant"
-		const messages = [{ role: "user" as const, content: "Hello" }]
-
-		mockGetAccessToken.mockResolvedValue("test-access-token")
-
-		// Mock async generator that yields mixed content
-		const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
-			yield { type: "reasoning", text: "Let me think about this..." }
-			yield { type: "text", text: "Here's my response!" }
-		}
-
-		mockCreateStreamingMessage.mockReturnValue(mockGenerator())
-
-		const stream = handler.createMessage(systemPrompt, messages)
-		const results = []
-
-		for await (const chunk of stream) {
-			results.push(chunk)
-		}
-
-		expect(results).toHaveLength(2)
-		expect(results[0]).toEqual({
-			type: "reasoning",
-			text: "Let me think about this...",
-		})
-		expect(results[1]).toEqual({
-			type: "text",
-			text: "Here's my response!",
-		})
-	})
-
-	test("should handle tool call partial chunks from streaming", async () => {
-		const systemPrompt = "You are a helpful assistant"
-		const messages = [{ role: "user" as const, content: "Hello" }]
-
-		mockGetAccessToken.mockResolvedValue("test-access-token")
-
-		// Mock async generator that yields tool call partial chunks
-		const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
-			yield { type: "tool_call_partial", index: 0, id: "tool_123", name: "read_file", arguments: undefined }
-			yield { type: "tool_call_partial", index: 0, id: undefined, name: undefined, arguments: '{"path":' }
-			yield { type: "tool_call_partial", index: 0, id: undefined, name: undefined, arguments: '"test.txt"}' }
-		}
-
-		mockCreateStreamingMessage.mockReturnValue(mockGenerator())
-
-		const stream = handler.createMessage(systemPrompt, messages)
-		const results = []
-
-		for await (const chunk of stream) {
-			results.push(chunk)
-		}
-
-		expect(results).toHaveLength(3)
-		expect(results[0]).toEqual({
-			type: "tool_call_partial",
-			index: 0,
-			id: "tool_123",
-			name: "read_file",
-			arguments: undefined,
-		})
-		expect(results[1]).toEqual({
-			type: "tool_call_partial",
-			index: 0,
-			id: undefined,
-			name: undefined,
-			arguments: '{"path":',
-		})
-		expect(results[2]).toEqual({
-			type: "tool_call_partial",
-			index: 0,
-			id: undefined,
-			name: undefined,
-			arguments: '"test.txt"}',
-		})
-	})
-
-	test("should handle usage and cost tracking from streaming", async () => {
-		const systemPrompt = "You are a helpful assistant"
-		const messages = [{ role: "user" as const, content: "Hello" }]
-
-		mockGetAccessToken.mockResolvedValue("test-access-token")
-
-		// Mock async generator with text and usage
-		const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
-			yield { type: "text", text: "Hello there!" }
-			yield {
-				type: "usage",
-				inputTokens: 10,
-				outputTokens: 20,
-				cacheReadTokens: 5,
-				cacheWriteTokens: 3,
-			}
-		}
-
-		mockCreateStreamingMessage.mockReturnValue(mockGenerator())
-
-		const stream = handler.createMessage(systemPrompt, messages)
-		const results = []
-
-		for await (const chunk of stream) {
-			results.push(chunk)
-		}
-
-		// Should have text chunk and usage chunk
-		expect(results).toHaveLength(2)
-		expect(results[0]).toEqual({
-			type: "text",
-			text: "Hello there!",
-		})
-		// Claude Code is subscription-based, no per-token cost
-		expect(results[1]).toEqual({
-			type: "usage",
-			inputTokens: 10,
-			outputTokens: 20,
-			cacheReadTokens: 5,
-			cacheWriteTokens: 3,
-			totalCost: 0,
-		})
-	})
-
-	test("should handle usage without cache tokens", async () => {
-		const systemPrompt = "You are a helpful assistant"
-		const messages = [{ role: "user" as const, content: "Hello" }]
-
-		mockGetAccessToken.mockResolvedValue("test-access-token")
-
-		// Mock async generator with usage without cache tokens
-		const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
-			yield { type: "text", text: "Hello there!" }
-			yield {
-				type: "usage",
-				inputTokens: 10,
-				outputTokens: 20,
-			}
-		}
-
-		mockCreateStreamingMessage.mockReturnValue(mockGenerator())
-
-		const stream = handler.createMessage(systemPrompt, messages)
-		const results = []
-
-		for await (const chunk of stream) {
-			results.push(chunk)
-		}
-
-		// Claude Code is subscription-based, no per-token cost
-		expect(results).toHaveLength(2)
-		expect(results[1]).toEqual({
-			type: "usage",
-			inputTokens: 10,
-			outputTokens: 20,
-			cacheReadTokens: undefined,
-			cacheWriteTokens: undefined,
-			totalCost: 0,
-		})
-	})
-
-	test("should handle API errors from streaming", async () => {
-		const systemPrompt = "You are a helpful assistant"
-		const messages = [{ role: "user" as const, content: "Hello" }]
-
-		mockGetAccessToken.mockResolvedValue("test-access-token")
-
-		// Mock async generator that yields an error
-		const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
-			yield { type: "error", error: "Invalid model name" }
-		}
-
-		mockCreateStreamingMessage.mockReturnValue(mockGenerator())
-
-		const stream = handler.createMessage(systemPrompt, messages)
-		const iterator = stream[Symbol.asyncIterator]()
-
-		// Should throw an error
-		await expect(iterator.next()).rejects.toThrow("Invalid model name")
-	})
-
-	test("should handle authentication refresh and continue streaming", async () => {
-		const systemPrompt = "You are a helpful assistant"
-		const messages = [{ role: "user" as const, content: "Hello" }]
-
-		// First call returns a valid token
-		mockGetAccessToken.mockResolvedValue("refreshed-token")
-
-		const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
-			yield { type: "text", text: "Response after refresh" }
-		}
-
-		mockCreateStreamingMessage.mockReturnValue(mockGenerator())
-
-		const stream = handler.createMessage(systemPrompt, messages)
-		const results = []
-
-		for await (const chunk of stream) {
-			results.push(chunk)
-		}
-
-		expect(results).toHaveLength(1)
-		expect(results[0]).toEqual({
-			type: "text",
-			text: "Response after refresh",
-		})
-
-		expect(mockCreateStreamingMessage).toHaveBeenCalledWith(
-			expect.objectContaining({
-				accessToken: "refreshed-token",
-			}),
-		)
-	})
-
-	describe("completePrompt", () => {
-		test("should throw error when not authenticated", async () => {
-			mockGetAccessToken.mockResolvedValue(null)
-
-			await expect(handler.completePrompt("Test prompt")).rejects.toThrow(/not authenticated/i)
-		})
-
-		test("should complete prompt and return text response", async () => {
-			mockGetAccessToken.mockResolvedValue("test-access-token")
-			mockGetEmail.mockResolvedValue("[email protected]")
-
-			// Mock async generator that yields text chunks
-			const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
-				yield { type: "text", text: "Hello " }
-				yield { type: "text", text: "world!" }
-				yield { type: "usage", inputTokens: 10, outputTokens: 5 }
-			}
-
-			mockCreateStreamingMessage.mockReturnValue(mockGenerator())
-
-			const result = await handler.completePrompt("Say hello")
-
-			expect(result).toBe("Hello world!")
-		})
-
-		test("should call createStreamingMessage with empty system prompt and thinking disabled", async () => {
-			mockGetAccessToken.mockResolvedValue("test-access-token")
-			mockGetEmail.mockResolvedValue("[email protected]")
-
-			// Mock empty async generator
-			const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
-				yield { type: "text", text: "Response" }
-			}
-
-			mockCreateStreamingMessage.mockReturnValue(mockGenerator())
-
-			await handler.completePrompt("Test prompt")
-
-			// Verify createStreamingMessage was called with correct parameters
-			// System prompt is empty because the prompt text contains all context
-			// createStreamingMessage will still prepend the Claude Code branding
-			expect(mockCreateStreamingMessage).toHaveBeenCalledWith({
-				accessToken: "test-access-token",
-				model: "claude-sonnet-4-5",
-				systemPrompt: "", // Empty - branding is added by createStreamingMessage
-				messages: [{ role: "user", content: "Test prompt" }],
-				maxTokens: 32768,
-				thinking: { type: "disabled" }, // No thinking for simple completions
-				metadata: {
-					user_id: "user_abc123_account_def456_session_ghi789",
-				},
-			})
-		})
-
-		test("should handle API errors from streaming", async () => {
-			mockGetAccessToken.mockResolvedValue("test-access-token")
-			mockGetEmail.mockResolvedValue("[email protected]")
-
-			// Mock async generator that yields an error
-			const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
-				yield { type: "error", error: "API rate limit exceeded" }
-			}
-
-			mockCreateStreamingMessage.mockReturnValue(mockGenerator())
-
-			await expect(handler.completePrompt("Test prompt")).rejects.toThrow("API rate limit exceeded")
-		})
-
-		test("should return empty string when no text chunks received", async () => {
-			mockGetAccessToken.mockResolvedValue("test-access-token")
-			mockGetEmail.mockResolvedValue("[email protected]")
-
-			// Mock async generator that only yields usage
-			const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
-				yield { type: "usage", inputTokens: 10, outputTokens: 0 }
-			}
-
-			mockCreateStreamingMessage.mockReturnValue(mockGenerator())
-
-			const result = await handler.completePrompt("Test prompt")
-
-			expect(result).toBe("")
-		})
-
-		test("should use opus model maxTokens when configured", async () => {
-			const options: ApiHandlerOptions = {
-				apiModelId: "claude-opus-4-5",
-			}
-			const handlerOpus = new ClaudeCodeHandler(options)
-
-			mockGetAccessToken.mockResolvedValue("test-access-token")
-			mockGetEmail.mockResolvedValue("[email protected]")
-
-			const mockGenerator = async function* (): AsyncGenerator<StreamChunk> {
-				yield { type: "text", text: "Response" }
-			}
-
-			mockCreateStreamingMessage.mockReturnValue(mockGenerator())
-
-			await handlerOpus.completePrompt("Test prompt")
-
-			expect(mockCreateStreamingMessage).toHaveBeenCalledWith(
-				expect.objectContaining({
-					model: "claude-opus-4-5",
-					maxTokens: 32768, // opus model maxTokens
-				}),
-			)
-		})
-	})
-})

+ 0 - 378
src/api/providers/claude-code.ts

@@ -1,378 +0,0 @@
-import type { Anthropic } from "@anthropic-ai/sdk"
-import OpenAI from "openai"
-import {
-	claudeCodeDefaultModelId,
-	type ClaudeCodeModelId,
-	claudeCodeModels,
-	claudeCodeReasoningConfig,
-	type ClaudeCodeReasoningLevel,
-	type ModelInfo,
-} from "@roo-code/types"
-import { type ApiHandler, ApiHandlerCreateMessageMetadata, type SingleCompletionHandler } from ".."
-import { ApiStreamUsageChunk, type ApiStream } from "../transform/stream"
-import { claudeCodeOAuthManager, generateUserId } from "../../integrations/claude-code/oauth"
-import {
-	createStreamingMessage,
-	type StreamChunk,
-	type ThinkingConfig,
-} from "../../integrations/claude-code/streaming-client"
-import { t } from "../../i18n"
-import { ApiHandlerOptions } from "../../shared/api"
-import { countTokens } from "../../utils/countTokens"
-import { convertOpenAIToolsToAnthropic } from "../../core/prompts/tools/native-tools/converters"
-
-/**
- * Converts OpenAI tool_choice to Anthropic ToolChoice format
- * @param toolChoice - OpenAI tool_choice parameter
- * @param parallelToolCalls - When true, allows parallel tool calls. When false (default), disables parallel tool calls.
- */
-function convertOpenAIToolChoice(
-	toolChoice: OpenAI.Chat.ChatCompletionCreateParams["tool_choice"],
-	parallelToolCalls?: boolean,
-): Anthropic.Messages.MessageCreateParams["tool_choice"] | undefined {
-	// Anthropic allows parallel tool calls by default. When parallelToolCalls is false or undefined,
-	// we disable parallel tool use to ensure one tool call at a time.
-	const disableParallelToolUse = !parallelToolCalls
-
-	if (!toolChoice) {
-		// Default to auto with parallel tool use control
-		return { type: "auto", disable_parallel_tool_use: disableParallelToolUse }
-	}
-
-	if (typeof toolChoice === "string") {
-		switch (toolChoice) {
-			case "none":
-				return undefined // Anthropic doesn't have "none", just omit tools
-			case "auto":
-				return { type: "auto", disable_parallel_tool_use: disableParallelToolUse }
-			case "required":
-				return { type: "any", disable_parallel_tool_use: disableParallelToolUse }
-			default:
-				return { type: "auto", disable_parallel_tool_use: disableParallelToolUse }
-		}
-	}
-
-	// Handle object form { type: "function", function: { name: string } }
-	if (typeof toolChoice === "object" && "function" in toolChoice) {
-		return {
-			type: "tool",
-			name: toolChoice.function.name,
-			disable_parallel_tool_use: disableParallelToolUse,
-		}
-	}
-
-	return { type: "auto", disable_parallel_tool_use: disableParallelToolUse }
-}
-
-export class ClaudeCodeHandler implements ApiHandler, SingleCompletionHandler {
-	private options: ApiHandlerOptions
-	/**
-	 * Store the last thinking block signature for interleaved thinking with tool use.
-	 * This is captured from thinking_complete events during streaming and
-	 * must be passed back to the API when providing tool results.
-	 * Similar to Gemini's thoughtSignature pattern.
-	 */
-	private lastThinkingSignature?: string
-
-	constructor(options: ApiHandlerOptions) {
-		this.options = options
-	}
-
-	/**
-	 * Get the thinking signature from the last response.
-	 * Used by Task.addToApiConversationHistory to persist the signature
-	 * so it can be passed back to the API for tool use continuations.
-	 * This follows the same pattern as Gemini's getThoughtSignature().
-	 */
-	public getThoughtSignature(): string | undefined {
-		return this.lastThinkingSignature
-	}
-
-	/**
-	 * Gets the reasoning effort level for the current request.
-	 * Returns the effective reasoning level (low/medium/high) or null if disabled.
-	 */
-	private getReasoningEffort(modelInfo: ModelInfo): ClaudeCodeReasoningLevel | null {
-		// Check if reasoning is explicitly disabled
-		if (this.options.enableReasoningEffort === false) {
-			return null
-		}
-
-		// Get the selected effort from settings or model default
-		const selectedEffort = this.options.reasoningEffort ?? modelInfo.reasoningEffort
-
-		// "disable" or no selection means no reasoning
-		if (!selectedEffort || selectedEffort === "disable") {
-			return null
-		}
-
-		// Only allow valid levels for Claude Code
-		if (selectedEffort === "low" || selectedEffort === "medium" || selectedEffort === "high") {
-			return selectedEffort
-		}
-
-		return null
-	}
-
-	async *createMessage(
-		systemPrompt: string,
-		messages: Anthropic.Messages.MessageParam[],
-		metadata?: ApiHandlerCreateMessageMetadata,
-	): ApiStream {
-		// Reset per-request state that we persist into apiConversationHistory
-		this.lastThinkingSignature = undefined
-
-		const buildNotAuthenticatedError = () =>
-			new Error(
-				t("common:errors.claudeCode.notAuthenticated", {
-					defaultValue:
-						"Not authenticated with Claude Code. Please sign in using the Claude Code OAuth flow.",
-				}),
-			)
-
-		async function* streamOnce(this: ClaudeCodeHandler, accessToken: string): ApiStream {
-			// Get user email for generating user_id metadata
-			const email = await claudeCodeOAuthManager.getEmail()
-
-			const model = this.getModel()
-
-			// Validate that the model ID is a valid ClaudeCodeModelId
-			const modelId = Object.hasOwn(claudeCodeModels, model.id)
-				? (model.id as ClaudeCodeModelId)
-				: claudeCodeDefaultModelId
-
-			// Generate user_id metadata in the format required by Claude Code API
-			const userId = generateUserId(email || undefined)
-
-			const anthropicTools = convertOpenAIToolsToAnthropic(metadata?.tools ?? [])
-			const anthropicToolChoice = convertOpenAIToolChoice(metadata?.tool_choice, metadata?.parallelToolCalls)
-
-			// Determine reasoning effort and thinking configuration
-			const reasoningLevel = this.getReasoningEffort(model.info)
-
-			let thinking: ThinkingConfig
-			// With interleaved thinking (enabled via beta header), budget_tokens can exceed max_tokens
-			// as the token limit becomes the entire context window. We use the model's maxTokens.
-			// See: https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#interleaved-thinking
-			const maxTokens = model.info.maxTokens ?? 16384
-
-			if (reasoningLevel) {
-				// Use thinking mode with budget_tokens from config
-				const config = claudeCodeReasoningConfig[reasoningLevel]
-				thinking = {
-					type: "enabled",
-					budget_tokens: config.budgetTokens,
-				}
-			} else {
-				// Explicitly disable thinking
-				thinking = { type: "disabled" }
-			}
-
-			// Create streaming request using OAuth
-			const stream = createStreamingMessage({
-				accessToken,
-				model: modelId,
-				systemPrompt,
-				messages,
-				maxTokens,
-				thinking,
-				tools: anthropicTools,
-				toolChoice: anthropicToolChoice,
-				metadata: {
-					user_id: userId,
-				},
-			})
-
-			// Track usage for cost calculation
-			let inputTokens = 0
-			let outputTokens = 0
-			let cacheReadTokens = 0
-			let cacheWriteTokens = 0
-
-			for await (const chunk of stream) {
-				switch (chunk.type) {
-					case "text":
-						yield {
-							type: "text",
-							text: chunk.text,
-						}
-						break
-
-					case "reasoning":
-						yield {
-							type: "reasoning",
-							text: chunk.text,
-						}
-						break
-
-					case "thinking_complete":
-						// Capture the signature for persistence in api_conversation_history
-						// This enables tool use continuations where thinking blocks must be passed back
-						if (chunk.signature) {
-							this.lastThinkingSignature = chunk.signature
-						}
-						// Emit a complete thinking block with signature
-						// This is critical for interleaved thinking with tool use
-						// The signature must be included when passing thinking blocks back to the API
-						yield {
-							type: "reasoning",
-							text: chunk.thinking,
-							signature: chunk.signature,
-						}
-						break
-
-					case "tool_call_partial":
-						yield {
-							type: "tool_call_partial",
-							index: chunk.index,
-							id: chunk.id,
-							name: chunk.name,
-							arguments: chunk.arguments,
-						}
-						break
-
-					case "usage": {
-						inputTokens = chunk.inputTokens
-						outputTokens = chunk.outputTokens
-						cacheReadTokens = chunk.cacheReadTokens || 0
-						cacheWriteTokens = chunk.cacheWriteTokens || 0
-
-						// Claude Code is subscription-based, no per-token cost
-						const usageChunk: ApiStreamUsageChunk = {
-							type: "usage",
-							inputTokens,
-							outputTokens,
-							cacheReadTokens: cacheReadTokens > 0 ? cacheReadTokens : undefined,
-							cacheWriteTokens: cacheWriteTokens > 0 ? cacheWriteTokens : undefined,
-							totalCost: 0,
-						}
-
-						yield usageChunk
-						break
-					}
-
-					case "error":
-						throw new Error(chunk.error)
-				}
-			}
-		}
-
-		// Get access token from OAuth manager
-		let accessToken = await claudeCodeOAuthManager.getAccessToken()
-		if (!accessToken) {
-			throw buildNotAuthenticatedError()
-		}
-
-		// Try the request with at most one force-refresh retry on auth failure
-		for (let attempt = 0; attempt < 2; attempt++) {
-			try {
-				yield* streamOnce.call(this, accessToken)
-				return
-			} catch (error) {
-				const message = error instanceof Error ? error.message : String(error)
-				const isAuthFailure = /unauthorized|invalid token|not authenticated|authentication/i.test(message)
-
-				// Only retry on auth failure during first attempt
-				const canRetry = attempt === 0 && isAuthFailure
-				if (!canRetry) {
-					throw error
-				}
-
-				// Force refresh the token for retry
-				const refreshed = await claudeCodeOAuthManager.forceRefreshAccessToken()
-				if (!refreshed) {
-					throw buildNotAuthenticatedError()
-				}
-				accessToken = refreshed
-			}
-		}
-
-		// Unreachable: loop always returns on success or throws on failure
-		throw buildNotAuthenticatedError()
-	}
-
-	getModel(): { id: string; info: ModelInfo } {
-		const modelId = this.options.apiModelId
-		if (modelId && Object.hasOwn(claudeCodeModels, modelId)) {
-			const id = modelId as ClaudeCodeModelId
-			return { id, info: { ...claudeCodeModels[id] } }
-		}
-
-		return {
-			id: claudeCodeDefaultModelId,
-			info: { ...claudeCodeModels[claudeCodeDefaultModelId] },
-		}
-	}
-
-	async countTokens(content: Anthropic.Messages.ContentBlockParam[]): Promise<number> {
-		if (content.length === 0) {
-			return 0
-		}
-		return countTokens(content, { useWorker: true })
-	}
-
-	/**
-	 * Completes a prompt using the Claude Code API.
-	 * This is used for context condensing and prompt enhancement.
-	 * The Claude Code branding is automatically prepended by createStreamingMessage.
-	 */
-	async completePrompt(prompt: string): Promise<string> {
-		// Get access token from OAuth manager
-		const accessToken = await claudeCodeOAuthManager.getAccessToken()
-
-		if (!accessToken) {
-			throw new Error(
-				t("common:errors.claudeCode.notAuthenticated", {
-					defaultValue:
-						"Not authenticated with Claude Code. Please sign in using the Claude Code OAuth flow.",
-				}),
-			)
-		}
-
-		// Get user email for generating user_id metadata
-		const email = await claudeCodeOAuthManager.getEmail()
-
-		const model = this.getModel()
-
-		// Validate that the model ID is a valid ClaudeCodeModelId
-		const modelId = Object.hasOwn(claudeCodeModels, model.id)
-			? (model.id as ClaudeCodeModelId)
-			: claudeCodeDefaultModelId
-
-		// Generate user_id metadata in the format required by Claude Code API
-		const userId = generateUserId(email || undefined)
-
-		// Use maxTokens from model info for completion
-		const maxTokens = model.info.maxTokens ?? 16384
-
-		// Create streaming request using OAuth
-		// The system prompt is empty here since the prompt itself contains all context
-		// createStreamingMessage will still prepend the Claude Code branding
-		const stream = createStreamingMessage({
-			accessToken,
-			model: modelId,
-			systemPrompt: "", // Empty system prompt - the prompt text contains all necessary context
-			messages: [{ role: "user", content: prompt }],
-			maxTokens,
-			thinking: { type: "disabled" }, // No thinking for simple completions
-			metadata: {
-				user_id: userId,
-			},
-		})
-
-		// Collect all text chunks into a single response
-		let result = ""
-
-		for await (const chunk of stream) {
-			switch (chunk.type) {
-				case "text":
-					result += chunk.text
-					break
-				case "error":
-					throw new Error(chunk.error)
-			}
-		}
-
-		return result
-	}
-}

+ 0 - 1
src/api/providers/index.ts

@@ -3,7 +3,6 @@ export { AnthropicHandler } from "./anthropic"
 export { AwsBedrockHandler } from "./bedrock"
 export { CerebrasHandler } from "./cerebras"
 export { ChutesHandler } from "./chutes"
-export { ClaudeCodeHandler } from "./claude-code"
 export { DeepSeekHandler } from "./deepseek"
 export { DoubaoHandler } from "./doubao"
 export { MoonshotHandler } from "./moonshot"

+ 2 - 1
src/core/config/ProviderSettingsManager.ts

@@ -183,7 +183,8 @@ export class ProviderSettingsManager {
 				if (!providerProfiles.migrations.claudeCodeLegacySettingsMigrated) {
 					// These keys were used by the removed local Claude Code CLI wrapper.
 					for (const apiConfig of Object.values(providerProfiles.apiConfigs)) {
-						if (apiConfig.apiProvider !== "claude-code") continue
+						// Cast to string for comparison since "claude-code" is no longer a valid ProviderName
+						if ((apiConfig.apiProvider as string) !== "claude-code") continue
 
 						const config = apiConfig as unknown as Record<string, unknown>
 						if ("claudeCodePath" in config) {

+ 25 - 35
src/core/config/__tests__/importExport.spec.ts

@@ -68,15 +68,6 @@ vi.mock("../../../api", () => ({
 	buildApiHandler: vi.fn().mockImplementation((config) => {
 		// Return different model info based on the provider and model
 		const getModelInfo = () => {
-			if (config.apiProvider === "claude-code") {
-				return {
-					id: config.apiModelId || "claude-sonnet-4-5",
-					info: {
-						supportsReasoningBudget: false,
-						requiredReasoningBudget: false,
-					},
-				}
-			}
 			if (config.apiProvider === "anthropic" && config.apiModelId === "claude-3-5-sonnet-20241022") {
 				return {
 					id: "claude-3-5-sonnet-20241022",
@@ -484,18 +475,17 @@ describe("importExport", () => {
 
 		it("should handle import when reasoning budget fields are missing from config", async () => {
 			// This test verifies that import works correctly when reasoning budget fields are not present
-			// Using claude-code provider which doesn't support reasoning budgets
 
 			;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
 
 			const mockFileContent = JSON.stringify({
 				providerProfiles: {
-					currentApiConfigName: "claude-code-provider",
+					currentApiConfigName: "openai-provider",
 					apiConfigs: {
-						"claude-code-provider": {
-							apiProvider: "claude-code" as ProviderName,
-							apiModelId: "claude-3-5-sonnet-20241022",
-							id: "claude-code-id",
+						"openai-provider": {
+							apiProvider: "openai" as ProviderName,
+							apiModelId: "gpt-4",
+							id: "openai-id",
 							apiKey: "test-key",
 							// No modelMaxTokens or modelMaxThinkingTokens fields
 						},
@@ -513,7 +503,7 @@ describe("importExport", () => {
 
 			mockProviderSettingsManager.export.mockResolvedValue(previousProviderProfiles)
 			mockProviderSettingsManager.listConfig.mockResolvedValue([
-				{ name: "claude-code-provider", id: "claude-code-id", apiProvider: "claude-code" as ProviderName },
+				{ name: "openai-provider", id: "openai-id", apiProvider: "openai" as ProviderName },
 				{ name: "default", id: "default-id", apiProvider: "anthropic" as ProviderName },
 			])
 
@@ -530,21 +520,21 @@ describe("importExport", () => {
 			expect(mockProviderSettingsManager.export).toHaveBeenCalled()
 
 			expect(mockProviderSettingsManager.import).toHaveBeenCalledWith({
-				currentApiConfigName: "claude-code-provider",
+				currentApiConfigName: "openai-provider",
 				apiConfigs: {
 					default: { apiProvider: "anthropic" as ProviderName, id: "default-id" },
-					"claude-code-provider": {
-						apiProvider: "claude-code" as ProviderName,
-						apiModelId: "claude-3-5-sonnet-20241022",
+					"openai-provider": {
+						apiProvider: "openai" as ProviderName,
+						apiModelId: "gpt-4",
 						apiKey: "test-key",
-						id: "claude-code-id",
+						id: "openai-id",
 					},
 				},
 				modeApiConfigs: {},
 			})
 
 			expect(mockContextProxy.setValues).toHaveBeenCalledWith({ mode: "code", autoApprovalEnabled: true })
-			expect(mockContextProxy.setValue).toHaveBeenCalledWith("currentApiConfigName", "claude-code-provider")
+			expect(mockContextProxy.setValue).toHaveBeenCalledWith("currentApiConfigName", "openai-provider")
 		})
 	})
 
@@ -1722,27 +1712,27 @@ describe("importExport", () => {
 		it.each([
 			{
 				testCase: "supportsReasoningBudget is false",
-				providerName: "claude-code-provider",
-				modelId: "claude-sonnet-4-5",
-				providerId: "claude-code-id",
+				providerName: "deepseek-provider",
+				modelId: "deepseek-chat",
+				providerId: "deepseek-id",
 			},
 			{
 				testCase: "requiredReasoningBudget is false",
-				providerName: "claude-code-provider-2",
-				modelId: "claude-sonnet-4-5",
-				providerId: "claude-code-id-2",
+				providerName: "deepseek-provider-2",
+				modelId: "deepseek-coder",
+				providerId: "deepseek-id-2",
 			},
 			{
 				testCase: "both supportsReasoningBudget and requiredReasoningBudget are false",
-				providerName: "claude-code-provider-3",
-				modelId: "claude-3-5-haiku-20241022",
-				providerId: "claude-code-id-3",
+				providerName: "deepseek-provider-3",
+				modelId: "deepseek-reasoner",
+				providerId: "deepseek-id-3",
 			},
 		])(
 			"should exclude modelMaxTokens and modelMaxThinkingTokens when $testCase",
 			async ({ providerName, modelId, providerId }) => {
 				// This test verifies that token fields are excluded when model doesn't support reasoning budget
-				// Using claude-code provider which has supportsReasoningBudget: false and requiredReasoningBudget: false
+				// Using deepseek provider which uses apiModelId and has supportsReasoningBudget: false
 
 				;(vscode.window.showSaveDialog as Mock).mockResolvedValue({
 					fsPath: "/mock/path/roo-code-settings.json",
@@ -1754,12 +1744,12 @@ describe("importExport", () => {
 				// Wait for initialization to complete
 				await realProviderSettingsManager.initialize()
 
-				// Save a claude-code provider config with token fields
+				// Save a deepseek provider config with token fields
 				await realProviderSettingsManager.saveConfig(providerName, {
-					apiProvider: "claude-code" as ProviderName,
+					apiProvider: "deepseek" as ProviderName,
 					apiModelId: modelId,
 					id: providerId,
-					apiKey: "test-key",
+					deepSeekApiKey: "test-key",
 					modelMaxTokens: 4096, // This should be removed during export
 					modelMaxThinkingTokens: 2048, // This should be removed during export
 				})

+ 0 - 8
src/core/webview/ClineProvider.ts

@@ -2211,14 +2211,6 @@ export class ClineProvider
 			openRouterImageApiKey,
 			openRouterImageGenerationSelectedModel,
 			featureRoomoteControlEnabled,
-			claudeCodeIsAuthenticated: await (async () => {
-				try {
-					const { claudeCodeOAuthManager } = await import("../../integrations/claude-code/oauth")
-					return await claudeCodeOAuthManager.isAuthenticated()
-				} catch {
-					return false
-				}
-			})(),
 			openAiCodexIsAuthenticated: await (async () => {
 				try {
 					const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth")

+ 0 - 70
src/core/webview/webviewMessageHandler.ts

@@ -2385,45 +2385,6 @@ export const webviewMessageHandler = async (
 
 			break
 		}
-		case "claudeCodeSignIn": {
-			try {
-				const { claudeCodeOAuthManager } = await import("../../integrations/claude-code/oauth")
-				const authUrl = claudeCodeOAuthManager.startAuthorizationFlow()
-
-				// Open the authorization URL in the browser
-				await vscode.env.openExternal(vscode.Uri.parse(authUrl))
-
-				// Wait for the callback in a separate promise (non-blocking)
-				claudeCodeOAuthManager
-					.waitForCallback()
-					.then(async () => {
-						vscode.window.showInformationMessage("Successfully signed in to Claude Code")
-						await provider.postStateToWebview()
-					})
-					.catch((error) => {
-						provider.log(`Claude Code OAuth callback failed: ${error}`)
-						if (!String(error).includes("timed out")) {
-							vscode.window.showErrorMessage(`Claude Code sign in failed: ${error.message || error}`)
-						}
-					})
-			} catch (error) {
-				provider.log(`Claude Code OAuth failed: ${error}`)
-				vscode.window.showErrorMessage("Claude Code sign in failed.")
-			}
-			break
-		}
-		case "claudeCodeSignOut": {
-			try {
-				const { claudeCodeOAuthManager } = await import("../../integrations/claude-code/oauth")
-				await claudeCodeOAuthManager.clearCredentials()
-				vscode.window.showInformationMessage("Signed out from Claude Code")
-				await provider.postStateToWebview()
-			} catch (error) {
-				provider.log(`Claude Code sign out failed: ${error}`)
-				vscode.window.showErrorMessage("Claude Code sign out failed.")
-			}
-			break
-		}
 		case "openAiCodexSignIn": {
 			try {
 				const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth")
@@ -3267,37 +3228,6 @@ export const webviewMessageHandler = async (
 			break
 		}
 
-		case "requestClaudeCodeRateLimits": {
-			try {
-				const { claudeCodeOAuthManager } = await import("../../integrations/claude-code/oauth")
-				const accessToken = await claudeCodeOAuthManager.getAccessToken()
-
-				if (!accessToken) {
-					provider.postMessageToWebview({
-						type: "claudeCodeRateLimits",
-						error: "Not authenticated with Claude Code",
-					})
-					break
-				}
-
-				const { fetchRateLimitInfo } = await import("../../integrations/claude-code/streaming-client")
-				const rateLimits = await fetchRateLimitInfo(accessToken)
-
-				provider.postMessageToWebview({
-					type: "claudeCodeRateLimits",
-					values: rateLimits,
-				})
-			} catch (error) {
-				const errorMessage = error instanceof Error ? error.message : String(error)
-				provider.log(`Error fetching Claude Code rate limits: ${errorMessage}`)
-				provider.postMessageToWebview({
-					type: "claudeCodeRateLimits",
-					error: errorMessage,
-				})
-			}
-			break
-		}
-
 		case "requestOpenAiCodexRateLimits": {
 			try {
 				const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth")

+ 0 - 4
src/extension.ts

@@ -27,7 +27,6 @@ import { ContextProxy } from "./core/config/ContextProxy"
 import { ClineProvider } from "./core/webview/ClineProvider"
 import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
 import { TerminalRegistry } from "./integrations/terminal/TerminalRegistry"
-import { claudeCodeOAuthManager } from "./integrations/claude-code/oauth"
 import { openAiCodexOAuthManager } from "./integrations/openai-codex/oauth"
 import { McpServerManager } from "./services/mcp/McpServerManager"
 import { CodeIndexManager } from "./services/code-index/manager"
@@ -151,9 +150,6 @@ export async function activate(context: vscode.ExtensionContext) {
 	// Initialize terminal shell execution handlers.
 	TerminalRegistry.initialize()
 
-	// Initialize Claude Code OAuth manager for direct API access.
-	claudeCodeOAuthManager.initialize(context, (message) => outputChannel.appendLine(message))
-
 	// Initialize OpenAI Codex OAuth manager for ChatGPT subscription-based access.
 	openAiCodexOAuthManager.initialize(context, (message) => outputChannel.appendLine(message))
 

+ 0 - 235
src/integrations/claude-code/__tests__/oauth.spec.ts

@@ -1,235 +0,0 @@
-import {
-	generateCodeVerifier,
-	generateCodeChallenge,
-	generateState,
-	generateUserId,
-	buildAuthorizationUrl,
-	isTokenExpired,
-	CLAUDE_CODE_OAUTH_CONFIG,
-	type ClaudeCodeCredentials,
-} from "../oauth"
-
-describe("Claude Code OAuth", () => {
-	describe("generateCodeVerifier", () => {
-		test("should generate a base64url encoded verifier", () => {
-			const verifier = generateCodeVerifier()
-			// Base64url encoded 32 bytes = 43 characters
-			expect(verifier).toHaveLength(43)
-			// Should only contain base64url safe characters
-			expect(verifier).toMatch(/^[A-Za-z0-9_-]+$/)
-		})
-
-		test("should generate unique verifiers on each call", () => {
-			const verifier1 = generateCodeVerifier()
-			const verifier2 = generateCodeVerifier()
-			expect(verifier1).not.toBe(verifier2)
-		})
-	})
-
-	describe("generateCodeChallenge", () => {
-		test("should generate a base64url encoded SHA256 hash", () => {
-			const verifier = "test-verifier-string"
-			const challenge = generateCodeChallenge(verifier)
-			// Base64url encoded SHA256 hash = 43 characters
-			expect(challenge).toHaveLength(43)
-			// Should only contain base64url safe characters
-			expect(challenge).toMatch(/^[A-Za-z0-9_-]+$/)
-		})
-
-		test("should generate consistent challenge for same verifier", () => {
-			const verifier = "test-verifier-string"
-			const challenge1 = generateCodeChallenge(verifier)
-			const challenge2 = generateCodeChallenge(verifier)
-			expect(challenge1).toBe(challenge2)
-		})
-
-		test("should generate different challenges for different verifiers", () => {
-			const challenge1 = generateCodeChallenge("verifier1")
-			const challenge2 = generateCodeChallenge("verifier2")
-			expect(challenge1).not.toBe(challenge2)
-		})
-	})
-
-	describe("generateState", () => {
-		test("should generate a 32-character hex string", () => {
-			const state = generateState()
-			expect(state).toHaveLength(32) // 16 bytes = 32 hex chars
-			expect(state).toMatch(/^[0-9a-f]+$/)
-		})
-
-		test("should generate unique states on each call", () => {
-			const state1 = generateState()
-			const state2 = generateState()
-			expect(state1).not.toBe(state2)
-		})
-	})
-
-	describe("generateUserId", () => {
-		test("should generate user ID with correct format", () => {
-			const userId = generateUserId()
-			// Format: user_<16 hex>_account_<32 hex>_session_<32 hex>
-			expect(userId).toMatch(/^user_[0-9a-f]{16}_account_[0-9a-f]{32}_session_[0-9a-f]{32}$/)
-		})
-
-		test("should generate unique session IDs on each call", () => {
-			const userId1 = generateUserId()
-			const userId2 = generateUserId()
-			// Full IDs should be different due to random session UUID
-			expect(userId1).not.toBe(userId2)
-		})
-
-		test("should generate deterministic user hash and account UUID from email", () => {
-			const email = "[email protected]"
-			const userId1 = generateUserId(email)
-			const userId2 = generateUserId(email)
-
-			// Extract user and account parts (everything except session)
-			const userAccount1 = userId1.replace(/_session_[0-9a-f]{32}$/, "")
-			const userAccount2 = userId2.replace(/_session_[0-9a-f]{32}$/, "")
-
-			// User hash and account UUID should be deterministic for same email
-			expect(userAccount1).toBe(userAccount2)
-
-			// But session UUID should be different
-			const session1 = userId1.match(/_session_([0-9a-f]{32})$/)?.[1]
-			const session2 = userId2.match(/_session_([0-9a-f]{32})$/)?.[1]
-			expect(session1).not.toBe(session2)
-		})
-
-		test("should generate different user hash for different emails", () => {
-			const userId1 = generateUserId("[email protected]")
-			const userId2 = generateUserId("[email protected]")
-
-			const userHash1 = userId1.match(/^user_([0-9a-f]{16})_/)?.[1]
-			const userHash2 = userId2.match(/^user_([0-9a-f]{16})_/)?.[1]
-
-			expect(userHash1).not.toBe(userHash2)
-		})
-
-		test("should generate random user hash and account UUID without email", () => {
-			const userId1 = generateUserId()
-			const userId2 = generateUserId()
-
-			// Without email, even user hash should be different each call
-			const userHash1 = userId1.match(/^user_([0-9a-f]{16})_/)?.[1]
-			const userHash2 = userId2.match(/^user_([0-9a-f]{16})_/)?.[1]
-
-			// Extremely unlikely to be the same (random 8 bytes)
-			expect(userHash1).not.toBe(userHash2)
-		})
-	})
-
-	describe("buildAuthorizationUrl", () => {
-		test("should build correct authorization URL with all parameters", () => {
-			const codeChallenge = "test-code-challenge"
-			const state = "test-state"
-			const url = buildAuthorizationUrl(codeChallenge, state)
-
-			const parsedUrl = new URL(url)
-			expect(parsedUrl.origin + parsedUrl.pathname).toBe(CLAUDE_CODE_OAUTH_CONFIG.authorizationEndpoint)
-
-			const params = parsedUrl.searchParams
-			expect(params.get("client_id")).toBe(CLAUDE_CODE_OAUTH_CONFIG.clientId)
-			expect(params.get("redirect_uri")).toBe(CLAUDE_CODE_OAUTH_CONFIG.redirectUri)
-			expect(params.get("scope")).toBe(CLAUDE_CODE_OAUTH_CONFIG.scopes)
-			expect(params.get("code_challenge")).toBe(codeChallenge)
-			expect(params.get("code_challenge_method")).toBe("S256")
-			expect(params.get("response_type")).toBe("code")
-			expect(params.get("state")).toBe(state)
-		})
-	})
-
-	describe("isTokenExpired", () => {
-		test("should return false for non-expired token", () => {
-			const futureDate = new Date(Date.now() + 60 * 60 * 1000) // 1 hour in future
-			const credentials: ClaudeCodeCredentials = {
-				type: "claude",
-				access_token: "test-token",
-				refresh_token: "test-refresh",
-				expired: futureDate.toISOString(),
-			}
-			expect(isTokenExpired(credentials)).toBe(false)
-		})
-
-		test("should return true for expired token", () => {
-			const pastDate = new Date(Date.now() - 60 * 60 * 1000) // 1 hour in past
-			const credentials: ClaudeCodeCredentials = {
-				type: "claude",
-				access_token: "test-token",
-				refresh_token: "test-refresh",
-				expired: pastDate.toISOString(),
-			}
-			expect(isTokenExpired(credentials)).toBe(true)
-		})
-
-		test("should return true for token expiring within 5 minute buffer", () => {
-			const almostExpired = new Date(Date.now() + 3 * 60 * 1000) // 3 minutes in future (within 5 min buffer)
-			const credentials: ClaudeCodeCredentials = {
-				type: "claude",
-				access_token: "test-token",
-				refresh_token: "test-refresh",
-				expired: almostExpired.toISOString(),
-			}
-			expect(isTokenExpired(credentials)).toBe(true)
-		})
-
-		test("should return false for token expiring after 5 minute buffer", () => {
-			const notYetExpiring = new Date(Date.now() + 10 * 60 * 1000) // 10 minutes in future
-			const credentials: ClaudeCodeCredentials = {
-				type: "claude",
-				access_token: "test-token",
-				refresh_token: "test-refresh",
-				expired: notYetExpiring.toISOString(),
-			}
-			expect(isTokenExpired(credentials)).toBe(false)
-		})
-	})
-
-	describe("CLAUDE_CODE_OAUTH_CONFIG", () => {
-		test("should have correct configuration values", () => {
-			expect(CLAUDE_CODE_OAUTH_CONFIG.authorizationEndpoint).toBe("https://claude.ai/oauth/authorize")
-			expect(CLAUDE_CODE_OAUTH_CONFIG.tokenEndpoint).toBe("https://console.anthropic.com/v1/oauth/token")
-			expect(CLAUDE_CODE_OAUTH_CONFIG.clientId).toBe("9d1c250a-e61b-44d9-88ed-5944d1962f5e")
-			expect(CLAUDE_CODE_OAUTH_CONFIG.redirectUri).toBe("http://localhost:54545/callback")
-			expect(CLAUDE_CODE_OAUTH_CONFIG.scopes).toBe("org:create_api_key user:profile user:inference")
-			expect(CLAUDE_CODE_OAUTH_CONFIG.callbackPort).toBe(54545)
-		})
-	})
-
-	describe("refresh token behavior", () => {
-		afterEach(() => {
-			vi.unstubAllGlobals()
-		})
-
-		test("refresh responses may omit refresh_token (should be tolerated)", async () => {
-			const { refreshAccessToken } = await import("../oauth")
-
-			// Mock fetch to return a refresh response with no refresh_token
-			const mockFetch = vi.fn().mockResolvedValue(
-				new Response(
-					JSON.stringify({
-						access_token: "new-access",
-						expires_in: 3600,
-						// refresh_token intentionally omitted
-					}),
-					{ status: 200, headers: { "Content-Type": "application/json" } },
-				),
-			)
-
-			vi.stubGlobal("fetch", mockFetch)
-
-			const creds: ClaudeCodeCredentials = {
-				type: "claude" as const,
-				access_token: "old-access",
-				refresh_token: "old-refresh",
-				expired: new Date(Date.now() - 1000).toISOString(),
-				email: "[email protected]",
-			}
-
-			const refreshed = await refreshAccessToken(creds)
-			expect(refreshed.access_token).toBe("new-access")
-			expect(refreshed.refresh_token).toBe("old-refresh")
-			expect(refreshed.email).toBe("[email protected]")
-		})
-	})
-})

+ 0 - 585
src/integrations/claude-code/__tests__/streaming-client.spec.ts

@@ -1,585 +0,0 @@
-import { CLAUDE_CODE_API_CONFIG } from "../streaming-client"
-
-describe("Claude Code Streaming Client", () => {
-	describe("CLAUDE_CODE_API_CONFIG", () => {
-		test("should have correct API endpoint", () => {
-			expect(CLAUDE_CODE_API_CONFIG.endpoint).toBe("https://api.anthropic.com/v1/messages")
-		})
-
-		test("should have correct API version", () => {
-			expect(CLAUDE_CODE_API_CONFIG.version).toBe("2023-06-01")
-		})
-
-		test("should have correct default betas", () => {
-			expect(CLAUDE_CODE_API_CONFIG.defaultBetas).toContain("claude-code-20250219")
-			expect(CLAUDE_CODE_API_CONFIG.defaultBetas).toContain("oauth-2025-04-20")
-			expect(CLAUDE_CODE_API_CONFIG.defaultBetas).toContain("interleaved-thinking-2025-05-14")
-			expect(CLAUDE_CODE_API_CONFIG.defaultBetas).toContain("fine-grained-tool-streaming-2025-05-14")
-		})
-
-		test("should have correct user agent", () => {
-			expect(CLAUDE_CODE_API_CONFIG.userAgent).toMatch(/^Roo-Code\/\d+\.\d+\.\d+$/)
-		})
-	})
-
-	describe("createStreamingMessage", () => {
-		let originalFetch: typeof global.fetch
-
-		beforeEach(() => {
-			originalFetch = global.fetch
-		})
-
-		afterEach(() => {
-			global.fetch = originalFetch
-		})
-
-		test("should make request with correct headers", async () => {
-			const mockFetch = vi.fn().mockResolvedValue({
-				ok: true,
-				body: {
-					getReader: () => ({
-						read: vi.fn().mockResolvedValue({ done: true, value: undefined }),
-						releaseLock: vi.fn(),
-					}),
-				},
-			})
-			global.fetch = mockFetch
-
-			const { createStreamingMessage } = await import("../streaming-client")
-
-			const stream = createStreamingMessage({
-				accessToken: "test-token",
-				model: "claude-3-5-sonnet-20241022",
-				systemPrompt: "You are helpful",
-				messages: [{ role: "user", content: "Hello" }],
-			})
-
-			// Consume the stream
-			for await (const _ of stream) {
-				// Just consume
-			}
-
-			expect(mockFetch).toHaveBeenCalledWith(
-				expect.stringContaining(CLAUDE_CODE_API_CONFIG.endpoint),
-				expect.objectContaining({
-					method: "POST",
-					headers: expect.objectContaining({
-						Authorization: "Bearer test-token",
-						"Content-Type": "application/json",
-						"Anthropic-Version": CLAUDE_CODE_API_CONFIG.version,
-						Accept: "text/event-stream",
-						"User-Agent": CLAUDE_CODE_API_CONFIG.userAgent,
-					}),
-				}),
-			)
-		})
-
-		test("should include correct body parameters", async () => {
-			const mockFetch = vi.fn().mockResolvedValue({
-				ok: true,
-				body: {
-					getReader: () => ({
-						read: vi.fn().mockResolvedValue({ done: true, value: undefined }),
-						releaseLock: vi.fn(),
-					}),
-				},
-			})
-			global.fetch = mockFetch
-
-			const { createStreamingMessage } = await import("../streaming-client")
-
-			const stream = createStreamingMessage({
-				accessToken: "test-token",
-				model: "claude-3-5-sonnet-20241022",
-				systemPrompt: "You are helpful",
-				messages: [{ role: "user", content: "Hello" }],
-				maxTokens: 4096,
-			})
-
-			// Consume the stream
-			for await (const _ of stream) {
-				// Just consume
-			}
-
-			const call = mockFetch.mock.calls[0]
-			const body = JSON.parse(call[1].body)
-
-			expect(body.model).toBe("claude-3-5-sonnet-20241022")
-			expect(body.stream).toBe(true)
-			expect(body.max_tokens).toBe(4096)
-			// System prompt should have cache_control on the user-provided text
-			expect(body.system).toEqual([
-				{ type: "text", text: "You are Claude Code, Anthropic's official CLI for Claude." },
-				{ type: "text", text: "You are helpful", cache_control: { type: "ephemeral" } },
-			])
-			// Messages should have cache_control on the last user message
-			expect(body.messages).toEqual([
-				{
-					role: "user",
-					content: [{ type: "text", text: "Hello", cache_control: { type: "ephemeral" } }],
-				},
-			])
-		})
-
-		test("should add cache breakpoints to last two user messages", async () => {
-			const mockFetch = vi.fn().mockResolvedValue({
-				ok: true,
-				body: {
-					getReader: () => ({
-						read: vi.fn().mockResolvedValue({ done: true, value: undefined }),
-						releaseLock: vi.fn(),
-					}),
-				},
-			})
-			global.fetch = mockFetch
-
-			const { createStreamingMessage } = await import("../streaming-client")
-
-			const stream = createStreamingMessage({
-				accessToken: "test-token",
-				model: "claude-3-5-sonnet-20241022",
-				systemPrompt: "You are helpful",
-				messages: [
-					{ role: "user", content: "First message" },
-					{ role: "assistant", content: "Response" },
-					{ role: "user", content: "Second message" },
-					{ role: "assistant", content: "Another response" },
-					{ role: "user", content: "Third message" },
-				],
-			})
-
-			// Consume the stream
-			for await (const _ of stream) {
-				// Just consume
-			}
-
-			const call = mockFetch.mock.calls[0]
-			const body = JSON.parse(call[1].body)
-
-			// Only the last two user messages should have cache_control
-			expect(body.messages[0].content).toBe("First message") // No cache_control
-			expect(body.messages[2].content).toEqual([
-				{ type: "text", text: "Second message", cache_control: { type: "ephemeral" } },
-			])
-			expect(body.messages[4].content).toEqual([
-				{ type: "text", text: "Third message", cache_control: { type: "ephemeral" } },
-			])
-		})
-
-		test("should filter out non-Anthropic block types", async () => {
-			const mockFetch = vi.fn().mockResolvedValue({
-				ok: true,
-				body: {
-					getReader: () => ({
-						read: vi.fn().mockResolvedValue({ done: true, value: undefined }),
-						releaseLock: vi.fn(),
-					}),
-				},
-			})
-			global.fetch = mockFetch
-
-			const { createStreamingMessage } = await import("../streaming-client")
-
-			const stream = createStreamingMessage({
-				accessToken: "test-token",
-				model: "claude-3-5-sonnet-20241022",
-				systemPrompt: "You are helpful",
-				messages: [
-					{
-						role: "user",
-						content: [{ type: "text", text: "Hello" }],
-					},
-					{
-						role: "assistant",
-						content: [
-							{ type: "reasoning", text: "Internal reasoning" }, // Should be filtered
-							{ type: "thoughtSignature", data: "encrypted" }, // Should be filtered
-							{ type: "text", text: "Response" },
-						],
-					},
-					{
-						role: "user",
-						content: [{ type: "text", text: "Follow up" }],
-					},
-				] as any,
-			})
-
-			// Consume the stream
-			for await (const _ of stream) {
-				// Just consume
-			}
-
-			const call = mockFetch.mock.calls[0]
-			const body = JSON.parse(call[1].body)
-
-			// The assistant message should only have the text block
-			expect(body.messages[1].content).toEqual([{ type: "text", text: "Response" }])
-		})
-
-		test("should preserve thinking and redacted_thinking blocks", async () => {
-			const mockFetch = vi.fn().mockResolvedValue({
-				ok: true,
-				body: {
-					getReader: () => ({
-						read: vi.fn().mockResolvedValue({ done: true, value: undefined }),
-						releaseLock: vi.fn(),
-					}),
-				},
-			})
-			global.fetch = mockFetch
-
-			const { createStreamingMessage } = await import("../streaming-client")
-
-			const stream = createStreamingMessage({
-				accessToken: "test-token",
-				model: "claude-3-5-sonnet-20241022",
-				systemPrompt: "You are helpful",
-				messages: [
-					{
-						role: "user",
-						content: [{ type: "text", text: "Hello" }],
-					},
-					{
-						role: "assistant",
-						content: [
-							{ type: "thinking", thinking: "Let me think...", signature: "abc123" },
-							{ type: "text", text: "Response" },
-						],
-					},
-					{
-						role: "user",
-						content: [{ type: "tool_result", tool_use_id: "123", content: "result" }],
-					},
-				] as any,
-			})
-
-			// Consume the stream
-			for await (const _ of stream) {
-				// Just consume
-			}
-
-			const call = mockFetch.mock.calls[0]
-			const body = JSON.parse(call[1].body)
-
-			// Thinking blocks should be preserved
-			expect(body.messages[1].content).toContainEqual({
-				type: "thinking",
-				thinking: "Let me think...",
-				signature: "abc123",
-			})
-			// Tool result blocks should be preserved
-			expect(body.messages[2].content).toContainEqual({
-				type: "tool_result",
-				tool_use_id: "123",
-				content: "result",
-			})
-		})
-
-		// Dropped: conversion of internal `reasoning` + `thoughtSignature` blocks into
-		// Anthropic `thinking` blocks. The Claude Code integration now relies on the
-		// Anthropic-native `thinking` block format persisted by Task.
-
-		test("should strip reasoning_details from messages (provider switching)", async () => {
-			// When switching from OpenRouter/Roo to Claude Code, messages may have
-			// reasoning_details fields that the Anthropic API doesn't accept
-			// This causes errors like: "messages.3.reasoning_details: Extra inputs are not permitted"
-			const mockFetch = vi.fn().mockResolvedValue({
-				ok: true,
-				body: {
-					getReader: () => ({
-						read: vi.fn().mockResolvedValue({ done: true, value: undefined }),
-						releaseLock: vi.fn(),
-					}),
-				},
-			})
-			global.fetch = mockFetch
-
-			const { createStreamingMessage } = await import("../streaming-client")
-
-			// Simulate messages with reasoning_details (added by OpenRouter for Gemini/o-series)
-			const messagesWithReasoningDetails = [
-				{ role: "user", content: "Hello" },
-				{
-					role: "assistant",
-					content: [{ type: "text", text: "I'll help with that." }],
-					// This field is added by OpenRouter/Roo providers for Gemini/OpenAI reasoning
-					reasoning_details: [{ type: "summary_text", summary: "Thinking about the request" }],
-				},
-				{ role: "user", content: "Follow up question" },
-			]
-
-			const stream = createStreamingMessage({
-				accessToken: "test-token",
-				model: "claude-3-5-sonnet-20241022",
-				systemPrompt: "You are helpful",
-				messages: messagesWithReasoningDetails as any,
-			})
-
-			// Consume the stream
-			for await (const _ of stream) {
-				// Just consume
-			}
-
-			const call = mockFetch.mock.calls[0]
-			const body = JSON.parse(call[1].body)
-
-			// The assistant message should NOT have reasoning_details
-			expect(body.messages[1]).not.toHaveProperty("reasoning_details")
-			// But should still have the content
-			expect(body.messages[1].content).toContainEqual(
-				expect.objectContaining({
-					type: "text",
-					text: "I'll help with that.",
-				}),
-			)
-			// Only role and content should be present
-			expect(Object.keys(body.messages[1])).toEqual(["role", "content"])
-		})
-
-		test("should strip other non-standard message fields", async () => {
-			// Ensure any non-standard fields are stripped from messages
-			const mockFetch = vi.fn().mockResolvedValue({
-				ok: true,
-				body: {
-					getReader: () => ({
-						read: vi.fn().mockResolvedValue({ done: true, value: undefined }),
-						releaseLock: vi.fn(),
-					}),
-				},
-			})
-			global.fetch = mockFetch
-
-			const { createStreamingMessage } = await import("../streaming-client")
-
-			const messagesWithExtraFields = [
-				{
-					role: "user",
-					content: "Hello",
-					customField: "should be stripped",
-					metadata: { foo: "bar" },
-				},
-				{
-					role: "assistant",
-					content: [{ type: "text", text: "Response" }],
-					internalId: "123",
-					timestamp: Date.now(),
-				},
-			]
-
-			const stream = createStreamingMessage({
-				accessToken: "test-token",
-				model: "claude-3-5-sonnet-20241022",
-				systemPrompt: "You are helpful",
-				messages: messagesWithExtraFields as any,
-			})
-
-			// Consume the stream
-			for await (const _ of stream) {
-				// Just consume
-			}
-
-			const call = mockFetch.mock.calls[0]
-			const body = JSON.parse(call[1].body)
-
-			// All messages should only have role and content
-			body.messages.forEach((msg: Record<string, unknown>) => {
-				expect(Object.keys(msg).filter((k) => k !== "role" && k !== "content")).toHaveLength(0)
-			})
-		})
-
-		test("should yield error chunk on non-ok response", async () => {
-			const mockFetch = vi.fn().mockResolvedValue({
-				ok: false,
-				status: 401,
-				statusText: "Unauthorized",
-				text: vi.fn().mockResolvedValue('{"error":{"message":"Invalid API key"}}'),
-			})
-			global.fetch = mockFetch
-
-			const { createStreamingMessage } = await import("../streaming-client")
-
-			const stream = createStreamingMessage({
-				accessToken: "invalid-token",
-				model: "claude-3-5-sonnet-20241022",
-				systemPrompt: "You are helpful",
-				messages: [{ role: "user", content: "Hello" }],
-			})
-
-			const chunks = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			expect(chunks).toHaveLength(1)
-			expect(chunks[0].type).toBe("error")
-			expect((chunks[0] as { type: "error"; error: string }).error).toBe("Invalid API key")
-		})
-
-		test("should yield error chunk when no response body", async () => {
-			const mockFetch = vi.fn().mockResolvedValue({
-				ok: true,
-				body: null,
-			})
-			global.fetch = mockFetch
-
-			const { createStreamingMessage } = await import("../streaming-client")
-
-			const stream = createStreamingMessage({
-				accessToken: "test-token",
-				model: "claude-3-5-sonnet-20241022",
-				systemPrompt: "You are helpful",
-				messages: [{ role: "user", content: "Hello" }],
-			})
-
-			const chunks = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			expect(chunks).toHaveLength(1)
-			expect(chunks[0].type).toBe("error")
-			expect((chunks[0] as { type: "error"; error: string }).error).toBe("No response body")
-		})
-
-		test("should parse text SSE events correctly", async () => {
-			const sseData = [
-				'event: content_block_start\ndata: {"index":0,"content_block":{"type":"text","text":"Hello"}}\n\n',
-				'event: content_block_delta\ndata: {"index":0,"delta":{"type":"text_delta","text":" world"}}\n\n',
-				"event: message_stop\ndata: {}\n\n",
-			]
-
-			let readIndex = 0
-			const mockFetch = vi.fn().mockResolvedValue({
-				ok: true,
-				body: {
-					getReader: () => ({
-						read: vi.fn().mockImplementation(() => {
-							if (readIndex < sseData.length) {
-								const value = new TextEncoder().encode(sseData[readIndex++])
-								return Promise.resolve({ done: false, value })
-							}
-							return Promise.resolve({ done: true, value: undefined })
-						}),
-						releaseLock: vi.fn(),
-					}),
-				},
-			})
-			global.fetch = mockFetch
-
-			const { createStreamingMessage } = await import("../streaming-client")
-
-			const stream = createStreamingMessage({
-				accessToken: "test-token",
-				model: "claude-3-5-sonnet-20241022",
-				systemPrompt: "You are helpful",
-				messages: [{ role: "user", content: "Hello" }],
-			})
-
-			const chunks = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			// Should have text chunks and usage
-			expect(chunks.some((c) => c.type === "text")).toBe(true)
-			expect(chunks.filter((c) => c.type === "text")).toEqual([
-				{ type: "text", text: "Hello" },
-				{ type: "text", text: " world" },
-			])
-		})
-
-		test("should parse thinking/reasoning SSE events correctly", async () => {
-			const sseData = [
-				'event: content_block_start\ndata: {"index":0,"content_block":{"type":"thinking","thinking":"Let me think..."}}\n\n',
-				'event: content_block_delta\ndata: {"index":0,"delta":{"type":"thinking_delta","thinking":" more thoughts"}}\n\n',
-				"event: message_stop\ndata: {}\n\n",
-			]
-
-			let readIndex = 0
-			const mockFetch = vi.fn().mockResolvedValue({
-				ok: true,
-				body: {
-					getReader: () => ({
-						read: vi.fn().mockImplementation(() => {
-							if (readIndex < sseData.length) {
-								const value = new TextEncoder().encode(sseData[readIndex++])
-								return Promise.resolve({ done: false, value })
-							}
-							return Promise.resolve({ done: true, value: undefined })
-						}),
-						releaseLock: vi.fn(),
-					}),
-				},
-			})
-			global.fetch = mockFetch
-
-			const { createStreamingMessage } = await import("../streaming-client")
-
-			const stream = createStreamingMessage({
-				accessToken: "test-token",
-				model: "claude-3-5-sonnet-20241022",
-				systemPrompt: "You are helpful",
-				messages: [{ role: "user", content: "Hello" }],
-			})
-
-			const chunks = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			expect(chunks.filter((c) => c.type === "reasoning")).toEqual([
-				{ type: "reasoning", text: "Let me think..." },
-				{ type: "reasoning", text: " more thoughts" },
-			])
-		})
-
-		test("should track and yield usage from message events", async () => {
-			const sseData = [
-				'event: message_start\ndata: {"message":{"usage":{"input_tokens":10,"output_tokens":0,"cache_read_input_tokens":5}}}\n\n',
-				'event: message_delta\ndata: {"usage":{"output_tokens":20}}\n\n',
-				"event: message_stop\ndata: {}\n\n",
-			]
-
-			let readIndex = 0
-			const mockFetch = vi.fn().mockResolvedValue({
-				ok: true,
-				body: {
-					getReader: () => ({
-						read: vi.fn().mockImplementation(() => {
-							if (readIndex < sseData.length) {
-								const value = new TextEncoder().encode(sseData[readIndex++])
-								return Promise.resolve({ done: false, value })
-							}
-							return Promise.resolve({ done: true, value: undefined })
-						}),
-						releaseLock: vi.fn(),
-					}),
-				},
-			})
-			global.fetch = mockFetch
-
-			const { createStreamingMessage } = await import("../streaming-client")
-
-			const stream = createStreamingMessage({
-				accessToken: "test-token",
-				model: "claude-3-5-sonnet-20241022",
-				systemPrompt: "You are helpful",
-				messages: [{ role: "user", content: "Hello" }],
-			})
-
-			const chunks = []
-			for await (const chunk of stream) {
-				chunks.push(chunk)
-			}
-
-			const usageChunk = chunks.find((c) => c.type === "usage")
-			expect(usageChunk).toBeDefined()
-			expect(usageChunk).toMatchObject({
-				type: "usage",
-				inputTokens: 10,
-				outputTokens: 20,
-				cacheReadTokens: 5,
-			})
-		})
-	})
-})

+ 0 - 638
src/integrations/claude-code/oauth.ts

@@ -1,638 +0,0 @@
-import * as crypto from "crypto"
-import * as http from "http"
-import { URL } from "url"
-import type { ExtensionContext } from "vscode"
-import { z } from "zod"
-
-// OAuth Configuration
-export const CLAUDE_CODE_OAUTH_CONFIG = {
-	authorizationEndpoint: "https://claude.ai/oauth/authorize",
-	tokenEndpoint: "https://console.anthropic.com/v1/oauth/token",
-	clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
-	redirectUri: "http://localhost:54545/callback",
-	scopes: "org:create_api_key user:profile user:inference",
-	callbackPort: 54545,
-} as const
-
-// Token storage key
-const CLAUDE_CODE_CREDENTIALS_KEY = "claude-code-oauth-credentials"
-
-// Credentials schema
-const claudeCodeCredentialsSchema = z.object({
-	type: z.literal("claude"),
-	access_token: z.string().min(1),
-	refresh_token: z.string().min(1),
-	expired: z.string(), // RFC3339 datetime
-	email: z.string().optional(),
-})
-
-export type ClaudeCodeCredentials = z.infer<typeof claudeCodeCredentialsSchema>
-
-// Token response schema from Anthropic
-const tokenResponseSchema = z.object({
-	access_token: z.string(),
-	// Refresh responses may omit refresh_token (common OAuth behavior). When omitted,
-	// callers must preserve the existing refresh token.
-	refresh_token: z.string().min(1).optional(),
-	expires_in: z.number(),
-	email: z.string().optional(),
-	token_type: z.string().optional(),
-})
-
-class ClaudeCodeOAuthTokenError extends Error {
-	public readonly status?: number
-	public readonly errorCode?: string
-
-	constructor(message: string, opts?: { status?: number; errorCode?: string }) {
-		super(message)
-		this.name = "ClaudeCodeOAuthTokenError"
-		this.status = opts?.status
-		this.errorCode = opts?.errorCode
-	}
-
-	public isLikelyInvalidGrant(): boolean {
-		if (this.errorCode && /invalid_grant/i.test(this.errorCode)) {
-			return true
-		}
-		if (this.status === 400 || this.status === 401 || this.status === 403) {
-			return /invalid_grant|revoked|expired|invalid refresh/i.test(this.message)
-		}
-		return false
-	}
-}
-
-function parseOAuthErrorDetails(errorText: string): { errorCode?: string; errorMessage?: string } {
-	try {
-		const json: unknown = JSON.parse(errorText)
-		if (!json || typeof json !== "object") {
-			return {}
-		}
-
-		const obj = json as Record<string, unknown>
-		const errorField = obj.error
-
-		const errorCode: string | undefined =
-			typeof errorField === "string"
-				? errorField
-				: errorField &&
-					  typeof errorField === "object" &&
-					  typeof (errorField as Record<string, unknown>).type === "string"
-					? ((errorField as Record<string, unknown>).type as string)
-					: undefined
-
-		const errorDescription = obj.error_description
-		const errorMessageFromError =
-			errorField && typeof errorField === "object" ? (errorField as Record<string, unknown>).message : undefined
-
-		const errorMessage: string | undefined =
-			typeof errorDescription === "string"
-				? errorDescription
-				: typeof errorMessageFromError === "string"
-					? errorMessageFromError
-					: typeof obj.message === "string"
-						? obj.message
-						: undefined
-
-		return { errorCode, errorMessage }
-	} catch {
-		return {}
-	}
-}
-
-/**
- * Generates a cryptographically random PKCE code verifier
- * Must be 43-128 characters long using unreserved characters
- */
-export function generateCodeVerifier(): string {
-	// Generate 32 random bytes and encode as base64url (will be 43 characters)
-	const buffer = crypto.randomBytes(32)
-	return buffer.toString("base64url")
-}
-
-/**
- * Generates the PKCE code challenge from the verifier using S256 method
- */
-export function generateCodeChallenge(verifier: string): string {
-	const hash = crypto.createHash("sha256").update(verifier).digest()
-	return hash.toString("base64url")
-}
-
-/**
- * Generates a random state parameter for CSRF protection
- */
-export function generateState(): string {
-	return crypto.randomBytes(16).toString("hex")
-}
-
-/**
- * Generates a user_id in the format required by Claude Code API
- * Format: user_<hash>_account_<uuid>_session_<uuid>
- */
-export function generateUserId(email?: string): string {
-	// Generate user hash from email or random bytes
-	const userHash = email
-		? crypto.createHash("sha256").update(email).digest("hex").slice(0, 16)
-		: crypto.randomBytes(8).toString("hex")
-
-	// Generate account UUID (persistent per email or random)
-	const accountUuid = email
-		? crypto.createHash("sha256").update(`account:${email}`).digest("hex").slice(0, 32)
-		: crypto.randomUUID().replace(/-/g, "")
-
-	// Generate session UUID (always random for each request)
-	const sessionUuid = crypto.randomUUID().replace(/-/g, "")
-
-	return `user_${userHash}_account_${accountUuid}_session_${sessionUuid}`
-}
-
-/**
- * Builds the authorization URL for OAuth flow
- */
-export function buildAuthorizationUrl(codeChallenge: string, state: string): string {
-	const params = new URLSearchParams({
-		client_id: CLAUDE_CODE_OAUTH_CONFIG.clientId,
-		redirect_uri: CLAUDE_CODE_OAUTH_CONFIG.redirectUri,
-		scope: CLAUDE_CODE_OAUTH_CONFIG.scopes,
-		code_challenge: codeChallenge,
-		code_challenge_method: "S256",
-		response_type: "code",
-		state,
-	})
-
-	return `${CLAUDE_CODE_OAUTH_CONFIG.authorizationEndpoint}?${params.toString()}`
-}
-
-/**
- * Exchanges the authorization code for tokens
- */
-export async function exchangeCodeForTokens(
-	code: string,
-	codeVerifier: string,
-	state: string,
-): Promise<ClaudeCodeCredentials> {
-	const body = {
-		code,
-		state,
-		grant_type: "authorization_code",
-		client_id: CLAUDE_CODE_OAUTH_CONFIG.clientId,
-		redirect_uri: CLAUDE_CODE_OAUTH_CONFIG.redirectUri,
-		code_verifier: codeVerifier,
-	}
-
-	const response = await fetch(CLAUDE_CODE_OAUTH_CONFIG.tokenEndpoint, {
-		method: "POST",
-		headers: {
-			"Content-Type": "application/json",
-		},
-		body: JSON.stringify(body),
-		signal: AbortSignal.timeout(30000),
-	})
-
-	if (!response.ok) {
-		const errorText = await response.text()
-		throw new Error(`Token exchange failed: ${response.status} ${response.statusText} - ${errorText}`)
-	}
-
-	const data = await response.json()
-	const tokenResponse = tokenResponseSchema.parse(data)
-
-	if (!tokenResponse.refresh_token) {
-		// The access token is unusable without a refresh token for persistence.
-		throw new Error("Token exchange did not return a refresh_token")
-	}
-
-	// Calculate expiry time
-	const expiresAt = new Date(Date.now() + tokenResponse.expires_in * 1000)
-
-	return {
-		type: "claude",
-		access_token: tokenResponse.access_token,
-		refresh_token: tokenResponse.refresh_token,
-		expired: expiresAt.toISOString(),
-		email: tokenResponse.email,
-	}
-}
-
-/**
- * Refreshes the access token using the refresh token
- */
-export async function refreshAccessToken(credentials: ClaudeCodeCredentials): Promise<ClaudeCodeCredentials> {
-	const body = {
-		grant_type: "refresh_token",
-		client_id: CLAUDE_CODE_OAUTH_CONFIG.clientId,
-		refresh_token: credentials.refresh_token,
-	}
-
-	const response = await fetch(CLAUDE_CODE_OAUTH_CONFIG.tokenEndpoint, {
-		method: "POST",
-		headers: {
-			"Content-Type": "application/json",
-		},
-		body: JSON.stringify(body),
-		signal: AbortSignal.timeout(30000),
-	})
-
-	if (!response.ok) {
-		const errorText = await response.text()
-		const { errorCode, errorMessage } = parseOAuthErrorDetails(errorText)
-		const details = errorMessage ? errorMessage : errorText
-		throw new ClaudeCodeOAuthTokenError(
-			`Token refresh failed: ${response.status} ${response.statusText}${details ? ` - ${details}` : ""}`,
-			{ status: response.status, errorCode },
-		)
-	}
-
-	const data = await response.json()
-	const tokenResponse = tokenResponseSchema.parse(data)
-
-	// Calculate expiry time
-	const expiresAt = new Date(Date.now() + tokenResponse.expires_in * 1000)
-
-	return {
-		type: "claude",
-		access_token: tokenResponse.access_token,
-		refresh_token: tokenResponse.refresh_token ?? credentials.refresh_token,
-		expired: expiresAt.toISOString(),
-		email: tokenResponse.email ?? credentials.email,
-	}
-}
-
-/**
- * Checks if the credentials are expired (with 5 minute buffer)
- */
-export function isTokenExpired(credentials: ClaudeCodeCredentials): boolean {
-	const expiryTime = new Date(credentials.expired).getTime()
-	const bufferMs = 5 * 60 * 1000 // 5 minutes buffer
-	return Date.now() >= expiryTime - bufferMs
-}
-
-/**
- * ClaudeCodeOAuthManager - Handles OAuth flow and token management
- */
-export class ClaudeCodeOAuthManager {
-	private context: ExtensionContext | null = null
-	private credentials: ClaudeCodeCredentials | null = null
-	private logFn: ((message: string) => void) | null = null
-	private refreshPromise: Promise<ClaudeCodeCredentials> | null = null
-	private pendingAuth: {
-		codeVerifier: string
-		state: string
-		server?: http.Server
-	} | null = null
-
-	private log(message: string): void {
-		if (this.logFn) {
-			this.logFn(message)
-		} else {
-			console.log(message)
-		}
-	}
-
-	private logError(message: string, error?: unknown): void {
-		const details = error instanceof Error ? error.message : error !== undefined ? String(error) : undefined
-		const full = details ? `${message} ${details}` : message
-		this.log(full)
-		console.error(full)
-	}
-
-	/**
-	 * Initialize the OAuth manager with VS Code extension context
-	 */
-	initialize(context: ExtensionContext, logFn?: (message: string) => void): void {
-		this.context = context
-		this.logFn = logFn ?? null
-	}
-
-	/**
-	 * Force a refresh using the stored refresh token even if the access token is not expired.
-	 * Useful when the server invalidates an access token early.
-	 */
-	async forceRefreshAccessToken(): Promise<string | null> {
-		if (!this.credentials) {
-			await this.loadCredentials()
-		}
-
-		if (!this.credentials) {
-			return null
-		}
-
-		try {
-			// De-dupe concurrent refreshes
-			if (!this.refreshPromise) {
-				const prevRefreshToken = this.credentials.refresh_token
-				this.log(`[claude-code-oauth] Forcing token refresh (expired=${this.credentials.expired})...`)
-				this.refreshPromise = refreshAccessToken(this.credentials).then((newCreds) => {
-					const rotated = newCreds.refresh_token !== prevRefreshToken
-					this.log(
-						`[claude-code-oauth] Forced refresh response received (expires_in≈${Math.round(
-							(new Date(newCreds.expired).getTime() - Date.now()) / 1000,
-						)}s, refresh_token_rotated=${rotated})`,
-					)
-					return newCreds
-				})
-			}
-
-			const newCredentials = await this.refreshPromise
-			this.refreshPromise = null
-			await this.saveCredentials(newCredentials)
-			this.log(`[claude-code-oauth] Forced token persisted (expired=${newCredentials.expired})`)
-			return newCredentials.access_token
-		} catch (error) {
-			this.refreshPromise = null
-			this.logError("[claude-code-oauth] Failed to force refresh token:", error)
-			if (error instanceof ClaudeCodeOAuthTokenError && error.isLikelyInvalidGrant()) {
-				this.log("[claude-code-oauth] Refresh token appears invalid; clearing stored credentials")
-				await this.clearCredentials()
-			}
-			return null
-		}
-	}
-
-	/**
-	 * Load credentials from storage
-	 */
-	async loadCredentials(): Promise<ClaudeCodeCredentials | null> {
-		if (!this.context) {
-			return null
-		}
-
-		try {
-			const credentialsJson = await this.context.secrets.get(CLAUDE_CODE_CREDENTIALS_KEY)
-			if (!credentialsJson) {
-				return null
-			}
-
-			const parsed = JSON.parse(credentialsJson)
-			this.credentials = claudeCodeCredentialsSchema.parse(parsed)
-			return this.credentials
-		} catch (error) {
-			this.logError("[claude-code-oauth] Failed to load credentials:", error)
-			return null
-		}
-	}
-
-	/**
-	 * Save credentials to storage
-	 */
-	async saveCredentials(credentials: ClaudeCodeCredentials): Promise<void> {
-		if (!this.context) {
-			throw new Error("OAuth manager not initialized")
-		}
-
-		await this.context.secrets.store(CLAUDE_CODE_CREDENTIALS_KEY, JSON.stringify(credentials))
-		this.credentials = credentials
-	}
-
-	/**
-	 * Clear credentials from storage
-	 */
-	async clearCredentials(): Promise<void> {
-		if (!this.context) {
-			return
-		}
-
-		await this.context.secrets.delete(CLAUDE_CODE_CREDENTIALS_KEY)
-		this.credentials = null
-	}
-
-	/**
-	 * Get a valid access token, refreshing if necessary
-	 */
-	async getAccessToken(): Promise<string | null> {
-		// Try to load credentials if not already loaded
-		if (!this.credentials) {
-			await this.loadCredentials()
-		}
-
-		if (!this.credentials) {
-			return null
-		}
-
-		// Check if token is expired and refresh if needed
-		if (isTokenExpired(this.credentials)) {
-			try {
-				// De-dupe concurrent refreshes
-				if (!this.refreshPromise) {
-					this.log(
-						`[claude-code-oauth] Access token expired (expired=${this.credentials.expired}). Refreshing...`,
-					)
-					const prevRefreshToken = this.credentials.refresh_token
-					this.refreshPromise = refreshAccessToken(this.credentials).then((newCreds) => {
-						const rotated = newCreds.refresh_token !== prevRefreshToken
-						this.log(
-							`[claude-code-oauth] Refresh response received (expires_in≈${Math.round(
-								(new Date(newCreds.expired).getTime() - Date.now()) / 1000,
-							)}s, refresh_token_rotated=${rotated})`,
-						)
-						return newCreds
-					})
-				}
-
-				const newCredentials = await this.refreshPromise
-				this.refreshPromise = null
-				await this.saveCredentials(newCredentials)
-				this.log(`[claude-code-oauth] Token persisted (expired=${newCredentials.expired})`)
-			} catch (error) {
-				this.refreshPromise = null
-				this.logError("[claude-code-oauth] Failed to refresh token:", error)
-
-				// Only clear secrets when the refresh token is clearly invalid/revoked.
-				if (error instanceof ClaudeCodeOAuthTokenError && error.isLikelyInvalidGrant()) {
-					this.log("[claude-code-oauth] Refresh token appears invalid; clearing stored credentials")
-					await this.clearCredentials()
-				}
-				return null
-			}
-		}
-
-		return this.credentials.access_token
-	}
-
-	/**
-	 * Get the user's email from credentials
-	 */
-	async getEmail(): Promise<string | null> {
-		if (!this.credentials) {
-			await this.loadCredentials()
-		}
-		return this.credentials?.email || null
-	}
-
-	/**
-	 * Check if the user is authenticated
-	 */
-	async isAuthenticated(): Promise<boolean> {
-		const token = await this.getAccessToken()
-		return token !== null
-	}
-
-	/**
-	 * Start the OAuth authorization flow
-	 * Returns the authorization URL to open in browser
-	 */
-	startAuthorizationFlow(): string {
-		// Cancel any existing authorization flow before starting a new one
-		this.cancelAuthorizationFlow()
-
-		const codeVerifier = generateCodeVerifier()
-		const codeChallenge = generateCodeChallenge(codeVerifier)
-		const state = generateState()
-
-		this.pendingAuth = {
-			codeVerifier,
-			state,
-		}
-
-		return buildAuthorizationUrl(codeChallenge, state)
-	}
-
-	/**
-	 * Start a local server to receive the OAuth callback
-	 * Returns a promise that resolves when authentication is complete
-	 */
-	async waitForCallback(): Promise<ClaudeCodeCredentials> {
-		if (!this.pendingAuth) {
-			throw new Error("No pending authorization flow")
-		}
-
-		// Close any existing server before starting a new one
-		if (this.pendingAuth.server) {
-			try {
-				this.pendingAuth.server.close()
-			} catch {
-				// Ignore errors when closing
-			}
-			this.pendingAuth.server = undefined
-		}
-
-		return new Promise((resolve, reject) => {
-			const server = http.createServer(async (req, res) => {
-				try {
-					const url = new URL(req.url || "", `http://localhost:${CLAUDE_CODE_OAUTH_CONFIG.callbackPort}`)
-
-					if (url.pathname !== "/callback") {
-						res.writeHead(404)
-						res.end("Not Found")
-						return
-					}
-
-					const code = url.searchParams.get("code")
-					const state = url.searchParams.get("state")
-					const error = url.searchParams.get("error")
-
-					if (error) {
-						res.writeHead(400)
-						res.end(`Authentication failed: ${error}`)
-						reject(new Error(`OAuth error: ${error}`))
-						server.close()
-						return
-					}
-
-					if (!code || !state) {
-						res.writeHead(400)
-						res.end("Missing code or state parameter")
-						reject(new Error("Missing code or state parameter"))
-						server.close()
-						return
-					}
-
-					if (state !== this.pendingAuth?.state) {
-						res.writeHead(400)
-						res.end("State mismatch - possible CSRF attack")
-						reject(new Error("State mismatch"))
-						server.close()
-						return
-					}
-
-					try {
-						const credentials = await exchangeCodeForTokens(code, this.pendingAuth.codeVerifier, state)
-
-						await this.saveCredentials(credentials)
-
-						res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" })
-						res.end(`<!DOCTYPE html>
-<html>
-<head>
-<meta charset="utf-8">
-<title>Authentication Successful</title>
-</head>
-<body style="font-family: system-ui; text-align: center; padding: 50px;">
-<h1>&#10003; Authentication Successful</h1>
-<p>You can close this window and return to VS Code.</p>
-<script>window.close();</script>
-</body>
-</html>`)
-
-						this.pendingAuth = null
-						server.close()
-						resolve(credentials)
-					} catch (exchangeError) {
-						res.writeHead(500)
-						res.end(`Token exchange failed: ${exchangeError}`)
-						reject(exchangeError)
-						server.close()
-					}
-				} catch (err) {
-					res.writeHead(500)
-					res.end("Internal server error")
-					reject(err)
-					server.close()
-				}
-			})
-
-			server.on("error", (err: NodeJS.ErrnoException) => {
-				this.pendingAuth = null
-				if (err.code === "EADDRINUSE") {
-					reject(
-						new Error(
-							`Port ${CLAUDE_CODE_OAUTH_CONFIG.callbackPort} is already in use. ` +
-								`Please close any other applications using this port and try again.`,
-						),
-					)
-				} else {
-					reject(err)
-				}
-			})
-
-			// Set a timeout for the callback
-			const timeout = setTimeout(
-				() => {
-					server.close()
-					reject(new Error("Authentication timed out"))
-				},
-				5 * 60 * 1000,
-			) // 5 minutes
-
-			server.listen(CLAUDE_CODE_OAUTH_CONFIG.callbackPort, () => {
-				if (this.pendingAuth) {
-					this.pendingAuth.server = server
-				}
-			})
-
-			// Clear timeout when server closes
-			server.on("close", () => {
-				clearTimeout(timeout)
-			})
-		})
-	}
-
-	/**
-	 * Cancel any pending authorization flow
-	 */
-	cancelAuthorizationFlow(): void {
-		if (this.pendingAuth?.server) {
-			this.pendingAuth.server.close()
-		}
-		this.pendingAuth = null
-	}
-
-	/**
-	 * Get the current credentials (for display purposes)
-	 */
-	getCredentials(): ClaudeCodeCredentials | null {
-		return this.credentials
-	}
-}
-
-// Singleton instance
-export const claudeCodeOAuthManager = new ClaudeCodeOAuthManager()

+ 0 - 759
src/integrations/claude-code/streaming-client.ts

@@ -1,759 +0,0 @@
-import type { Anthropic } from "@anthropic-ai/sdk"
-import type { ClaudeCodeRateLimitInfo } from "@roo-code/types"
-import { Package } from "../../shared/package"
-
-/**
- * Set of content block types that are valid for Anthropic API.
- * Only these types will be passed through to the API.
- * See: https://docs.anthropic.com/en/api/messages
- */
-const VALID_ANTHROPIC_BLOCK_TYPES = new Set([
-	"text",
-	"image",
-	"tool_use",
-	"tool_result",
-	"thinking",
-	"redacted_thinking",
-	"document",
-])
-
-type ContentBlockWithType = { type: string }
-
-/**
- * Filters out non-Anthropic content blocks from messages before sending to the API.
- *
- * NOTE: This function performs FILTERING ONLY - no type conversion is performed.
- * Blocks are either kept as-is or removed entirely based on the allowlist.
- *
- * Uses an allowlist approach - only blocks with types in VALID_ANTHROPIC_BLOCK_TYPES are kept.
- * This automatically filters out:
- * - Internal "reasoning" blocks (Roo Code's internal representation) - NOT converted to "thinking"
- * - Gemini's "thoughtSignature" blocks
- * - Any other unknown block types
- *
- * IMPORTANT: This function also strips message-level fields that are not part of the Anthropic API:
- * - `reasoning_details` (added by OpenRouter/Roo providers for Gemini/OpenAI reasoning)
- * - Any other non-standard fields added by other providers
- *
- * We preserve ALL "thinking" blocks (Anthropic's native extended thinking format) for these reasons:
- * 1. Rewind functionality - users need to be able to go back in conversation history
- * 2. Claude Opus 4.5+ preserves thinking blocks by default (per Anthropic docs)
- * 3. Interleaved thinking requires thinking blocks to be passed back for tool use continuations
- *
- * The API will handle thinking blocks appropriately based on the model:
- * - Claude Opus 4.5+: thinking blocks preserved (enables cache optimization)
- * - Older models: thinking blocks stripped from prior turns automatically
- */
-function filterNonAnthropicBlocks(messages: Anthropic.Messages.MessageParam[]): Anthropic.Messages.MessageParam[] {
-	const result: Anthropic.Messages.MessageParam[] = []
-
-	for (const message of messages) {
-		// Extract ONLY the standard Anthropic message fields (role, content)
-		// This strips out any extra fields like `reasoning_details` that other providers
-		// may have added to the messages (e.g., OpenRouter adds reasoning_details for Gemini/o-series)
-		const { role, content } = message
-
-		if (typeof content === "string") {
-			// Return a clean message with only role and content
-			result.push({ role, content })
-			continue
-		}
-
-		// Filter out invalid block types (allowlist)
-		const filteredContent = content.filter((block) =>
-			VALID_ANTHROPIC_BLOCK_TYPES.has((block as ContentBlockWithType).type),
-		)
-
-		// If all content was filtered out, skip this message
-		if (filteredContent.length === 0) {
-			continue
-		}
-
-		// Return a clean message with only role and content (no extra fields)
-		result.push({
-			role,
-			content: filteredContent,
-		})
-	}
-
-	return result
-}
-
-/**
- * Adds cache_control breakpoints to the last two user messages for prompt caching.
- * This follows Anthropic's recommended pattern:
- * - Cache the system prompt (handled separately)
- * - Cache the last text block of the second-to-last user message
- * - Cache the last text block of the last user message
- *
- * According to Anthropic docs:
- * - System prompts and tools remain cached despite thinking parameter changes
- * - Message cache breakpoints are invalidated when thinking parameters change
- * - When using extended thinking, thinking blocks from previous turns are stripped from context
- */
-function addMessageCacheBreakpoints(messages: Anthropic.Messages.MessageParam[]): Anthropic.Messages.MessageParam[] {
-	// Find indices of user messages
-	const userMsgIndices = messages.reduce(
-		(acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc),
-		[] as number[],
-	)
-
-	const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
-	const secondLastUserMsgIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
-
-	return messages.map((message, index) => {
-		// Only add cache control to the last two user messages
-		if (index !== lastUserMsgIndex && index !== secondLastUserMsgIndex) {
-			return message
-		}
-
-		// Handle string content
-		if (typeof message.content === "string") {
-			return {
-				...message,
-				content: [
-					{
-						type: "text" as const,
-						text: message.content,
-						cache_control: { type: "ephemeral" as const },
-					},
-				],
-			}
-		}
-
-		// Handle array content - add cache_control to the last text block
-		const contentWithCache = message.content.map((block, blockIndex) => {
-			// Find the last text block index
-			let lastTextIndex = -1
-			for (let i = message.content.length - 1; i >= 0; i--) {
-				if ((message.content[i] as { type: string }).type === "text") {
-					lastTextIndex = i
-					break
-				}
-			}
-
-			// Only add cache_control to text blocks (the last one specifically)
-			if (blockIndex === lastTextIndex && (block as { type: string }).type === "text") {
-				const textBlock = block as { type: "text"; text: string }
-				return {
-					type: "text" as const,
-					text: textBlock.text,
-					cache_control: { type: "ephemeral" as const },
-				}
-			}
-
-			return block
-		})
-
-		return {
-			...message,
-			content: contentWithCache,
-		}
-	})
-}
-
-// API Configuration
-export const CLAUDE_CODE_API_CONFIG = {
-	endpoint: "https://api.anthropic.com/v1/messages",
-	version: "2023-06-01",
-	defaultBetas: [
-		"prompt-caching-2024-07-31",
-		"claude-code-20250219",
-		"oauth-2025-04-20",
-		"interleaved-thinking-2025-05-14",
-		"fine-grained-tool-streaming-2025-05-14",
-	],
-	userAgent: `Roo-Code/${Package.version}`,
-} as const
-
-/**
- * SSE Event types from Anthropic streaming API
- */
-export type SSEEventType =
-	| "message_start"
-	| "content_block_start"
-	| "content_block_delta"
-	| "content_block_stop"
-	| "message_delta"
-	| "message_stop"
-	| "ping"
-	| "error"
-
-export interface SSEEvent {
-	event: SSEEventType
-	data: unknown
-}
-
-/**
- * Thinking configuration for extended thinking mode
- */
-export type ThinkingConfig =
-	| {
-			type: "enabled"
-			budget_tokens: number
-	  }
-	| {
-			type: "disabled"
-	  }
-
-/**
- * Stream message request options
- */
-export interface StreamMessageOptions {
-	accessToken: string
-	model: string
-	systemPrompt: string
-	messages: Anthropic.Messages.MessageParam[]
-	maxTokens?: number
-	thinking?: ThinkingConfig
-	tools?: Anthropic.Messages.Tool[]
-	toolChoice?: Anthropic.Messages.ToolChoice
-	metadata?: {
-		user_id?: string
-	}
-	signal?: AbortSignal
-}
-
-/**
- * SSE Parser state that persists across chunks
- * This is necessary because SSE events can be split across multiple chunks
- */
-interface SSEParserState {
-	buffer: string
-	currentEvent: string | null
-	currentData: string[]
-}
-
-/**
- * Creates initial SSE parser state
- */
-function createSSEParserState(): SSEParserState {
-	return {
-		buffer: "",
-		currentEvent: null,
-		currentData: [],
-	}
-}
-
-/**
- * Parses SSE lines from a text chunk
- * Returns parsed events and updates the state for the next chunk
- *
- * The state persists across chunks to handle events that span multiple chunks:
- * - buffer: incomplete line from previous chunk
- * - currentEvent: event type if we've seen "event:" but not the complete event
- * - currentData: accumulated data lines for the current event
- */
-function parseSSEChunk(chunk: string, state: SSEParserState): { events: SSEEvent[]; state: SSEParserState } {
-	const events: SSEEvent[] = []
-	const lines = (state.buffer + chunk).split("\n")
-
-	// Start with the accumulated state
-	let currentEvent = state.currentEvent
-	let currentData = [...state.currentData]
-	let remaining = ""
-
-	for (let i = 0; i < lines.length; i++) {
-		const line = lines[i]
-
-		// If this is the last line and doesn't end with newline, it might be incomplete
-		if (i === lines.length - 1 && !chunk.endsWith("\n") && line !== "") {
-			remaining = line
-			continue
-		}
-
-		// Empty line signals end of event
-		if (line === "") {
-			if (currentEvent && currentData.length > 0) {
-				try {
-					const dataStr = currentData.join("\n")
-					const data = dataStr === "[DONE]" ? null : JSON.parse(dataStr)
-					events.push({
-						event: currentEvent as SSEEventType,
-						data,
-					})
-				} catch {
-					// Skip malformed events
-					console.error("[claude-code-streaming] Failed to parse SSE data:", currentData.join("\n"))
-				}
-			}
-			currentEvent = null
-			currentData = []
-			continue
-		}
-
-		// Parse event type
-		if (line.startsWith("event: ")) {
-			currentEvent = line.slice(7)
-			continue
-		}
-
-		// Parse data
-		if (line.startsWith("data: ")) {
-			currentData.push(line.slice(6))
-			continue
-		}
-	}
-
-	// Return updated state for next chunk
-	return {
-		events,
-		state: {
-			buffer: remaining,
-			currentEvent,
-			currentData,
-		},
-	}
-}
-
-/**
- * Stream chunk types that the handler can yield
- */
-export interface StreamTextChunk {
-	type: "text"
-	text: string
-}
-
-export interface StreamReasoningChunk {
-	type: "reasoning"
-	text: string
-}
-
-/**
- * A complete thinking block with signature, used for tool use continuations.
- * According to Anthropic docs:
- * - During tool use, you must pass thinking blocks back to the API for the last assistant message
- * - Include the complete unmodified block back to the API to maintain reasoning continuity
- * - The signature field is used to verify that thinking blocks were generated by Claude
- */
-export interface StreamThinkingCompleteChunk {
-	type: "thinking_complete"
-	index: number
-	thinking: string
-	signature: string
-}
-
-export interface StreamToolCallPartialChunk {
-	type: "tool_call_partial"
-	index: number
-	id?: string
-	name?: string
-	arguments?: string
-}
-
-export interface StreamUsageChunk {
-	type: "usage"
-	inputTokens: number
-	outputTokens: number
-	cacheReadTokens?: number
-	cacheWriteTokens?: number
-	totalCost?: number
-}
-
-export interface StreamErrorChunk {
-	type: "error"
-	error: string
-}
-
-export type StreamChunk =
-	| StreamTextChunk
-	| StreamReasoningChunk
-	| StreamThinkingCompleteChunk
-	| StreamToolCallPartialChunk
-	| StreamUsageChunk
-	| StreamErrorChunk
-
-/**
- * Creates a streaming message request to the Anthropic API using OAuth
- */
-export async function* createStreamingMessage(options: StreamMessageOptions): AsyncGenerator<StreamChunk> {
-	const { accessToken, model, systemPrompt, messages, maxTokens, thinking, tools, toolChoice, metadata, signal } =
-		options
-
-	// Filter out non-Anthropic blocks before processing
-	const sanitizedMessages = filterNonAnthropicBlocks(messages)
-
-	// Add cache breakpoints to the last two user messages
-	// According to Anthropic docs:
-	// - System prompts and tools remain cached despite thinking parameter changes
-	// - Message cache breakpoints are invalidated when thinking parameters change
-	// - We cache the last two user messages for optimal cache hit rates
-	const messagesWithCache = addMessageCacheBreakpoints(sanitizedMessages)
-
-	// Build request body - match Claude Code format exactly
-	const body: Record<string, unknown> = {
-		model,
-		stream: true,
-		messages: messagesWithCache,
-	}
-
-	// Only include max_tokens if explicitly provided
-	if (maxTokens !== undefined) {
-		body.max_tokens = maxTokens
-	}
-
-	// Add thinking configuration for extended thinking mode
-	if (thinking) {
-		body.thinking = thinking
-	}
-
-	// System prompt as array of content blocks (Claude Code format)
-	// Prepend Claude Code branding as required by the API
-	// Add cache_control to the last text block for prompt caching
-	// System prompt caching is preserved even when thinking parameters change
-	body.system = [
-		{ type: "text", text: "You are Claude Code, Anthropic's official CLI for Claude." },
-		...(systemPrompt ? [{ type: "text", text: systemPrompt, cache_control: { type: "ephemeral" } }] : []),
-	]
-
-	// Metadata with user_id is required for Claude Code
-	if (metadata) {
-		body.metadata = metadata
-	}
-
-	if (tools && tools.length > 0) {
-		body.tools = tools
-		// Default tool_choice to "auto" when tools are provided (as per spec example)
-		body.tool_choice = toolChoice || { type: "auto" }
-	} else if (toolChoice) {
-		body.tool_choice = toolChoice
-	}
-
-	// Build minimal headers
-	const headers: Record<string, string> = {
-		Authorization: `Bearer ${accessToken}`,
-		"Content-Type": "application/json",
-		"Anthropic-Version": CLAUDE_CODE_API_CONFIG.version,
-		"Anthropic-Beta": CLAUDE_CODE_API_CONFIG.defaultBetas.join(","),
-		Accept: "text/event-stream",
-		"User-Agent": CLAUDE_CODE_API_CONFIG.userAgent,
-	}
-
-	// Make the request
-	const response = await fetch(`${CLAUDE_CODE_API_CONFIG.endpoint}?beta=true`, {
-		method: "POST",
-		headers,
-		body: JSON.stringify(body),
-		signal,
-	})
-
-	if (!response.ok) {
-		const errorText = await response.text()
-		let errorMessage = `API request failed: ${response.status} ${response.statusText}`
-		try {
-			const errorJson = JSON.parse(errorText)
-			if (errorJson.error?.message) {
-				errorMessage = errorJson.error.message
-			}
-		} catch {
-			if (errorText) {
-				errorMessage += ` - ${errorText}`
-			}
-		}
-		yield { type: "error", error: errorMessage }
-		return
-	}
-
-	if (!response.body) {
-		yield { type: "error", error: "No response body" }
-		return
-	}
-
-	// Track usage across events
-	let totalInputTokens = 0
-	let totalOutputTokens = 0
-	let cacheReadTokens = 0
-	let cacheWriteTokens = 0
-
-	// Track content blocks by index for proper assembly
-	// This is critical for interleaved thinking - we need to capture complete thinking blocks
-	// with their signatures so they can be passed back to the API for tool use continuations
-	const contentBlocks: Map<
-		number,
-		{
-			type: string
-			text: string
-			signature?: string
-			id?: string
-			name?: string
-			arguments?: string
-		}
-	> = new Map()
-
-	// Read the stream
-	const reader = response.body.getReader()
-	const decoder = new TextDecoder()
-	let sseState = createSSEParserState()
-
-	try {
-		while (true) {
-			const { done, value } = await reader.read()
-			if (done) break
-
-			const chunk = decoder.decode(value, { stream: true })
-			const result = parseSSEChunk(chunk, sseState)
-			sseState = result.state
-			const events = result.events
-
-			for (const event of events) {
-				const eventData = event.data as Record<string, unknown> | null
-
-				if (!eventData) {
-					continue
-				}
-
-				switch (event.event) {
-					case "message_start": {
-						const message = eventData.message as Record<string, unknown>
-						if (!message) {
-							break
-						}
-						const usage = message.usage as Record<string, number> | undefined
-						if (usage) {
-							totalInputTokens += usage.input_tokens || 0
-							totalOutputTokens += usage.output_tokens || 0
-							cacheReadTokens += usage.cache_read_input_tokens || 0
-							cacheWriteTokens += usage.cache_creation_input_tokens || 0
-						}
-						break
-					}
-
-					case "content_block_start": {
-						const contentBlock = eventData.content_block as Record<string, unknown>
-						const index = eventData.index as number
-
-						if (contentBlock) {
-							switch (contentBlock.type) {
-								case "text":
-									// Initialize text block tracking
-									contentBlocks.set(index, {
-										type: "text",
-										text: (contentBlock.text as string) || "",
-									})
-									if (contentBlock.text) {
-										yield { type: "text", text: contentBlock.text as string }
-									}
-									break
-								case "thinking":
-									// Initialize thinking block tracking - critical for interleaved thinking
-									// We need to accumulate the text and capture the signature
-									contentBlocks.set(index, {
-										type: "thinking",
-										text: (contentBlock.thinking as string) || "",
-									})
-									if (contentBlock.thinking) {
-										yield { type: "reasoning", text: contentBlock.thinking as string }
-									}
-									break
-								case "tool_use":
-									contentBlocks.set(index, {
-										type: "tool_use",
-										text: "",
-										id: contentBlock.id as string,
-										name: contentBlock.name as string,
-										arguments: "",
-									})
-									yield {
-										type: "tool_call_partial",
-										index,
-										id: contentBlock.id as string,
-										name: contentBlock.name as string,
-										arguments: undefined,
-									}
-									break
-							}
-						}
-						break
-					}
-
-					case "content_block_delta": {
-						const delta = eventData.delta as Record<string, unknown>
-						const index = eventData.index as number
-						const block = contentBlocks.get(index)
-
-						if (delta) {
-							switch (delta.type) {
-								case "text_delta":
-									if (delta.text) {
-										// Accumulate text
-										if (block && block.type === "text") {
-											block.text += delta.text as string
-										}
-										yield { type: "text", text: delta.text as string }
-									}
-									break
-								case "thinking_delta":
-									if (delta.thinking) {
-										// Accumulate thinking text
-										if (block && block.type === "thinking") {
-											block.text += delta.thinking as string
-										}
-										yield { type: "reasoning", text: delta.thinking as string }
-									}
-									break
-								case "signature_delta":
-									// Capture the signature for the thinking block
-									// This is critical for interleaved thinking - the signature
-									// must be included when passing thinking blocks back to the API
-									if (delta.signature && block && block.type === "thinking") {
-										block.signature = delta.signature as string
-									}
-									break
-								case "input_json_delta":
-									if (block && block.type === "tool_use") {
-										block.arguments = (block.arguments || "") + (delta.partial_json as string)
-									}
-									yield {
-										type: "tool_call_partial",
-										index,
-										id: undefined,
-										name: undefined,
-										arguments: delta.partial_json as string,
-									}
-									break
-							}
-						}
-						break
-					}
-
-					case "content_block_stop": {
-						// When a content block completes, emit complete thinking blocks
-						// This enables the caller to preserve them for tool use continuations
-						const index = eventData.index as number
-						const block = contentBlocks.get(index)
-
-						if (block && block.type === "thinking" && block.signature) {
-							// Emit the complete thinking block with signature
-							// This is required for interleaved thinking with tool use
-							yield {
-								type: "thinking_complete",
-								index,
-								thinking: block.text,
-								signature: block.signature,
-							}
-						}
-						break
-					}
-
-					case "message_delta": {
-						const usage = eventData.usage as Record<string, number> | undefined
-						if (usage && usage.output_tokens !== undefined) {
-							// output_tokens in message_delta is the running total, not a delta
-							// So we replace rather than add
-							totalOutputTokens = usage.output_tokens
-						}
-						break
-					}
-
-					case "message_stop": {
-						// Yield final usage chunk
-						yield {
-							type: "usage",
-							inputTokens: totalInputTokens,
-							outputTokens: totalOutputTokens,
-							cacheReadTokens: cacheReadTokens > 0 ? cacheReadTokens : undefined,
-							cacheWriteTokens: cacheWriteTokens > 0 ? cacheWriteTokens : undefined,
-						}
-						break
-					}
-
-					case "error": {
-						const errorData = eventData.error as Record<string, unknown>
-						yield {
-							type: "error",
-							error: (errorData?.message as string) || "Unknown streaming error",
-						}
-						break
-					}
-				}
-			}
-		}
-	} finally {
-		reader.releaseLock()
-	}
-}
-
-/**
- * Parse rate limit headers from a response into a structured format
- */
-function parseRateLimitHeaders(headers: Headers): ClaudeCodeRateLimitInfo {
-	const getHeader = (name: string): string | null => headers.get(name)
-	const parseFloat = (val: string | null): number => (val ? Number.parseFloat(val) : 0)
-	const parseInt = (val: string | null): number => (val ? Number.parseInt(val, 10) : 0)
-
-	return {
-		fiveHour: {
-			status: getHeader("anthropic-ratelimit-unified-5h-status") || "unknown",
-			utilization: parseFloat(getHeader("anthropic-ratelimit-unified-5h-utilization")),
-			resetTime: parseInt(getHeader("anthropic-ratelimit-unified-5h-reset")),
-		},
-		weekly: {
-			status: getHeader("anthropic-ratelimit-unified-7d_sonnet-status") || "unknown",
-			utilization: parseFloat(getHeader("anthropic-ratelimit-unified-7d_sonnet-utilization")),
-			resetTime: parseInt(getHeader("anthropic-ratelimit-unified-7d_sonnet-reset")),
-		},
-		weeklyUnified: {
-			status: getHeader("anthropic-ratelimit-unified-7d-status") || "unknown",
-			utilization: parseFloat(getHeader("anthropic-ratelimit-unified-7d-utilization")),
-			resetTime: parseInt(getHeader("anthropic-ratelimit-unified-7d-reset")),
-		},
-		representativeClaim: getHeader("anthropic-ratelimit-unified-representative-claim") || undefined,
-		overage: {
-			status: getHeader("anthropic-ratelimit-unified-overage-status") || "unknown",
-			disabledReason: getHeader("anthropic-ratelimit-unified-overage-disabled-reason") || undefined,
-		},
-		fallbackPercentage: parseFloat(getHeader("anthropic-ratelimit-unified-fallback-percentage")) || undefined,
-		organizationId: getHeader("anthropic-organization-id") || undefined,
-		fetchedAt: Date.now(),
-	}
-}
-
-/**
- * Fetch rate limit information by making a minimal API call
- * Uses a small request to get the response headers containing rate limit data
- */
-export async function fetchRateLimitInfo(accessToken: string): Promise<ClaudeCodeRateLimitInfo> {
-	// Build minimal request body - use haiku for speed and lowest cost
-	const body = {
-		model: "claude-haiku-4-5",
-		max_tokens: 1,
-		system: [{ type: "text", text: "You are Claude Code, Anthropic's official CLI for Claude." }],
-		messages: [{ role: "user", content: "hi" }],
-	}
-
-	// Build minimal headers
-	const headers: Record<string, string> = {
-		Authorization: `Bearer ${accessToken}`,
-		"Content-Type": "application/json",
-		"Anthropic-Version": CLAUDE_CODE_API_CONFIG.version,
-		"Anthropic-Beta": CLAUDE_CODE_API_CONFIG.defaultBetas.join(","),
-		"User-Agent": CLAUDE_CODE_API_CONFIG.userAgent,
-	}
-
-	// Make the request
-	const response = await fetch(`${CLAUDE_CODE_API_CONFIG.endpoint}?beta=true`, {
-		method: "POST",
-		headers,
-		body: JSON.stringify(body),
-		signal: AbortSignal.timeout(30000),
-	})
-
-	if (!response.ok) {
-		const errorText = await response.text()
-		let errorMessage = `API request failed: ${response.status} ${response.statusText}`
-		try {
-			const errorJson = JSON.parse(errorText)
-			if (errorJson.error?.message) {
-				errorMessage = errorJson.error.message
-			}
-		} catch {
-			if (errorText) {
-				errorMessage += ` - ${errorText}`
-			}
-		}
-		throw new Error(errorMessage)
-	}
-
-	// Parse rate limit headers from the response
-	return parseRateLimitHeaders(response.headers)
-}

+ 1 - 1
src/shared/__tests__/api.spec.ts

@@ -9,7 +9,7 @@ describe("getModelMaxOutputTokens", () => {
 		supportsPromptCache: true,
 	}
 
-	test("should return model maxTokens when not using claude-code provider and maxTokens is within 20% of context window", () => {
+	test("should return model maxTokens when maxTokens is within 20% of context window", () => {
 		const settings: ProviderSettings = {
 			apiProvider: "anthropic",
 		}

+ 0 - 7
src/shared/__tests__/checkExistApiConfig.spec.ts

@@ -67,13 +67,6 @@ describe("checkExistKey", () => {
 		expect(checkExistKey(config)).toBe(true)
 	})
 
-	it("should return true for claude-code provider without API key", () => {
-		const config: ProviderSettings = {
-			apiProvider: "claude-code",
-		}
-		expect(checkExistKey(config)).toBe(true)
-	})
-
 	it("should return true for openai-codex provider without API key", () => {
 		const config: ProviderSettings = {
 			apiProvider: "openai-codex",

+ 2 - 5
src/shared/checkExistApiConfig.ts

@@ -5,11 +5,8 @@ export function checkExistKey(config: ProviderSettings | undefined) {
 		return false
 	}
 
-	// Special case for fake-ai, claude-code, openai-codex, qwen-code, and roo providers which don't need any configuration.
-	if (
-		config.apiProvider &&
-		["fake-ai", "claude-code", "openai-codex", "qwen-code", "roo"].includes(config.apiProvider)
-	) {
+	// Special case for fake-ai, openai-codex, qwen-code, and roo providers which don't need any configuration.
+	if (config.apiProvider && ["fake-ai", "openai-codex", "qwen-code", "roo"].includes(config.apiProvider)) {
 		return true
 	}
 

+ 14 - 26
webview-ui/src/components/chat/ChatRow.tsx

@@ -1113,32 +1113,20 @@ export const ChatRowContent = ({
 					let body = t(`chat:apiRequest.failed`)
 					let retryInfo, rawError, code, docsURL
 					if (message.text !== undefined) {
-						// Check for Claude Code authentication error first
-						if (message.text.includes("Not authenticated with Claude Code")) {
-							body = t("chat:apiRequest.errorMessage.claudeCodeNotAuthenticated")
-							docsURL = "roocode://settings?provider=claude-code"
-						} else {
-							// Try to show richer error message for that code, if available
-							const potentialCode = parseInt(message.text.substring(0, 3))
-							if (!isNaN(potentialCode) && potentialCode >= 400) {
-								code = potentialCode
-								const stringForError = `chat:apiRequest.errorMessage.${code}`
-								if (i18n.exists(stringForError)) {
-									body = t(stringForError)
-									// Fill this out in upcoming PRs
-									// Do not remove this
-									// switch(code) {
-									// 	case ERROR_CODE:
-									// 		docsURL = ???
-									// 		break;
-									// }
-								} else {
-									body = t("chat:apiRequest.errorMessage.unknown")
-									docsURL =
-										"mailto:[email protected]?subject=Unknown API Error&body=[Please include full error details]"
-								}
-							} else if (message.text.indexOf("Connection error") === 0) {
-								body = t("chat:apiRequest.errorMessage.connection")
+						// Try to show richer error message for that code, if available
+						const potentialCode = parseInt(message.text.substring(0, 3))
+						if (!isNaN(potentialCode) && potentialCode >= 400) {
+							code = potentialCode
+							const stringForError = `chat:apiRequest.errorMessage.${code}`
+							if (i18n.exists(stringForError)) {
+								body = t(stringForError)
+								// Fill this out in upcoming PRs
+								// Do not remove this
+								// switch(code) {
+								// 	case ERROR_CODE:
+								// 		docsURL = ???
+								// 		break;
+								// }
 							} else {
 								// Non-HTTP-status-code error message - store full text as errorDetails
 								body = t("chat:apiRequest.errorMessage.unknown")

+ 1 - 14
webview-ui/src/components/settings/ApiOptions.tsx

@@ -16,7 +16,6 @@ import {
 	openAiCodexDefaultModelId,
 	anthropicDefaultModelId,
 	doubaoDefaultModelId,
-	claudeCodeDefaultModelId,
 	qwenCodeDefaultModelId,
 	geminiDefaultModelId,
 	deepSeekDefaultModelId,
@@ -78,7 +77,6 @@ import {
 	Bedrock,
 	Cerebras,
 	Chutes,
-	ClaudeCode,
 	DeepSeek,
 	Doubao,
 	Gemini,
@@ -148,8 +146,7 @@ const ApiOptions = ({
 	setErrorMessage,
 }: ApiOptionsProps) => {
 	const { t } = useAppTranslation()
-	const { organizationAllowList, cloudIsAuthenticated, claudeCodeIsAuthenticated, openAiCodexIsAuthenticated } =
-		useExtensionState()
+	const { organizationAllowList, cloudIsAuthenticated, openAiCodexIsAuthenticated } = useExtensionState()
 
 	const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => {
 		const headers = apiConfiguration?.openAiHeaders || {}
@@ -347,7 +344,6 @@ const ApiOptions = ({
 				litellm: { field: "litellmModelId", default: litellmDefaultModelId },
 				anthropic: { field: "apiModelId", default: anthropicDefaultModelId },
 				cerebras: { field: "apiModelId", default: cerebrasDefaultModelId },
-				"claude-code": { field: "apiModelId", default: claudeCodeDefaultModelId },
 				"openai-codex": { field: "apiModelId", default: openAiCodexDefaultModelId },
 				"qwen-code": { field: "apiModelId", default: qwenCodeDefaultModelId },
 				"openai-native": { field: "apiModelId", default: openAiNativeDefaultModelId },
@@ -562,15 +558,6 @@ const ApiOptions = ({
 				/>
 			)}
 
-			{selectedProvider === "claude-code" && (
-				<ClaudeCode
-					apiConfiguration={apiConfiguration}
-					setApiConfigurationField={setApiConfigurationField}
-					simplifySettings={fromWelcomeView}
-					claudeCodeIsAuthenticated={claudeCodeIsAuthenticated}
-				/>
-			)}
-
 			{selectedProvider === "openai-codex" && (
 				<OpenAICodex
 					apiConfiguration={apiConfiguration}

+ 0 - 3
webview-ui/src/components/settings/constants.ts

@@ -4,7 +4,6 @@ import {
 	anthropicModels,
 	bedrockModels,
 	cerebrasModels,
-	claudeCodeModels,
 	deepSeekModels,
 	moonshotModels,
 	geminiModels,
@@ -26,7 +25,6 @@ import {
 
 export const MODELS_BY_PROVIDER: Partial<Record<ProviderName, Record<string, ModelInfo>>> = {
 	anthropic: anthropicModels,
-	"claude-code": claudeCodeModels,
 	bedrock: bedrockModels,
 	cerebras: cerebrasModels,
 	deepseek: deepSeekModels,
@@ -52,7 +50,6 @@ export const PROVIDERS = [
 	{ value: "openrouter", label: "OpenRouter", proxy: false },
 	{ value: "deepinfra", label: "DeepInfra", proxy: false },
 	{ value: "anthropic", label: "Anthropic", proxy: false },
-	{ value: "claude-code", label: "Claude Code", proxy: false },
 	{ value: "cerebras", label: "Cerebras", proxy: false },
 	{ value: "gemini", label: "Google Gemini", proxy: false },
 	{ value: "doubao", label: "Doubao", proxy: false },

+ 0 - 71
webview-ui/src/components/settings/providers/ClaudeCode.tsx

@@ -1,71 +0,0 @@
-import React from "react"
-
-import { type ProviderSettings, claudeCodeDefaultModelId, claudeCodeModels } from "@roo-code/types"
-
-import { useAppTranslation } from "@src/i18n/TranslationContext"
-import { Button } from "@src/components/ui"
-import { vscode } from "@src/utils/vscode"
-
-import { ModelPicker } from "../ModelPicker"
-import { ClaudeCodeRateLimitDashboard } from "./ClaudeCodeRateLimitDashboard"
-
-interface ClaudeCodeProps {
-	apiConfiguration: ProviderSettings
-	setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
-	simplifySettings?: boolean
-	claudeCodeIsAuthenticated?: boolean
-}
-
-export const ClaudeCode: React.FC<ClaudeCodeProps> = ({
-	apiConfiguration,
-	setApiConfigurationField,
-	simplifySettings,
-	claudeCodeIsAuthenticated = false,
-}) => {
-	const { t } = useAppTranslation()
-
-	return (
-		<div className="flex flex-col gap-4">
-			{/* Authentication Section */}
-			<div className="flex flex-col gap-2">
-				{claudeCodeIsAuthenticated ? (
-					<div className="flex justify-end">
-						<Button
-							variant="secondary"
-							size="sm"
-							onClick={() => vscode.postMessage({ type: "claudeCodeSignOut" })}>
-							{t("settings:providers.claudeCode.signOutButton", {
-								defaultValue: "Sign Out",
-							})}
-						</Button>
-					</div>
-				) : (
-					<Button
-						variant="primary"
-						onClick={() => vscode.postMessage({ type: "claudeCodeSignIn" })}
-						className="w-fit">
-						{t("settings:providers.claudeCode.signInButton", {
-							defaultValue: "Sign in to Claude Code",
-						})}
-					</Button>
-				)}
-			</div>
-
-			{/* Rate Limit Dashboard - only shown when authenticated */}
-			<ClaudeCodeRateLimitDashboard isAuthenticated={claudeCodeIsAuthenticated} />
-
-			{/* Model Picker */}
-			<ModelPicker
-				apiConfiguration={apiConfiguration}
-				setApiConfigurationField={setApiConfigurationField}
-				defaultModelId={claudeCodeDefaultModelId}
-				models={claudeCodeModels}
-				modelIdKey="apiModelId"
-				serviceName="Claude Code"
-				serviceUrl="https://claude.ai"
-				simplifySettings={simplifySettings}
-				hidePricing
-			/>
-		</div>
-	)
-}

+ 0 - 181
webview-ui/src/components/settings/providers/ClaudeCodeRateLimitDashboard.tsx

@@ -1,181 +0,0 @@
-import React, { useEffect, useState, useCallback } from "react"
-import type { ClaudeCodeRateLimitInfo } from "@roo-code/types"
-import { vscode } from "@src/utils/vscode"
-
-interface ClaudeCodeRateLimitDashboardProps {
-	isAuthenticated: boolean
-}
-
-/**
- * Formats a Unix timestamp reset time into a human-readable duration
- */
-function formatResetTime(resetTimestamp: number): string {
-	if (!resetTimestamp) return "N/A"
-
-	const now = Date.now() / 1000 // Current time in seconds
-	const diff = resetTimestamp - now
-
-	if (diff <= 0) return "Now"
-
-	const hours = Math.floor(diff / 3600)
-	const minutes = Math.floor((diff % 3600) / 60)
-
-	if (hours > 24) {
-		const days = Math.floor(hours / 24)
-		const remainingHours = hours % 24
-		return `${days}d ${remainingHours}h`
-	}
-
-	if (hours > 0) {
-		return `${hours}h ${minutes}m`
-	}
-
-	return `${minutes}m`
-}
-
-/**
- * Formats utilization as a percentage
- */
-function formatUtilization(utilization: number): string {
-	return `${(utilization * 100).toFixed(1)}%`
-}
-
-/**
- * Progress bar component for displaying usage
- */
-const UsageProgressBar: React.FC<{ utilization: number; label: string }> = ({ utilization, label }) => {
-	const percentage = Math.min(utilization * 100, 100)
-	const isWarning = percentage >= 70
-	const isCritical = percentage >= 90
-
-	return (
-		<div className="w-full">
-			<div className="text-xs text-vscode-descriptionForeground mb-1">{label}</div>
-			<div className="w-full bg-vscode-input-background rounded-sm h-2 overflow-hidden">
-				<div
-					className={`h-full transition-all duration-300 ${
-						isCritical
-							? "bg-vscode-errorForeground"
-							: isWarning
-								? "bg-vscode-editorWarning-foreground"
-								: "bg-vscode-button-background"
-					}`}
-					style={{ width: `${percentage}%` }}
-				/>
-			</div>
-		</div>
-	)
-}
-
-export const ClaudeCodeRateLimitDashboard: React.FC<ClaudeCodeRateLimitDashboardProps> = ({ isAuthenticated }) => {
-	const [rateLimits, setRateLimits] = useState<ClaudeCodeRateLimitInfo | null>(null)
-	const [isLoading, setIsLoading] = useState(false)
-	const [error, setError] = useState<string | null>(null)
-
-	const fetchRateLimits = useCallback(() => {
-		if (!isAuthenticated) {
-			setRateLimits(null)
-			setError(null)
-			return
-		}
-
-		setIsLoading(true)
-		setError(null)
-		vscode.postMessage({ type: "requestClaudeCodeRateLimits" })
-	}, [isAuthenticated])
-
-	useEffect(() => {
-		const handleMessage = (event: MessageEvent) => {
-			const message = event.data
-			if (message.type === "claudeCodeRateLimits") {
-				setIsLoading(false)
-				if (message.error) {
-					setError(message.error)
-					setRateLimits(null)
-				} else if (message.values) {
-					setRateLimits(message.values)
-					setError(null)
-				}
-			}
-		}
-
-		window.addEventListener("message", handleMessage)
-		return () => window.removeEventListener("message", handleMessage)
-	}, [])
-
-	// Fetch rate limits when authenticated
-	useEffect(() => {
-		if (isAuthenticated) {
-			fetchRateLimits()
-		}
-	}, [isAuthenticated, fetchRateLimits])
-
-	if (!isAuthenticated) {
-		return null
-	}
-
-	if (isLoading && !rateLimits) {
-		return (
-			<div className="bg-vscode-editor-background border border-vscode-panel-border rounded-md p-3">
-				<div className="text-sm text-vscode-descriptionForeground">Loading rate limits...</div>
-			</div>
-		)
-	}
-
-	if (error) {
-		return (
-			<div className="bg-vscode-editor-background border border-vscode-panel-border rounded-md p-3">
-				<div className="flex items-center justify-between">
-					<div className="text-sm text-vscode-errorForeground">Failed to load rate limits</div>
-					<button
-						onClick={fetchRateLimits}
-						className="text-xs text-vscode-textLink-foreground hover:text-vscode-textLink-activeForeground cursor-pointer bg-transparent border-none">
-						Retry
-					</button>
-				</div>
-			</div>
-		)
-	}
-
-	if (!rateLimits) {
-		return null
-	}
-
-	return (
-		<div className="bg-vscode-editor-background border border-vscode-panel-border rounded-md p-3">
-			<div className="mb-3">
-				<div className="text-sm font-medium text-vscode-foreground">Usage Limits</div>
-			</div>
-
-			<div className="space-y-3">
-				{/* 5-hour limit */}
-				<div className="flex flex-col gap-1">
-					<div className="flex items-center justify-between text-xs">
-						<span className="text-vscode-foreground">
-							Limit: {rateLimits.representativeClaim || "5-hour"}
-						</span>
-						<span className="text-vscode-descriptionForeground">
-							{formatUtilization(rateLimits.fiveHour.utilization)} used • resets in{" "}
-							{formatResetTime(rateLimits.fiveHour.resetTime)}
-						</span>
-					</div>
-					<UsageProgressBar utilization={rateLimits.fiveHour.utilization} label="" />
-				</div>
-
-				{/* Weekly limit (if available) */}
-				{rateLimits.weeklyUnified && rateLimits.weeklyUnified.utilization > 0 && (
-					<div className="flex flex-col gap-1">
-						<div className="flex items-center justify-between text-xs">
-							<span className="text-vscode-foreground">Weekly</span>
-							<span className="text-vscode-descriptionForeground">
-								{formatUtilization(rateLimits.weeklyUnified.utilization)} used • resets in{" "}
-								{formatResetTime(rateLimits.weeklyUnified.resetTime)}
-							</span>
-						</div>
-						<UsageProgressBar utilization={rateLimits.weeklyUnified.utilization} label="" />
-					</div>
-				)}
-			</div>
-		</div>
-	)
-}

+ 0 - 1
webview-ui/src/components/settings/providers/index.ts

@@ -2,7 +2,6 @@ export { Anthropic } from "./Anthropic"
 export { Bedrock } from "./Bedrock"
 export { Cerebras } from "./Cerebras"
 export { Chutes } from "./Chutes"
-export { ClaudeCode } from "./ClaudeCode"
 export { DeepSeek } from "./DeepSeek"
 export { Doubao } from "./Doubao"
 export { Gemini } from "./Gemini"

+ 0 - 1
webview-ui/src/components/settings/utils/__tests__/providerModelConfig.spec.ts

@@ -168,7 +168,6 @@ describe("providerModelConfig", () => {
 			expect(PROVIDERS_WITH_CUSTOM_MODEL_UI).toContain("ollama")
 			expect(PROVIDERS_WITH_CUSTOM_MODEL_UI).toContain("lmstudio")
 			expect(PROVIDERS_WITH_CUSTOM_MODEL_UI).toContain("vscode-lm")
-			expect(PROVIDERS_WITH_CUSTOM_MODEL_UI).toContain("claude-code")
 		})
 
 		it("does not include static providers using generic picker", () => {

+ 0 - 1
webview-ui/src/components/settings/utils/providerModelConfig.ts

@@ -132,7 +132,6 @@ export const PROVIDERS_WITH_CUSTOM_MODEL_UI: ProviderName[] = [
 	"requesty",
 	"unbound",
 	"deepinfra",
-	"claude-code",
 	"openai", // OpenAI Compatible
 	"litellm",
 	"io-intelligence",

+ 0 - 71
webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts

@@ -412,77 +412,6 @@ describe("useSelectedModel", () => {
 		})
 	})
 
-	describe("claude-code provider", () => {
-		it("should return claude-code model with correct model info", () => {
-			mockUseRouterModels.mockReturnValue({
-				data: {
-					openrouter: {},
-					requesty: {},
-					unbound: {},
-					litellm: {},
-					"io-intelligence": {},
-				},
-				isLoading: false,
-				isError: false,
-			} as any)
-
-			mockUseOpenRouterModelProviders.mockReturnValue({
-				data: {},
-				isLoading: false,
-				isError: false,
-			} as any)
-
-			const apiConfiguration: ProviderSettings = {
-				apiProvider: "claude-code",
-				apiModelId: "claude-sonnet-4-5", // Use valid claude-code model ID
-			}
-
-			const wrapper = createWrapper()
-			const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper })
-
-			expect(result.current.provider).toBe("claude-code")
-			expect(result.current.id).toBe("claude-sonnet-4-5")
-			expect(result.current.info).toBeDefined()
-			expect(result.current.info?.supportsImages).toBe(true) // Claude Code now supports images
-			expect(result.current.info?.supportsPromptCache).toBe(true) // Claude Code now supports prompt cache
-			// Verify it inherits other properties from claude-code models
-			expect(result.current.info?.maxTokens).toBe(32768)
-			expect(result.current.info?.contextWindow).toBe(200_000)
-		})
-
-		it("should use default claude-code model when no modelId is specified", () => {
-			mockUseRouterModels.mockReturnValue({
-				data: {
-					openrouter: {},
-					requesty: {},
-					unbound: {},
-					litellm: {},
-					"io-intelligence": {},
-				},
-				isLoading: false,
-				isError: false,
-			} as any)
-
-			mockUseOpenRouterModelProviders.mockReturnValue({
-				data: {},
-				isLoading: false,
-				isError: false,
-			} as any)
-
-			const apiConfiguration: ProviderSettings = {
-				apiProvider: "claude-code",
-			}
-
-			const wrapper = createWrapper()
-			const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper })
-
-			expect(result.current.provider).toBe("claude-code")
-			expect(result.current.id).toBe("claude-sonnet-4-5") // Default model
-			expect(result.current.info).toBeDefined()
-			expect(result.current.info?.supportsImages).toBe(true) // Claude Code now supports images
-		})
-	})
-
 	describe("bedrock provider with 1M context", () => {
 		beforeEach(() => {
 			mockUseRouterModels.mockReturnValue({

+ 0 - 10
webview-ui/src/components/ui/hooks/useSelectedModel.ts

@@ -19,8 +19,6 @@ import {
 	groqModels,
 	vscodeLlmModels,
 	vscodeLlmDefaultModelId,
-	claudeCodeModels,
-	normalizeClaudeCodeModelId,
 	openAiCodexModels,
 	sambaNovaModels,
 	doubaoModels,
@@ -316,14 +314,6 @@ function getSelectedModel({
 			const info = vscodeLlmModels[modelFamily as keyof typeof vscodeLlmModels]
 			return { id, info: { ...openAiModelInfoSaneDefaults, ...info, supportsImages: false } } // VSCode LM API currently doesn't support images.
 		}
-		case "claude-code": {
-			// Claude Code models extend anthropic models but with images and prompt caching disabled
-			// Normalize legacy model IDs to current canonical model IDs for backward compatibility
-			const rawId = apiConfiguration.apiModelId ?? defaultModelId
-			const normalizedId = normalizeClaudeCodeModelId(rawId)
-			const info = claudeCodeModels[normalizedId]
-			return { id: normalizedId, info: { ...openAiModelInfoSaneDefaults, ...info } }
-		}
 		case "cerebras": {
 			const id = apiConfiguration.apiModelId ?? defaultModelId
 			const info = cerebrasModels[id as keyof typeof cerebrasModels]