From 7e031265cc2f44f41508d2de138023e4482dc989 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 9 Jan 2026 17:39:11 +0000 Subject: [PATCH] feat: remove Claude Code provider completely - Remove claude-code integration directory and OAuth implementation - Delete claude-code type definitions, models, and tests - Remove claude-code API handlers and streaming client - Update core extension files (extension.ts, webviewMessageHandler, ClineProvider) - Remove claude-code from provider settings and schemas - Delete ClaudeCode and ClaudeCodeRateLimitDashboard UI components - Update UI hooks, constants, and ApiOptions - Remove claude-code i18n translations from all locales - Clean up claude-code message types and handlers --- packages/types/src/provider-settings.ts | 10 +- .../providers/__tests__/claude-code.spec.ts | 46 -- packages/types/src/providers/claude-code.ts | 160 ---- packages/types/src/providers/index.ts | 4 - src/api/index.ts | 3 - .../__tests__/claude-code-caching.spec.ts | 169 ---- .../providers/__tests__/claude-code.spec.ts | 597 -------------- src/api/providers/claude-code.ts | 389 --------- src/api/providers/index.ts | 1 - src/core/webview/ClineProvider.ts | 8 - src/core/webview/webviewMessageHandler.ts | 70 -- src/extension.ts | 4 - src/i18n/locales/ca/common.json | 7 - src/i18n/locales/de/common.json | 7 - src/i18n/locales/en/common.json | 7 - src/i18n/locales/es/common.json | 7 - src/i18n/locales/fr/common.json | 7 - src/i18n/locales/hi/common.json | 7 - src/i18n/locales/id/common.json | 7 - src/i18n/locales/it/common.json | 7 - src/i18n/locales/ja/common.json | 7 - src/i18n/locales/ko/common.json | 7 - src/i18n/locales/nl/common.json | 7 - src/i18n/locales/pl/common.json | 7 - src/i18n/locales/pt-BR/common.json | 7 - src/i18n/locales/ru/common.json | 7 - src/i18n/locales/tr/common.json | 7 - src/i18n/locales/vi/common.json | 7 - src/i18n/locales/zh-CN/common.json | 7 - src/i18n/locales/zh-TW/common.json | 7 - .../claude-code/__tests__/oauth.spec.ts | 235 ------ .../__tests__/streaming-client.spec.ts | 585 -------------- src/integrations/claude-code/oauth.ts | 638 --------------- .../claude-code/streaming-client.ts | 759 ------------------ src/shared/ExtensionMessage.ts | 2 - src/shared/WebviewMessage.ts | 3 - src/shared/checkExistApiConfig.ts | 4 +- webview-ui/src/components/chat/ChatRow.tsx | 46 +- .../src/components/settings/ApiOptions.tsx | 17 +- .../src/components/settings/constants.ts | 3 - .../settings/providers/ClaudeCode.tsx | 68 -- .../ClaudeCodeRateLimitDashboard.tsx | 181 ----- .../components/settings/providers/index.ts | 1 - .../components/ui/hooks/useSelectedModel.ts | 10 - webview-ui/src/i18n/locales/ca/chat.json | 1 - webview-ui/src/i18n/locales/ca/settings.json | 8 - webview-ui/src/i18n/locales/de/chat.json | 1 - webview-ui/src/i18n/locales/de/settings.json | 8 - webview-ui/src/i18n/locales/en/chat.json | 1 - webview-ui/src/i18n/locales/en/settings.json | 8 - webview-ui/src/i18n/locales/es/chat.json | 1 - webview-ui/src/i18n/locales/es/settings.json | 8 - webview-ui/src/i18n/locales/fr/chat.json | 1 - webview-ui/src/i18n/locales/fr/settings.json | 8 - webview-ui/src/i18n/locales/hi/chat.json | 1 - webview-ui/src/i18n/locales/hi/settings.json | 8 - webview-ui/src/i18n/locales/id/chat.json | 1 - webview-ui/src/i18n/locales/id/settings.json | 8 - webview-ui/src/i18n/locales/it/chat.json | 1 - webview-ui/src/i18n/locales/it/settings.json | 8 - webview-ui/src/i18n/locales/ja/chat.json | 1 - webview-ui/src/i18n/locales/ja/settings.json | 8 - webview-ui/src/i18n/locales/ko/chat.json | 1 - webview-ui/src/i18n/locales/ko/settings.json | 8 - webview-ui/src/i18n/locales/nl/chat.json | 1 - webview-ui/src/i18n/locales/nl/settings.json | 8 - webview-ui/src/i18n/locales/pl/chat.json | 1 - webview-ui/src/i18n/locales/pl/settings.json | 8 - webview-ui/src/i18n/locales/pt-BR/chat.json | 1 - .../src/i18n/locales/pt-BR/settings.json | 8 - webview-ui/src/i18n/locales/ru/chat.json | 1 - webview-ui/src/i18n/locales/ru/settings.json | 8 - webview-ui/src/i18n/locales/tr/chat.json | 1 - webview-ui/src/i18n/locales/tr/settings.json | 8 - webview-ui/src/i18n/locales/vi/chat.json | 1 - webview-ui/src/i18n/locales/vi/settings.json | 8 - webview-ui/src/i18n/locales/zh-CN/chat.json | 1 - .../src/i18n/locales/zh-CN/settings.json | 8 - webview-ui/src/i18n/locales/zh-TW/chat.json | 1 - .../src/i18n/locales/zh-TW/settings.json | 8 - 80 files changed, 25 insertions(+), 4276 deletions(-) delete mode 100644 packages/types/src/providers/__tests__/claude-code.spec.ts delete mode 100644 packages/types/src/providers/claude-code.ts delete mode 100644 src/api/providers/__tests__/claude-code-caching.spec.ts delete mode 100644 src/api/providers/__tests__/claude-code.spec.ts delete mode 100644 src/api/providers/claude-code.ts delete mode 100644 src/integrations/claude-code/__tests__/oauth.spec.ts delete mode 100644 src/integrations/claude-code/__tests__/streaming-client.spec.ts delete mode 100644 src/integrations/claude-code/oauth.ts delete mode 100644 src/integrations/claude-code/streaming-client.ts delete mode 100644 webview-ui/src/components/settings/providers/ClaudeCode.tsx delete mode 100644 webview-ui/src/components/settings/providers/ClaudeCodeRateLimitDashboard.tsx diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 6463a974991..b9aff958ee8 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -7,7 +7,6 @@ import { basetenModels, bedrockModels, cerebrasModels, - claudeCodeModels, deepSeekModels, doubaoModels, featherlessModels, @@ -122,7 +121,6 @@ export const providerNames = [ "bedrock", "baseten", "cerebras", - "claude-code", "doubao", "deepseek", "featherless", @@ -200,8 +198,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(), @@ -425,7 +421,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") })), @@ -466,7 +461,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, @@ -554,7 +548,6 @@ export const isTypicalProvider = (key: unknown): key is TypicalProvider => export const modelIdKeysByProvider: Record = { anthropic: "apiModelId", - "claude-code": "apiModelId", openrouter: "openRouterModelId", bedrock: "apiModelId", vertex: "apiModelId", @@ -593,7 +586,7 @@ export const modelIdKeysByProvider: Record = { */ // 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)) { @@ -640,7 +633,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", diff --git a/packages/types/src/providers/__tests__/claude-code.spec.ts b/packages/types/src/providers/__tests__/claude-code.spec.ts deleted file mode 100644 index 5ed66209a53..00000000000 --- a/packages/types/src/providers/__tests__/claude-code.spec.ts +++ /dev/null @@ -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") - }) -}) diff --git a/packages/types/src/providers/claude-code.ts b/packages/types/src/providers/claude-code.ts deleted file mode 100644 index 28863675d07..00000000000 --- a/packages/types/src/providers/claude-code.ts +++ /dev/null @@ -1,160 +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, - supportsNativeTools: true, - defaultToolProtocol: "native", - 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, - supportsNativeTools: true, - defaultToolProtocol: "native", - 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, - supportsNativeTools: true, - defaultToolProtocol: "native", - supportsReasoningEffort: ["disable", "low", "medium", "high"], - reasoningEffort: "medium", - description: "Claude Opus 4.5 - Most capable with thinking", - }, -} as const satisfies Record - -// 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 diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index a08d673e221..eeb1e514954 100644 --- a/packages/types/src/providers/index.ts +++ b/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" @@ -37,7 +36,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" @@ -123,8 +121,6 @@ export function getProviderDefaultModelId( return deepInfraDefaultModelId case "vscode-lm": return vscodeLlmDefaultModelId - case "claude-code": - return claudeCodeDefaultModelId case "cerebras": return cerebrasDefaultModelId case "sambanova": diff --git a/src/api/index.ts b/src/api/index.ts index 2ee882ad72c..088522f2436 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -28,7 +28,6 @@ import { HuggingFaceHandler, ChutesHandler, LiteLLMHandler, - ClaudeCodeHandler, QwenCodeHandler, SambaNovaHandler, IOIntelligenceHandler, @@ -122,8 +121,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": diff --git a/src/api/providers/__tests__/claude-code-caching.spec.ts b/src/api/providers/__tests__/claude-code-caching.spec.ts deleted file mode 100644 index a0996ab244b..00000000000 --- a/src/api/providers/__tests__/claude-code-caching.spec.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - 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) - }) -}) diff --git a/src/api/providers/__tests__/claude-code.spec.ts b/src/api/providers/__tests__/claude-code.spec.ts deleted file mode 100644 index 5b5bdca65ae..00000000000 --- a/src/api/providers/__tests__/claude-code.spec.ts +++ /dev/null @@ -1,597 +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 { - // 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({ - 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: undefined, - toolChoice: undefined, - 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 { - // 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({ - accessToken: "test-access-token", - model: "claude-sonnet-4-5", - systemPrompt, - messages, - maxTokens: 32768, // model maxTokens from claudeCodeModels definition - thinking: { type: "disabled" }, - tools: undefined, - toolChoice: undefined, - 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 { - // 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({ - 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: undefined, - toolChoice: undefined, - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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("test@example.com") - - // Mock async generator that yields text chunks - const mockGenerator = async function* (): AsyncGenerator { - 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("test@example.com") - - // Mock empty async generator - const mockGenerator = async function* (): AsyncGenerator { - 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("test@example.com") - - // Mock async generator that yields an error - const mockGenerator = async function* (): AsyncGenerator { - 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("test@example.com") - - // Mock async generator that only yields usage - const mockGenerator = async function* (): AsyncGenerator { - 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("test@example.com") - - const mockGenerator = async function* (): AsyncGenerator { - 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 - }), - ) - }) - }) -}) diff --git a/src/api/providers/claude-code.ts b/src/api/providers/claude-code.ts deleted file mode 100644 index f2bccc329c7..00000000000 --- a/src/api/providers/claude-code.ts +++ /dev/null @@ -1,389 +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) - - // Convert OpenAI tools to Anthropic format if provided and protocol is native - // Exclude tools when tool_choice is "none" since that means "don't use tools" - const shouldIncludeNativeTools = - metadata?.tools && - metadata.tools.length > 0 && - metadata?.toolProtocol !== "xml" && - metadata?.tool_choice !== "none" - - const anthropicTools = shouldIncludeNativeTools ? convertOpenAIToolsToAnthropic(metadata.tools!) : undefined - - const anthropicToolChoice = shouldIncludeNativeTools - ? convertOpenAIToolChoice(metadata.tool_choice, metadata.parallelToolCalls) - : undefined - - // 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 { - 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 { - // 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 - } -} diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index fe9388962f0..8f543f64db2 100644 --- a/src/api/providers/index.ts +++ b/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" diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index a2a400660e7..723a266e22c 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2157,14 +2157,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 - } - })(), debug: vscode.workspace.getConfiguration(Package.name).get("debug", false), } } diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 0df014b49ab..2fd9fa6d394 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2340,45 +2340,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 "rooCloudManualUrl": { try { if (!message.text) { @@ -3160,37 +3121,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 "openDebugApiHistory": case "openDebugUiHistory": { const currentTask = provider.getCurrentTask() diff --git a/src/extension.ts b/src/extension.ts index 76f02af6de2..3fe68da1460 100644 --- a/src/extension.ts +++ b/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 { McpServerManager } from "./services/mcp/McpServerManager" import { CodeIndexManager } from "./services/code-index/manager" import { MdmService } from "./services/mdm/MdmService" @@ -101,9 +100,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)) - // Get default commands from configuration. const defaultCommands = vscode.workspace.getConfiguration(Package.name).get("allowedCommands") || [] diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index ecc60195bd5..fb70ca94eca 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -89,13 +89,6 @@ "command_already_exists": "L'ordre \"{{commandName}}\" ja existeix", "create_command_failed": "Error en crear l'ordre", "command_template_content": "---\ndescription: \"Breu descripció del que fa aquesta ordre\"\n---\n\nAquesta és una nova ordre slash. Edita aquest fitxer per personalitzar el comportament de l'ordre.", - "claudeCode": { - "processExited": "El procés Claude Code ha sortit amb codi {{exitCode}}.", - "errorOutput": "Sortida d'error: {{output}}", - "processExitedWithError": "El procés Claude Code ha sortit amb codi {{exitCode}}. Sortida d'error: {{output}}", - "stoppedWithReason": "Claude Code s'ha aturat per la raó: {{reason}}", - "apiKeyModelPlanMismatch": "Les claus API i els plans de subscripció permeten models diferents. Assegura't que el model seleccionat estigui inclòs al teu pla." - }, "message": { "no_active_task_to_delete": "No hi ha cap tasca activa de la qual eliminar missatges", "invalid_timestamp_for_deletion": "Marca de temps del missatge no vàlida per a l'eliminació", diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 73c8213ce00..7935cc506e8 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -86,13 +86,6 @@ "command_already_exists": "Befehl \"{{commandName}}\" existiert bereits", "create_command_failed": "Fehler beim Erstellen des Befehls", "command_template_content": "---\ndescription: \"Kurze Beschreibung dessen, was dieser Befehl macht\"\n---\n\nDies ist ein neuer Slash-Befehl. Bearbeite diese Datei, um das Befehlsverhalten anzupassen.", - "claudeCode": { - "processExited": "Claude Code Prozess wurde mit Code {{exitCode}} beendet.", - "errorOutput": "Fehlerausgabe: {{output}}", - "processExitedWithError": "Claude Code Prozess wurde mit Code {{exitCode}} beendet. Fehlerausgabe: {{output}}", - "stoppedWithReason": "Claude Code wurde mit Grund gestoppt: {{reason}}", - "apiKeyModelPlanMismatch": "API-Schlüssel und Abonnement-Pläne erlauben verschiedene Modelle. Stelle sicher, dass das ausgewählte Modell in deinem Plan enthalten ist." - }, "message": { "no_active_task_to_delete": "Keine aktive Aufgabe, aus der Nachrichten gelöscht werden können", "invalid_timestamp_for_deletion": "Ungültiger Nachrichten-Zeitstempel zum Löschen", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 2d17e13feab..6ac9c45c2f1 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -86,13 +86,6 @@ "command_already_exists": "Command \"{{commandName}}\" already exists", "create_command_failed": "Failed to create command", "command_template_content": "---\ndescription: \"Brief description of what this command does\"\n---\n\nThis is a new slash command. Edit this file to customize the command behavior.", - "claudeCode": { - "processExited": "Claude Code process exited with code {{exitCode}}.", - "errorOutput": "Error output: {{output}}", - "processExitedWithError": "Claude Code process exited with code {{exitCode}}. Error output: {{output}}", - "stoppedWithReason": "Claude Code stopped with reason: {{reason}}", - "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan." - }, "message": { "no_active_task_to_delete": "No active task to delete messages from", "invalid_timestamp_for_deletion": "Invalid message timestamp for deletion", diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 9cc23f5bb1c..cddc9dfa120 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -86,13 +86,6 @@ "command_already_exists": "El comando \"{{commandName}}\" ya existe", "create_command_failed": "Error al crear comando", "command_template_content": "---\ndescription: \"Breve descripción de lo que hace este comando\"\n---\n\nEste es un nuevo comando slash. Edita este archivo para personalizar el comportamiento del comando.", - "claudeCode": { - "processExited": "El proceso de Claude Code terminó con código {{exitCode}}.", - "errorOutput": "Salida de error: {{output}}", - "processExitedWithError": "El proceso de Claude Code terminó con código {{exitCode}}. Salida de error: {{output}}", - "stoppedWithReason": "Claude Code se detuvo por la razón: {{reason}}", - "apiKeyModelPlanMismatch": "Las claves API y los planes de suscripción permiten diferentes modelos. Asegúrate de que el modelo seleccionado esté incluido en tu plan." - }, "message": { "no_active_task_to_delete": "No hay tarea activa de la cual eliminar mensajes", "invalid_timestamp_for_deletion": "Marca de tiempo del mensaje no válida para eliminación", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index ce991866451..e700d437909 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -86,13 +86,6 @@ "command_already_exists": "La commande \"{{commandName}}\" existe déjà", "create_command_failed": "Échec de la création de la commande", "command_template_content": "---\ndescription: \"Brève description de ce que fait cette commande\"\n---\n\nCeci est une nouvelle commande slash. Modifie ce fichier pour personnaliser le comportement de la commande.", - "claudeCode": { - "processExited": "Le processus Claude Code s'est terminé avec le code {{exitCode}}.", - "errorOutput": "Sortie d'erreur : {{output}}", - "processExitedWithError": "Le processus Claude Code s'est terminé avec le code {{exitCode}}. Sortie d'erreur : {{output}}", - "stoppedWithReason": "Claude Code s'est arrêté pour la raison : {{reason}}", - "apiKeyModelPlanMismatch": "Les clés API et les plans d'abonnement permettent différents modèles. Assurez-vous que le modèle sélectionné est inclus dans votre plan." - }, "message": { "no_active_task_to_delete": "Aucune tâche active pour supprimer des messages", "invalid_timestamp_for_deletion": "Horodatage du message invalide pour la suppression", diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index 9cf2456f7c3..73f0a472067 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -86,13 +86,6 @@ "command_already_exists": "कमांड \"{{commandName}}\" पहले से मौजूद है", "create_command_failed": "कमांड बनाने में विफल", "command_template_content": "---\ndescription: \"इस कमांड के कार्य का संक्षिप्त विवरण\"\n---\n\nयह एक नया स्लैश कमांड है। कमांड व्यवहार को कस्टमाइज़ करने के लिए इस फ़ाइल को संपादित करें।", - "claudeCode": { - "processExited": "Claude Code प्रक्रिया कोड {{exitCode}} के साथ समाप्त हुई।", - "errorOutput": "त्रुटि आउटपुट: {{output}}", - "processExitedWithError": "Claude Code प्रक्रिया कोड {{exitCode}} के साथ समाप्त हुई। त्रुटि आउटपुट: {{output}}", - "stoppedWithReason": "Claude Code इस कारण से रुका: {{reason}}", - "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan." - }, "message": { "no_active_task_to_delete": "संदेशों को हटाने के लिए कोई सक्रिय कार्य नहीं", "invalid_timestamp_for_deletion": "हटाने के लिए अमान्य संदेश टाइमस्टैम्प", diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index 295f843b8a6..2c5cceea6cc 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -86,13 +86,6 @@ "command_already_exists": "Perintah \"{{commandName}}\" sudah ada", "create_command_failed": "Gagal membuat perintah", "command_template_content": "---\ndescription: \"Deskripsi singkat tentang fungsi perintah ini\"\n---\n\nIni adalah perintah slash baru. Edit file ini untuk menyesuaikan perilaku perintah.", - "claudeCode": { - "processExited": "Proses Claude Code keluar dengan kode {{exitCode}}.", - "errorOutput": "Output error: {{output}}", - "processExitedWithError": "Proses Claude Code keluar dengan kode {{exitCode}}. Output error: {{output}}", - "stoppedWithReason": "Claude Code berhenti karena alasan: {{reason}}", - "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan." - }, "message": { "no_active_task_to_delete": "Tidak ada tugas aktif untuk menghapus pesan", "invalid_timestamp_for_deletion": "Timestamp pesan tidak valid untuk penghapusan", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index 237fc4faa30..33b81a06e7d 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -86,13 +86,6 @@ "command_already_exists": "Il comando \"{{commandName}}\" esiste già", "create_command_failed": "Errore nella creazione del comando", "command_template_content": "---\ndescription: \"Breve descrizione di cosa fa questo comando\"\n---\n\nQuesto è un nuovo comando slash. Modifica questo file per personalizzare il comportamento del comando.", - "claudeCode": { - "processExited": "Il processo Claude Code è terminato con codice {{exitCode}}.", - "errorOutput": "Output di errore: {{output}}", - "processExitedWithError": "Il processo Claude Code è terminato con codice {{exitCode}}. Output di errore: {{output}}", - "stoppedWithReason": "Claude Code si è fermato per il motivo: {{reason}}", - "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan." - }, "message": { "no_active_task_to_delete": "Nessuna attività attiva da cui eliminare messaggi", "invalid_timestamp_for_deletion": "Timestamp del messaggio non valido per l'eliminazione", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index a2da92cfac8..ee103c31729 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -86,13 +86,6 @@ "command_already_exists": "コマンド \"{{commandName}}\" は既に存在します", "create_command_failed": "コマンドの作成に失敗しました", "command_template_content": "---\ndescription: \"このコマンドが何をするかの簡潔な説明\"\n---\n\nこれは新しいスラッシュコマンドです。このファイルを編集してコマンドの動作をカスタマイズしてください。", - "claudeCode": { - "processExited": "Claude Code プロセスがコード {{exitCode}} で終了しました。", - "errorOutput": "エラー出力:{{output}}", - "processExitedWithError": "Claude Code プロセスがコード {{exitCode}} で終了しました。エラー出力:{{output}}", - "stoppedWithReason": "Claude Code が理由により停止しました:{{reason}}", - "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan." - }, "message": { "no_active_task_to_delete": "メッセージを削除するアクティブなタスクがありません", "invalid_timestamp_for_deletion": "削除用のメッセージタイムスタンプが無効です", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index d2d696510dc..1d5397920a2 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -86,13 +86,6 @@ "command_already_exists": "명령 \"{{commandName}}\"이(가) 이미 존재합니다", "create_command_failed": "명령 생성에 실패했습니다", "command_template_content": "---\ndescription: \"이 명령이 수행하는 작업에 대한 간단한 설명\"\n---\n\n이것은 새로운 슬래시 명령입니다. 이 파일을 편집하여 명령 동작을 사용자 정의하세요.", - "claudeCode": { - "processExited": "Claude Code 프로세스가 코드 {{exitCode}}로 종료되었습니다.", - "errorOutput": "오류 출력: {{output}}", - "processExitedWithError": "Claude Code 프로세스가 코드 {{exitCode}}로 종료되었습니다. 오류 출력: {{output}}", - "stoppedWithReason": "Claude Code가 다음 이유로 중지되었습니다: {{reason}}", - "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan." - }, "message": { "no_active_task_to_delete": "메시지를 삭제할 활성 작업이 없습니다", "invalid_timestamp_for_deletion": "삭제를 위한 메시지 타임스탬프가 유효하지 않습니다", diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index 91a3a94401d..534269a6866 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -86,13 +86,6 @@ "command_already_exists": "Opdracht \"{{commandName}}\" bestaat al", "create_command_failed": "Kan opdracht niet aanmaken", "command_template_content": "---\ndescription: \"Korte beschrijving van wat deze opdracht doet\"\n---\n\nDit is een nieuwe slash-opdracht. Bewerk dit bestand om het opdrachtgedrag aan te passen.", - "claudeCode": { - "processExited": "Claude Code proces beëindigd met code {{exitCode}}.", - "errorOutput": "Foutuitvoer: {{output}}", - "processExitedWithError": "Claude Code proces beëindigd met code {{exitCode}}. Foutuitvoer: {{output}}", - "stoppedWithReason": "Claude Code gestopt om reden: {{reason}}", - "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan." - }, "message": { "no_active_task_to_delete": "Geen actieve taak om berichten uit te verwijderen", "invalid_timestamp_for_deletion": "Ongeldig bericht tijdstempel voor verwijdering", diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index 7247573cd37..5d589b9b083 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -86,13 +86,6 @@ "command_already_exists": "Polecenie \"{{commandName}}\" już istnieje", "create_command_failed": "Nie udało się utworzyć polecenia", "command_template_content": "---\ndescription: \"Krótki opis tego, co robi to polecenie\"\n---\n\nTo jest nowe polecenie slash. Edytuj ten plik, aby dostosować zachowanie polecenia.", - "claudeCode": { - "processExited": "Proces Claude Code zakończył się kodem {{exitCode}}.", - "errorOutput": "Wyjście błędu: {{output}}", - "processExitedWithError": "Proces Claude Code zakończył się kodem {{exitCode}}. Wyjście błędu: {{output}}", - "stoppedWithReason": "Claude Code zatrzymał się z powodu: {{reason}}", - "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan." - }, "message": { "no_active_task_to_delete": "Brak aktywnego zadania do usunięcia wiadomości", "invalid_timestamp_for_deletion": "Nieprawidłowy znacznik czasu wiadomości do usunięcia", diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index bf53a9764bc..198077b1b22 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -90,13 +90,6 @@ "command_already_exists": "Comando \"{{commandName}}\" já existe", "create_command_failed": "Falha ao criar comando", "command_template_content": "---\ndescription: \"Breve descrição do que este comando faz\"\n---\n\nEste é um novo comando slash. Edite este arquivo para personalizar o comportamento do comando.", - "claudeCode": { - "processExited": "O processo Claude Code saiu com código {{exitCode}}.", - "errorOutput": "Saída de erro: {{output}}", - "processExitedWithError": "O processo Claude Code saiu com código {{exitCode}}. Saída de erro: {{output}}", - "stoppedWithReason": "Claude Code parou pela razão: {{reason}}", - "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan." - }, "message": { "no_active_task_to_delete": "Nenhuma tarefa ativa para excluir mensagens", "invalid_timestamp_for_deletion": "Timestamp da mensagem inválido para exclusão", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index dedbe8450e9..8ca4230592d 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -86,13 +86,6 @@ "command_already_exists": "Команда \"{{commandName}}\" уже существует", "create_command_failed": "Не удалось создать команду", "command_template_content": "---\ndescription: \"Краткое описание того, что делает эта команда\"\n---\n\nЭто новая slash-команда. Отредактируйте этот файл, чтобы настроить поведение команды.", - "claudeCode": { - "processExited": "Процесс Claude Code завершился с кодом {{exitCode}}.", - "errorOutput": "Вывод ошибки: {{output}}", - "processExitedWithError": "Процесс Claude Code завершился с кодом {{exitCode}}. Вывод ошибки: {{output}}", - "stoppedWithReason": "Claude Code остановился по причине: {{reason}}", - "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan." - }, "message": { "no_active_task_to_delete": "Нет активной задачи для удаления сообщений", "invalid_timestamp_for_deletion": "Недействительная временная метка сообщения для удаления", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 4cbb45210dc..5a59f1c3a4d 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -86,13 +86,6 @@ "command_already_exists": "\"{{commandName}}\" komutu zaten mevcut", "create_command_failed": "Komut oluşturulamadı", "command_template_content": "---\ndescription: \"Bu komutun ne yaptığının kısa açıklaması\"\n---\n\nBu yeni bir slash komutudur. Komut davranışını özelleştirmek için bu dosyayı düzenleyin.", - "claudeCode": { - "processExited": "Claude Code işlemi {{exitCode}} koduyla çıktı.", - "errorOutput": "Hata çıktısı: {{output}}", - "processExitedWithError": "Claude Code işlemi {{exitCode}} koduyla çıktı. Hata çıktısı: {{output}}", - "stoppedWithReason": "Claude Code şu nedenle durdu: {{reason}}", - "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan." - }, "message": { "no_active_task_to_delete": "Mesaj silinecek aktif görev yok", "invalid_timestamp_for_deletion": "Silme için geçersiz mesaj zaman damgası", diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 659b0ad0afe..890750c90c7 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -86,13 +86,6 @@ "command_already_exists": "Lệnh \"{{commandName}}\" đã tồn tại", "create_command_failed": "Không thể tạo lệnh", "command_template_content": "---\ndescription: \"Mô tả ngắn gọn về chức năng của lệnh này\"\n---\n\nĐây là một lệnh slash mới. Chỉnh sửa tệp này để tùy chỉnh hành vi của lệnh.", - "claudeCode": { - "processExited": "Tiến trình Claude Code thoát với mã {{exitCode}}.", - "errorOutput": "Đầu ra lỗi: {{output}}", - "processExitedWithError": "Tiến trình Claude Code thoát với mã {{exitCode}}. Đầu ra lỗi: {{output}}", - "stoppedWithReason": "Claude Code dừng lại vì lý do: {{reason}}", - "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan." - }, "message": { "no_active_task_to_delete": "Không có nhiệm vụ hoạt động để xóa tin nhắn", "invalid_timestamp_for_deletion": "Dấu thời gian tin nhắn không hợp lệ để xóa", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index fcc054d8a7d..c264bc526ab 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -91,13 +91,6 @@ "command_already_exists": "命令 \"{{commandName}}\" 已存在", "create_command_failed": "创建命令失败", "command_template_content": "---\ndescription: \"此命令功能的简要描述\"\n---\n\n这是一个新的斜杠命令。编辑此文件以自定义命令行为。", - "claudeCode": { - "processExited": "Claude Code 进程退出,退出码:{{exitCode}}。", - "errorOutput": "错误输出:{{output}}", - "processExitedWithError": "Claude Code 进程退出,退出码:{{exitCode}}。错误输出:{{output}}", - "stoppedWithReason": "Claude Code 停止,原因:{{reason}}", - "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan." - }, "message": { "no_active_task_to_delete": "没有可删除消息的活跃任务", "invalid_timestamp_for_deletion": "删除操作的消息时间戳无效", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index b8a6fc42a1b..bbc676ac87e 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -85,13 +85,6 @@ "command_already_exists": "指令 \"{{commandName}}\" 已存在", "create_command_failed": "建立指令失敗", "command_template_content": "---\ndescription: \"此指令功能的簡要描述\"\n---\n\n這是一個新的斜線指令。編輯此檔案以自訂指令行為。", - "claudeCode": { - "processExited": "Claude Code 程序退出,退出碼:{{exitCode}}。", - "errorOutput": "錯誤輸出:{{output}}", - "processExitedWithError": "Claude Code 程序退出,退出碼:{{exitCode}}。錯誤輸出:{{output}}", - "stoppedWithReason": "Claude Code 停止,原因:{{reason}}", - "apiKeyModelPlanMismatch": "API 金鑰和訂閱方案允許不同的模型。請確保所選模型包含在您的方案中。" - }, "message": { "no_active_task_to_delete": "沒有可刪除訊息的活躍工作", "invalid_timestamp_for_deletion": "刪除操作的訊息時間戳無效", diff --git a/src/integrations/claude-code/__tests__/oauth.spec.ts b/src/integrations/claude-code/__tests__/oauth.spec.ts deleted file mode 100644 index 7de75ec5292..00000000000 --- a/src/integrations/claude-code/__tests__/oauth.spec.ts +++ /dev/null @@ -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 = "test@example.com" - 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("user1@example.com") - const userId2 = generateUserId("user2@example.com") - - 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: "test@example.com", - } - - const refreshed = await refreshAccessToken(creds) - expect(refreshed.access_token).toBe("new-access") - expect(refreshed.refresh_token).toBe("old-refresh") - expect(refreshed.email).toBe("test@example.com") - }) - }) -}) diff --git a/src/integrations/claude-code/__tests__/streaming-client.spec.ts b/src/integrations/claude-code/__tests__/streaming-client.spec.ts deleted file mode 100644 index 8ccb108827d..00000000000 --- a/src/integrations/claude-code/__tests__/streaming-client.spec.ts +++ /dev/null @@ -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) => { - 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, - }) - }) - }) -}) diff --git a/src/integrations/claude-code/oauth.ts b/src/integrations/claude-code/oauth.ts deleted file mode 100644 index 5d7a929e1cc..00000000000 --- a/src/integrations/claude-code/oauth.ts +++ /dev/null @@ -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 - -// 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 - const errorField = obj.error - - const errorCode: string | undefined = - typeof errorField === "string" - ? errorField - : errorField && - typeof errorField === "object" && - typeof (errorField as Record).type === "string" - ? ((errorField as Record).type as string) - : undefined - - const errorDescription = obj.error_description - const errorMessageFromError = - errorField && typeof errorField === "object" ? (errorField as Record).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__account__session_ - */ -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 { - 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 { - 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 | 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 { - 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 { - 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 { - 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 { - 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 { - // 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 { - if (!this.credentials) { - await this.loadCredentials() - } - return this.credentials?.email || null - } - - /** - * Check if the user is authenticated - */ - async isAuthenticated(): Promise { - 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 { - 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(` - - - -Authentication Successful - - -

✓ Authentication Successful

-

You can close this window and return to VS Code.

- - -`) - - 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() diff --git a/src/integrations/claude-code/streaming-client.ts b/src/integrations/claude-code/streaming-client.ts deleted file mode 100644 index b864995f2cd..00000000000 --- a/src/integrations/claude-code/streaming-client.ts +++ /dev/null @@ -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 { - 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 = { - 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 = { - 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 | null - - if (!eventData) { - continue - } - - switch (event.event) { - case "message_start": { - const message = eventData.message as Record - if (!message) { - break - } - const usage = message.usage as Record | 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 - 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 - 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 | 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 - 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 { - // 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 = { - 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) -} diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 2eec4cb6c88..fd3ecd8db01 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -130,7 +130,6 @@ export interface ExtensionMessage { | "interactionRequired" | "browserSessionUpdate" | "browserSessionNavigate" - | "claudeCodeRateLimits" | "customToolsResult" text?: string payload?: any // Add a generic payload for now, can refine later @@ -359,7 +358,6 @@ export type ExtensionState = Pick< remoteControlEnabled: boolean taskSyncEnabled: boolean featureRoomoteControlEnabled: boolean - claudeCodeIsAuthenticated?: boolean debug?: boolean } diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 4c3e321dea8..ffaaa8a2d0c 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -126,8 +126,6 @@ export interface WebviewMessage { | "cloudLandingPageSignIn" | "rooCloudSignOut" | "rooCloudManualUrl" - | "claudeCodeSignIn" - | "claudeCodeSignOut" | "switchOrganization" | "condenseTaskContextRequest" | "requestIndexingStatus" @@ -178,7 +176,6 @@ export interface WebviewMessage { | "openDebugApiHistory" | "openDebugUiHistory" | "downloadErrorDiagnostics" - | "requestClaudeCodeRateLimits" | "refreshCustomTools" text?: string editedMessageContent?: string diff --git a/src/shared/checkExistApiConfig.ts b/src/shared/checkExistApiConfig.ts index 37b468ce1ac..2a633f4eee2 100644 --- a/src/shared/checkExistApiConfig.ts +++ b/src/shared/checkExistApiConfig.ts @@ -5,8 +5,8 @@ export function checkExistKey(config: ProviderSettings | undefined) { return false } - // Special case for fake-ai, claude-code, qwen-code, and roo providers which don't need any configuration. - if (config.apiProvider && ["fake-ai", "claude-code", "qwen-code", "roo"].includes(config.apiProvider)) { + // Special case for fake-ai, qwen-code, and roo providers which don't need any configuration. + if (config.apiProvider && ["fake-ai", "qwen-code", "roo"].includes(config.apiProvider)) { return true } diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index ef2231b26d1..fda06c06e56 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -1104,36 +1104,30 @@ 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:support@roocode.com?subject=Unknown API Error" - } - } 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") docsURL = "mailto:support@roocode.com?subject=Unknown API Error" } + } else if (message.text.indexOf("Connection error") === 0) { + body = t("chat:apiRequest.errorMessage.connection") + } else { + // Non-HTTP-status-code error message - store full text as errorDetails + body = t("chat:apiRequest.errorMessage.unknown") + docsURL = "mailto:support@roocode.com?subject=Unknown API Error" } // This isn't pretty, but since the retry logic happens at a lower level diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 8e2c117e7c0..8b3dec656ff 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -15,7 +15,6 @@ import { openAiNativeDefaultModelId, anthropicDefaultModelId, doubaoDefaultModelId, - claudeCodeDefaultModelId, qwenCodeDefaultModelId, geminiDefaultModelId, deepSeekDefaultModelId, @@ -69,7 +68,6 @@ import { Bedrock, Cerebras, Chutes, - ClaudeCode, DeepSeek, Doubao, Gemini, @@ -138,7 +136,7 @@ const ApiOptions = ({ setErrorMessage, }: ApiOptionsProps) => { const { t } = useAppTranslation() - const { organizationAllowList, cloudIsAuthenticated, claudeCodeIsAuthenticated } = useExtensionState() + const { organizationAllowList, cloudIsAuthenticated } = useExtensionState() const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => { const headers = apiConfiguration?.openAiHeaders || {} @@ -341,7 +339,6 @@ const ApiOptions = ({ litellm: { field: "litellmModelId", default: litellmDefaultModelId }, anthropic: { field: "apiModelId", default: anthropicDefaultModelId }, cerebras: { field: "apiModelId", default: cerebrasDefaultModelId }, - "claude-code": { field: "apiModelId", default: claudeCodeDefaultModelId }, "qwen-code": { field: "apiModelId", default: qwenCodeDefaultModelId }, "openai-native": { field: "apiModelId", default: openAiNativeDefaultModelId }, gemini: { field: "apiModelId", default: geminiDefaultModelId }, @@ -554,15 +551,6 @@ const ApiOptions = ({ /> )} - {selectedProvider === "claude-code" && ( - - )} - {selectedProvider === "openai-native" && ( )} - {/* Skip generic model picker for claude-code since it has its own in ClaudeCode.tsx */} - {selectedProviderModels.length > 0 && selectedProvider !== "claude-code" && ( + {selectedProviderModels.length > 0 && ( <>
diff --git a/webview-ui/src/components/settings/constants.ts b/webview-ui/src/components/settings/constants.ts index d35ec547b4d..7f519f2549f 100644 --- a/webview-ui/src/components/settings/constants.ts +++ b/webview-ui/src/components/settings/constants.ts @@ -4,7 +4,6 @@ import { anthropicModels, bedrockModels, cerebrasModels, - claudeCodeModels, deepSeekModels, moonshotModels, geminiModels, @@ -25,7 +24,6 @@ import { export const MODELS_BY_PROVIDER: Partial>> = { anthropic: anthropicModels, - "claude-code": claudeCodeModels, bedrock: bedrockModels, cerebras: cerebrasModels, deepseek: deepSeekModels, @@ -50,7 +48,6 @@ export const PROVIDERS = [ { value: "openrouter", label: "OpenRouter" }, { value: "deepinfra", label: "DeepInfra" }, { value: "anthropic", label: "Anthropic" }, - { value: "claude-code", label: "Claude Code" }, { value: "cerebras", label: "Cerebras" }, { value: "gemini", label: "Google Gemini" }, { value: "doubao", label: "Doubao" }, diff --git a/webview-ui/src/components/settings/providers/ClaudeCode.tsx b/webview-ui/src/components/settings/providers/ClaudeCode.tsx deleted file mode 100644 index 87072a9b976..00000000000 --- a/webview-ui/src/components/settings/providers/ClaudeCode.tsx +++ /dev/null @@ -1,68 +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 = ({ - apiConfiguration, - setApiConfigurationField, - simplifySettings, - claudeCodeIsAuthenticated = false, -}) => { - const { t } = useAppTranslation() - - return ( -
- {/* Authentication Section */} -
- {claudeCodeIsAuthenticated ? ( -
- -
- ) : ( - - )} -
- - {/* Rate Limit Dashboard - only shown when authenticated */} - - - {/* Model Picker */} - -
- ) -} diff --git a/webview-ui/src/components/settings/providers/ClaudeCodeRateLimitDashboard.tsx b/webview-ui/src/components/settings/providers/ClaudeCodeRateLimitDashboard.tsx deleted file mode 100644 index 9b152c27177..00000000000 --- a/webview-ui/src/components/settings/providers/ClaudeCodeRateLimitDashboard.tsx +++ /dev/null @@ -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 ( -
-
{label}
-
-
-
-
- ) -} - -export const ClaudeCodeRateLimitDashboard: React.FC = ({ isAuthenticated }) => { - const [rateLimits, setRateLimits] = useState(null) - const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState(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 ( -
-
Loading rate limits...
-
- ) - } - - if (error) { - return ( -
-
-
Failed to load rate limits
- -
-
- ) - } - - if (!rateLimits) { - return null - } - - return ( -
-
-
Usage Limits
-
- -
- {/* 5-hour limit */} -
-
- - Limit: {rateLimits.representativeClaim || "5-hour"} - - - {formatUtilization(rateLimits.fiveHour.utilization)} used • resets in{" "} - {formatResetTime(rateLimits.fiveHour.resetTime)} - -
- -
- - {/* Weekly limit (if available) */} - {rateLimits.weeklyUnified && rateLimits.weeklyUnified.utilization > 0 && ( -
-
- Weekly - - {formatUtilization(rateLimits.weeklyUnified.utilization)} used • resets in{" "} - {formatResetTime(rateLimits.weeklyUnified.resetTime)} - -
- -
- )} -
-
- ) -} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index 0dd722a5220..d7433694ac7 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/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" diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 68f54ab0f3c..e256e4fee93 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -17,8 +17,6 @@ import { groqModels, vscodeLlmModels, vscodeLlmDefaultModelId, - claudeCodeModels, - normalizeClaudeCodeModelId, sambaNovaModels, doubaoModels, internationalZAiModels, @@ -333,14 +331,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] diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index b583cf40d3c..c892eca823c 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -151,7 +151,6 @@ "500": "Error del servidor del proveïdor. Quelcom va malament del costat del proveïdor, no hi ha res de malament amb la teva sol·licitud.", "unknown": "Error API desconegut. Si us plau contacta amb el suport de Roo Code.", "connection": "Error de connexió. Assegureu-vos que teniu una connexió a Internet funcional.", - "claudeCodeNotAuthenticated": "Has d'iniciar sessió per utilitzar Claude Code. Vés a Configuració i fes clic a \"Iniciar sessió a Claude Code\" per autenticar-te." } }, "checkpoint": { diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index a78775eee13..16efb64667c 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -512,14 +512,6 @@ "description": "Controla el nivell de detall de les respostes del model. La verbositat baixa produeix respostes concises, mentre que la verbositat alta proporciona explicacions exhaustives." }, "setReasoningLevel": "Activa l'esforç de raonament", - "claudeCode": { - "pathLabel": "Ruta del Codi Claude", - "description": "Ruta opcional al teu CLI de Claude Code. Per defecte, 'claude' si no s'estableix.", - "placeholder": "Per defecte: claude", - "maxTokensLabel": "Tokens màxims de sortida", - "maxTokensDescription": "Nombre màxim de tokens de sortida per a les respostes de Claude Code. El valor per defecte és 8000." - } - }, "browser": { "enable": { "label": "Habilitar eina de navegador", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 8e1826905fd..e526335258d 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -151,7 +151,6 @@ "500": "Fehler auf dem Server des Anbieters. Es stimmt etwas mit der Anbieterseite nicht, mit deiner Anfrage stimmt alles.", "unknown": "Unbekannter API-Fehler. Bitte kontaktiere den Roo Code Support.", "connection": "Verbindungsfehler. Stelle sicher, dass du eine funktionierende Internetverbindung hast.", - "claudeCodeNotAuthenticated": "Du musst dich anmelden, um Claude Code zu verwenden. Gehe zu den Einstellungen und klicke auf \"Bei Claude Code anmelden\", um dich zu authentifizieren." } }, "checkpoint": { diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 276fda10c29..22d78adc513 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -512,14 +512,6 @@ "description": "Steuert, wie detailliert die Antworten des Modells sind. Niedrige Ausführlichkeit erzeugt knappe Antworten, während hohe Ausführlichkeit gründliche Erklärungen liefert." }, "setReasoningLevel": "Denkaufwand aktivieren", - "claudeCode": { - "pathLabel": "Claude-Code-Pfad", - "description": "Optionaler Pfad zu Ihrer Claude Code CLI. Standard ist 'claude', wenn nicht festgelegt.", - "placeholder": "Standard: claude", - "maxTokensLabel": "Maximale Ausgabe-Tokens", - "maxTokensDescription": "Maximale Anzahl an Ausgabe-Tokens für Claude Code-Antworten. Standard ist 8000." - } - }, "browser": { "enable": { "label": "Browser-Tool aktivieren", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 53ecf74da3a..974a2e5e18f 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -157,7 +157,6 @@ "500": "Provider server error. Something is wrong on the provider side, there's nothing wrong with your request.", "connection": "Connection error. Make sure you have a working internet connection.", "unknown": "Unknown API error. Please contact Roo Code support.", - "claudeCodeNotAuthenticated": "You need to sign in to use Claude Code. Go to Settings and click \"Sign in to Claude Code\" to authenticate." } }, "checkpoint": { diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index e13a97af479..9d73c9a43f4 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -521,14 +521,6 @@ "description": "Controls how detailed the model's responses are. Low verbosity produces concise answers, while high verbosity provides thorough explanations." }, "setReasoningLevel": "Enable Reasoning Effort", - "claudeCode": { - "pathLabel": "Claude Code Path", - "description": "Optional path to your Claude Code CLI. Defaults to 'claude' if not set.", - "placeholder": "Default: claude", - "maxTokensLabel": "Max Output Tokens", - "maxTokensDescription": "Maximum number of output tokens for Claude Code responses. Default is 8000." - } - }, "browser": { "enable": { "label": "Enable browser tool", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index b3d285a3596..2569943a56f 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -151,7 +151,6 @@ "500": "Error del servidor del proveedor. Algo está mal en el lado del proveedor, no hay nada mal con tu solicitud.", "unknown": "Error API desconocido. Por favor contacta al soporte de Roo Code.", "connection": "Error de conexión. Asegúrate de tener una conexión a Internet funcional.", - "claudeCodeNotAuthenticated": "Debes iniciar sesión para usar Claude Code. Ve a Configuración y haz clic en \"Iniciar sesión en Claude Code\" para autenticarte." } }, "checkpoint": { diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 9102adf812f..2df4744ed6a 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -512,14 +512,6 @@ "description": "Controla qué tan detalladas son las respuestas del modelo. La verbosidad baja produce respuestas concisas, mientras que la verbosidad alta proporciona explicaciones exhaustivas." }, "setReasoningLevel": "Habilitar esfuerzo de razonamiento", - "claudeCode": { - "pathLabel": "Ruta de Claude Code", - "description": "Ruta opcional a su CLI de Claude Code. Por defecto, es 'claude' si no se establece.", - "placeholder": "Por defecto: claude", - "maxTokensLabel": "Tokens máximos de salida", - "maxTokensDescription": "Número máximo de tokens de salida para las respuestas de Claude Code. El valor predeterminado es 8000." - } - }, "browser": { "enable": { "label": "Habilitar herramienta de navegador", diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 1deda1e9c8f..94a52276c49 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -151,7 +151,6 @@ "500": "Erreur du serveur du fournisseur. Quelque chose ne va pas du côté du fournisseur, il n'y a rien de mal avec votre demande.", "unknown": "Erreur API inconnue. Veuillez contacter le support Roo Code.", "connection": "Erreur de connexion. Assurez-vous que vous avez une connexion Internet fonctionnelle.", - "claudeCodeNotAuthenticated": "Vous devez vous connecter pour utiliser Claude Code. Allez dans les Paramètres et cliquez sur \"Se connecter à Claude Code\" pour vous authentifier." } }, "checkpoint": { diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 0a7746a5791..caa82ddc22c 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -512,14 +512,6 @@ "description": "Contrôle le niveau de détail des réponses du modèle. Une faible verbosité produit des réponses concises, tandis qu'une verbosité élevée fournit des explications approfondies." }, "setReasoningLevel": "Activer l'effort de raisonnement", - "claudeCode": { - "pathLabel": "Chemin du code Claude", - "description": "Chemin facultatif vers votre CLI Claude Code. La valeur par défaut est 'claude' si non défini.", - "placeholder": "Défaut : claude", - "maxTokensLabel": "Jetons de sortie max", - "maxTokensDescription": "Nombre maximum de jetons de sortie pour les réponses de Claude Code. La valeur par défaut est 8000." - } - }, "browser": { "enable": { "label": "Activer l'outil de navigateur", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index 66e51859872..398244043f7 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -151,7 +151,6 @@ "500": "प्रदाता सर्वर त्रुटि। प्रदाता की ओर से कुछ गलत है, आपके अनुरोध में कुछ गलत नहीं है।", "unknown": "अज्ञात API त्रुटि। कृपया Roo Code सहायता से संपर्क करें।", "connection": "कनेक्शन त्रुटि। सुनिश्चित करें कि आपके पास कार्यशील इंटरनेट कनेक्शन है।", - "claudeCodeNotAuthenticated": "Claude Code का उपयोग करने के लिए आपको साइन इन करना होगा। सेटिंग्स में जाएं और प्रमाणित करने के लिए \"Claude Code में साइन इन करें\" पर क्लिक करें।" } }, "checkpoint": { diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 1003c5ad482..27f71f0d14e 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -512,14 +512,6 @@ "description": "मॉडल की प्रतिक्रियाएं कितनी विस्तृत हैं, इसे नियंत्रित करता है। कम वर्बोसिटी संक्षिप्त उत्तर देती है, जबकि उच्च वर्बोसिटी विस्तृत स्पष्टीकरण प्रदान करती है।" }, "setReasoningLevel": "तर्क प्रयास सक्षम करें", - "claudeCode": { - "pathLabel": "क्लाउड कोड पथ", - "description": "आपके क्लाउड कोड सीएलआई का वैकल्पिक पथ। यदि सेट नहीं है तो डिफ़ॉल्ट 'claude' है।", - "placeholder": "डिफ़ॉल्ट: claude", - "maxTokensLabel": "अधिकतम आउटपुट टोकन", - "maxTokensDescription": "Claude Code प्रतिक्रियाओं के लिए आउटपुट टोकन की अधिकतम संख्या। डिफ़ॉल्ट 8000 है।" - } - }, "browser": { "enable": { "label": "ब्राउज़र टूल सक्षम करें", diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index da795acc3ef..11ca9377f5d 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -160,7 +160,6 @@ "500": "Kesalahan server penyedia. Ada yang salah di sisi penyedia, tidak ada yang salah dengan permintaan Anda.", "unknown": "Kesalahan API yang tidak diketahui. Harap hubungi dukungan Roo Code.", "connection": "Kesalahan koneksi. Pastikan Anda memiliki koneksi internet yang berfungsi.", - "claudeCodeNotAuthenticated": "Anda perlu masuk untuk menggunakan Claude Code. Buka Pengaturan dan klik \"Masuk ke Claude Code\" untuk mengautentikasi." } }, "checkpoint": { diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 2790c922229..d6c391e2c1a 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -516,14 +516,6 @@ "description": "Mengontrol seberapa detail respons model. Verbositas rendah menghasilkan jawaban singkat, sedangkan verbositas tinggi memberikan penjelasan menyeluruh." }, "setReasoningLevel": "Aktifkan Upaya Reasoning", - "claudeCode": { - "pathLabel": "Jalur Kode Claude", - "description": "Jalur opsional ke Claude Code CLI Anda. Defaultnya adalah 'claude' jika tidak diatur.", - "placeholder": "Default: claude", - "maxTokensLabel": "Token Output Maks", - "maxTokensDescription": "Jumlah maksimum token output untuk respons Claude Code. Default adalah 8000." - } - }, "browser": { "enable": { "label": "Aktifkan tool browser", diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index 9629c2a5ad7..65d8f6a00e3 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -154,7 +154,6 @@ "500": "Errore del server del provider. C'è qualcosa di sbagliato dal lato del provider, non c'è nulla di sbagliato nella tua richiesta.", "unknown": "Errore API sconosciuto. Contatta il supporto di Roo Code.", "connection": "Errore di connessione. Assicurati di avere una connessione Internet funzionante.", - "claudeCodeNotAuthenticated": "Devi accedere per utilizzare Claude Code. Vai su Impostazioni e clicca su \"Accedi a Claude Code\" per autenticarti." } }, "checkpoint": { diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 315ad3f664c..9623b99a915 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -512,14 +512,6 @@ "description": "Controlla il livello di dettaglio delle risposte del modello. Una verbosity bassa produce risposte concise, mentre una verbosity alta fornisce spiegazioni approfondite." }, "setReasoningLevel": "Abilita sforzo di ragionamento", - "claudeCode": { - "pathLabel": "Percorso Claude Code", - "description": "Percorso facoltativo per la tua CLI Claude Code. Predefinito 'claude' se non impostato.", - "placeholder": "Predefinito: claude", - "maxTokensLabel": "Token di output massimi", - "maxTokensDescription": "Numero massimo di token di output per le risposte di Claude Code. Il valore predefinito è 8000." - } - }, "browser": { "enable": { "label": "Abilita strumento browser", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index b9981828173..deecdc76fe9 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -151,7 +151,6 @@ "500": "プロバイダー サーバー エラー。プロバイダー側に問題があり、リクエスト自体に問題はありません。", "unknown": "不明な API エラー。Roo Code のサポートにお問い合わせください。", "connection": "接続エラー。インターネット接続が機能していることを確認してください。", - "claudeCodeNotAuthenticated": "Claude Codeを使用するにはサインインが必要です。設定に移動して「Claude Codeにサインイン」をクリックして認証してください。" } }, "checkpoint": { diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index a72baa5674a..66c9b002c13 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -512,14 +512,6 @@ "description": "モデルの応答の詳細度を制御します。冗長性が低いと簡潔な回答が生成され、高いと詳細な説明が提供されます。" }, "setReasoningLevel": "推論労力を有効にする", - "claudeCode": { - "pathLabel": "クロードコードパス", - "description": "Claude Code CLIへのオプションパス。設定されていない場合、デフォルトは「claude」です。", - "placeholder": "デフォルト:claude", - "maxTokensLabel": "最大出力トークン", - "maxTokensDescription": "Claude Codeレスポンスの最大出力トークン数。デフォルトは8000です。" - } - }, "browser": { "enable": { "label": "ブラウザツールを有効化", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 0121586b548..2146d86b3af 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -151,7 +151,6 @@ "500": "공급자 서버 오류입니다. 공급자 쪽에 문제가 있으며 요청에는 문제가 없습니다.", "unknown": "알 수 없는 API 오류입니다. Roo Code 지원팀에 문의하세요.", "connection": "연결 오류입니다. 인터넷 연결이 제대로 작동하는지 확인하세요.", - "claudeCodeNotAuthenticated": "Claude Code를 사용하려면 로그인해야 합니다. 설정으로 이동하여 \"Claude Code에 로그인\"을 클릭하여 인증하세요." } }, "checkpoint": { diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 47278eec7f7..0db23de38c7 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -512,14 +512,6 @@ "description": "모델 응답의 상세도를 제어합니다. 낮은 상세도는 간결한 답변을 생성하고, 높은 상세도는 상세한 설명을 제공합니다." }, "setReasoningLevel": "추론 노력 활성화", - "claudeCode": { - "pathLabel": "클로드 코드 경로", - "description": "Claude Code CLI의 선택적 경로입니다. 설정하지 않으면 'claude'가 기본값입니다.", - "placeholder": "기본값: claude", - "maxTokensLabel": "최대 출력 토큰", - "maxTokensDescription": "Claude Code 응답의 최대 출력 토큰 수. 기본값은 8000입니다." - } - }, "browser": { "enable": { "label": "브라우저 도구 활성화", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index a2032aa3e33..329400d06bf 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -146,7 +146,6 @@ "500": "Provider-serverfout. Er is iets mis aan de kant van de provider, er is niets mis met je verzoek.", "unknown": "Onbekende API-fout. Neem alsjeblieft contact op met Roo Code-ondersteuning.", "connection": "Verbindingsfout. Zorg ervoor dat je een werkende internetverbinding hebt.", - "claudeCodeNotAuthenticated": "Je moet inloggen om Claude Code te gebruiken. Ga naar Instellingen en klik op \"Inloggen bij Claude Code\" om te authenticeren." } }, "checkpoint": { diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 569099cddb2..8506e914c16 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -512,14 +512,6 @@ "description": "Bepaalt hoe gedetailleerd de reacties van het model zijn. Lage uitvoerbaarheid levert beknopte antwoorden op, terwijl hoge uitvoerbaarheid uitgebreide uitleg geeft." }, "setReasoningLevel": "Redeneervermogen inschakelen", - "claudeCode": { - "pathLabel": "Claude Code Pad", - "description": "Optioneel pad naar uw Claude Code CLI. Standaard 'claude' als niet ingesteld.", - "placeholder": "Standaard: claude", - "maxTokensLabel": "Max Output Tokens", - "maxTokensDescription": "Maximaal aantal output-tokens voor Claude Code-reacties. Standaard is 8000." - } - }, "browser": { "enable": { "label": "Browserhulpmiddel inschakelen", diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 5c4183a0ac0..40753537933 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -151,7 +151,6 @@ "500": "Błąd serwera dostawcy. Po stronie dostawcy coś się nie powiodło, w Twoim żądaniu nie ma nic złego.", "unknown": "Nieznany błąd API. Skontaktuj się z pomocą techniczną Roo Code.", "connection": "Błąd połączenia. Upewnij się, że masz działające połączenie internetowe.", - "claudeCodeNotAuthenticated": "Musisz się zalogować, aby korzystać z Claude Code. Przejdź do Ustawień i kliknij \"Zaloguj się do Claude Code\", aby się uwierzytelnić." } }, "checkpoint": { diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 3907ac765da..1e598feab9a 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -512,14 +512,6 @@ "description": "Kontroluje, jak szczegółowe są odpowiedzi modelu. Niska szczegółowość generuje zwięzłe odpowiedzi, podczas gdy wysoka szczegółowość dostarcza dokładnych wyjaśnień." }, "setReasoningLevel": "Włącz wysiłek rozumowania", - "claudeCode": { - "pathLabel": "Ścieżka Claude Code", - "description": "Opcjonalna ścieżka do Twojego CLI Claude Code. Domyślnie 'claude', jeśli nie ustawiono.", - "placeholder": "Domyślnie: claude", - "maxTokensLabel": "Maksymalna liczba tokenów wyjściowych", - "maxTokensDescription": "Maksymalna liczba tokenów wyjściowych dla odpowiedzi Claude Code. Domyślnie 8000." - } - }, "browser": { "enable": { "label": "Włącz narzędzie przeglądarki", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index eb2df5e8fc2..f0dea49d86a 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -151,7 +151,6 @@ "500": "Erro do servidor do provedor. Algo está errado do lado do provedor, não há nada de errado com sua solicitação.", "unknown": "Erro de API desconhecido. Por favor, entre em contato com o suporte do Roo Code.", "connection": "Erro de conexão. Certifique-se de que você tem uma conexão de internet funcionando.", - "claudeCodeNotAuthenticated": "Você precisa fazer login para usar o Claude Code. Vá para Configurações e clique em \"Entrar no Claude Code\" para autenticar." } }, "checkpoint": { diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 1b5cc8fcf3d..35fdddc8ee9 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -512,14 +512,6 @@ "description": "Controla o quão detalhadas são as respostas do modelo. A verbosidade baixa produz respostas concisas, enquanto a verbosidade alta fornisce explicações detalhadas." }, "setReasoningLevel": "Habilitar esforço de raciocínio", - "claudeCode": { - "pathLabel": "Caminho do Claude Code", - "description": "Caminho opcional para o seu Claude Code CLI. O padrão é 'claude' se não for definido.", - "placeholder": "Padrão: claude", - "maxTokensLabel": "Tokens de saída máximos", - "maxTokensDescription": "Número máximo de tokens de saída para respostas do Claude Code. O padrão é 8000." - } - }, "browser": { "enable": { "label": "Ativar ferramenta de navegador", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 85a144f31e9..1c7906181e7 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -146,7 +146,6 @@ "500": "Ошибка сервера провайдера. На стороне провайдера что-то пошло не так, с вашим запросом все в порядке.", "unknown": "Неизвестная ошибка API. Пожалуйста, свяжитесь с поддержкой Roo Code.", "connection": "Ошибка подключения. Убедитесь, что у вас есть рабочее подключение к Интернету.", - "claudeCodeNotAuthenticated": "Вам необходимо войти в систему, чтобы использовать Claude Code. Перейдите в Настройки и нажмите «Войти в Claude Code» для аутентификации." } }, "checkpoint": { diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index a30fed3395f..05201f34106 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -512,14 +512,6 @@ "description": "Контролирует, насколько подробны ответы модели. Низкая подробность дает краткие ответы, а высокая — подробные объяснения." }, "setReasoningLevel": "Включить усилие рассуждения", - "claudeCode": { - "pathLabel": "Путь к Claude Code", - "description": "Необязательный путь к вашему Claude Code CLI. По умолчанию используется 'claude', если не установлено.", - "placeholder": "По умолчанию: claude", - "maxTokensLabel": "Макс. выходных токенов", - "maxTokensDescription": "Максимальное количество выходных токенов для ответов Claude Code. По умолчанию 8000." - } - }, "browser": { "enable": { "label": "Включить инструмент браузера", diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 8bfeffea22b..f49de64bdfc 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -151,7 +151,6 @@ "500": "Sağlayıcı sunucu hatası. Sağlayıcı tarafında bir sorun var, isteğinizde sorun yok.", "unknown": "Bilinmeyen API hatası. Lütfen Roo Code desteğiyle iletişime geç.", "connection": "Bağlantı hatası. Çalışan bir internet bağlantınız olduğundan emin olun.", - "claudeCodeNotAuthenticated": "Claude Code'u kullanmak için oturum açmanız gerekiyor. Ayarlar'a gidin ve kimlik doğrulaması yapmak için \"Claude Code'da Oturum Aç\" seçeneğine tıklayın." } }, "checkpoint": { diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 951bc29183c..c80519f2e05 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -512,14 +512,6 @@ "description": "Modelin yanıtlarının ne kadar ayrıntılı olduğunu kontrol eder. Düşük ayrıntı düzeyi kısa yanıtlar üretirken, yüksek ayrıntı düzeyi kapsamlı açıklamalar sunar." }, "setReasoningLevel": "Akıl Yürütme Çabasını Etkinleştir", - "claudeCode": { - "pathLabel": "Claude Code Yolu", - "description": "Claude Code CLI'nize isteğe bağlı yol. Ayarlanmazsa varsayılan olarak 'claude' kullanılır.", - "placeholder": "Varsayılan: claude", - "maxTokensLabel": "Maksimum Çıktı Token sayısı", - "maxTokensDescription": "Claude Code yanıtları için maksimum çıktı token sayısı. Varsayılan 8000'dir." - } - }, "browser": { "enable": { "label": "Tarayıcı aracını etkinleştir", diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index 76483e19d58..1fa932d3f43 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -151,7 +151,6 @@ "500": "Lỗi máy chủ của nhà cung cấp. Có sự cố ở phía nhà cung cấp, không có gì sai với yêu cầu của bạn.", "unknown": "Lỗi API không xác định. Vui lòng liên hệ hỗ trợ Roo Code.", "connection": "Lỗi kết nối. Đảm bảo rằng bạn có kết nối Internet hoạt động.", - "claudeCodeNotAuthenticated": "Bạn cần đăng nhập để sử dụng Claude Code. Vào Cài đặt và nhấp vào \"Đăng nhập vào Claude Code\" để xác thực." } }, "checkpoint": { diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 4468b307f49..6e10a601baf 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -512,14 +512,6 @@ "description": "Kiểm soát mức độ chi tiết của các câu trả lời của mô hình. Mức độ chi tiết thấp tạo ra các câu trả lời ngắn gọn, trong khi mức độ chi tiết cao cung cấp giải thích kỹ lưỡng." }, "setReasoningLevel": "Kích hoạt nỗ lực suy luận", - "claudeCode": { - "pathLabel": "Đường dẫn Claude Code", - "description": "Đường dẫn tùy chọn đến Claude Code CLI của bạn. Mặc định là 'claude' nếu không được đặt.", - "placeholder": "Mặc định: claude", - "maxTokensLabel": "Số token đầu ra tối đa", - "maxTokensDescription": "Số lượng token đầu ra tối đa cho các phản hồi của Claude Code. Mặc định là 8000." - } - }, "browser": { "enable": { "label": "Bật công cụ trình duyệt", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 26b874c2b0d..147c88390bb 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -151,7 +151,6 @@ "500": "提供商服务器错误。提供商端出现问题,您的请求无问题。", "unknown": "未知 API 错误。请联系 Roo Code 支持。", "connection": "连接错误。确保您有可用的互联网连接。", - "claudeCodeNotAuthenticated": "你需要登录才能使用 Claude Code。前往设置并点击「登录到 Claude Code」进行身份验证。" } }, "checkpoint": { diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index b2952120b4f..3bc409a0c31 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -512,14 +512,6 @@ "description": "控制模型响应的详细程度。低详细度产生简洁的回答,而高详细度提供详尽的解释。" }, "setReasoningLevel": "启用推理工作量", - "claudeCode": { - "pathLabel": "Claude Code 路径", - "description": "您的 Claude Code CLI 的可选路径。如果未设置,则默认为 “claude”。", - "placeholder": "默认:claude", - "maxTokensLabel": "最大输出 Token", - "maxTokensDescription": "Claude Code 响应的最大输出 Token 数量。默认为 8000。" - } - }, "browser": { "enable": { "label": "启用浏览器工具", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 0449b84a7ae..9c1b34bc915 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -157,7 +157,6 @@ "500": "提供商伺服器錯誤。提供商端發生問題,您的請求沒有問題。", "unknown": "未知 API 錯誤。請聯絡 Roo Code 支援。", "connection": "連線錯誤。請確保您有可用的網際網路連線。", - "claudeCodeNotAuthenticated": "你需要登入才能使用 Claude Code。前往設定並點擊「登入 Claude Code」以進行驗證。" } }, "checkpoint": { diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 01ca8e8d5e7..ec72d058483 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -512,14 +512,6 @@ "description": "控制模型回應的詳細程度。低詳細度產生簡潔的回答,而高詳細度提供詳盡的解釋。" }, "setReasoningLevel": "啟用推理工作量", - "claudeCode": { - "pathLabel": "Claude Code 路徑", - "description": "可選的 Claude Code CLI 路徑。如果未設定,則預設為 'claude'。", - "placeholder": "預設:claude", - "maxTokensLabel": "最大輸出 Token", - "maxTokensDescription": "Claude Code 回應的最大輸出 Token 數量。預設為 8000。" - } - }, "browser": { "enable": { "label": "啟用瀏覽器工具",