diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index a66aae08a243..639800a530dc 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -193,6 +193,7 @@ const anthropicSchema = apiModelIdProviderModelSchema.extend({ anthropicBaseUrl: z.string().optional(), anthropicUseAuthToken: z.boolean().optional(), anthropicBeta1MContext: z.boolean().optional(), // Enable 'context-1m-2025-08-07' beta for 1M context window. + anthropicUseBatchApi: z.boolean().optional(), // Enable Batch API for 50% cost savings with async processing. }) const claudeCodeSchema = apiModelIdProviderModelSchema.extend({ diff --git a/src/api/providers/__tests__/anthropic.spec.ts b/src/api/providers/__tests__/anthropic.spec.ts index b05e50125b80..0f69aafa89ca 100644 --- a/src/api/providers/__tests__/anthropic.spec.ts +++ b/src/api/providers/__tests__/anthropic.spec.ts @@ -2,8 +2,17 @@ import { AnthropicHandler } from "../anthropic" import { ApiHandlerOptions } from "../../../shared/api" +import delay from "delay" const mockCreate = vitest.fn() +const mockFetch = vitest.fn() + +vitest.mock("delay", () => ({ + default: vitest.fn(() => Promise.resolve()), +})) + +// Mock global fetch +global.fetch = mockFetch as any vitest.mock("@anthropic-ai/sdk", () => { const mockAnthropicConstructor = vitest.fn().mockImplementation(() => ({ @@ -289,4 +298,342 @@ describe("AnthropicHandler", () => { expect(model.info.outputPrice).toBe(22.5) }) }) + + describe("Batch API", () => { + beforeEach(() => { + vitest.clearAllMocks() + // Reset fetch mock + mockFetch.mockReset() + }) + + it("should use batch API when anthropicUseBatchApi is enabled", async () => { + const handlerWithBatch = new AnthropicHandler({ + ...mockOptions, + anthropicUseBatchApi: true, + }) + + // Mock batch API responses + mockFetch + // First call: Create batch job + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + id: "batch-123", + status: "processing", + created_at: "2024-01-01T00:00:00Z", + }), + }) + // Second call: Check job status (still processing) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + id: "batch-123", + status: "processing", + created_at: "2024-01-01T00:00:00Z", + }), + }) + // Third call: Check job status (ended) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + id: "batch-123", + status: "ended", + created_at: "2024-01-01T00:00:00Z", + ended_at: "2024-01-01T00:00:30Z", + }), + }) + // Fourth call: Get results + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + results: [ + { + custom_id: "req_123", + result: { + type: "succeeded", + message: { + content: [{ type: "text", text: "Batch response" }], + usage: { + input_tokens: 100, + output_tokens: 50, + }, + }, + }, + }, + ], + }), + }) + + const systemPrompt = "You are a helpful assistant" + const messages = [{ role: "user" as const, content: "Hello" }] + + const stream = handlerWithBatch.createMessage(systemPrompt, messages) + + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Verify batch job was created + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining("/v1/messages/batches"), + expect.objectContaining({ + method: "POST", + headers: expect.objectContaining({ + "Content-Type": "application/json", + "x-api-key": mockOptions.apiKey, + "anthropic-version": "2023-06-01", + "anthropic-beta": "message-batches-2024-09-24", + }), + }), + ) + + // Verify polling occurred + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining("/v1/messages/batches/batch-123"), + expect.objectContaining({ + method: "GET", + }), + ) + + // Verify results were retrieved + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining("/v1/messages/batches/batch-123/results"), + expect.objectContaining({ + method: "GET", + }), + ) + + // Verify response content + const textChunks = chunks.filter((chunk) => chunk.type === "text") + expect(textChunks.some((chunk) => chunk.text.includes("Batch response"))).toBe(true) + + // Verify cost calculation with 50% discount + const usageChunk = chunks.find((chunk) => chunk.type === "usage" && chunk.totalCost !== undefined) + expect(usageChunk).toBeDefined() + }) + + it("should handle batch API timeout", async () => { + const handlerWithBatch = new AnthropicHandler({ + ...mockOptions, + anthropicUseBatchApi: true, + }) + + // Mock batch job creation + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + id: "batch-123", + status: "processing", + created_at: "2024-01-01T00:00:00Z", + }), + }) + // Keep returning processing status + .mockResolvedValue({ + ok: true, + json: async () => ({ + id: "batch-123", + status: "processing", + created_at: "2024-01-01T00:00:00Z", + }), + }) + + // Mock Date.now to simulate timeout + const originalDateNow = Date.now + let currentTime = originalDateNow() + Date.now = vitest.fn(() => { + currentTime += 11 * 60 * 1000 // Add 11 minutes each call + return currentTime + }) + + const systemPrompt = "You are a helpful assistant" + const messages = [{ role: "user" as const, content: "Hello" }] + + const stream = handlerWithBatch.createMessage(systemPrompt, messages) + + // Expect timeout error + await expect(async () => { + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + }).rejects.toThrow("Batch job timed out after 10 minutes") + + // Restore Date.now + Date.now = originalDateNow + }) + + it("should handle batch API failure", async () => { + const handlerWithBatch = new AnthropicHandler({ + ...mockOptions, + anthropicUseBatchApi: true, + }) + + // Mock batch job creation + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + id: "batch-123", + status: "processing", + created_at: "2024-01-01T00:00:00Z", + }), + }) + // Return failed status + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + id: "batch-123", + status: "failed", + created_at: "2024-01-01T00:00:00Z", + error: { + type: "api_error", + message: "Batch processing failed", + }, + }), + }) + + const systemPrompt = "You are a helpful assistant" + const messages = [{ role: "user" as const, content: "Hello" }] + + const stream = handlerWithBatch.createMessage(systemPrompt, messages) + + // Expect failure error + await expect(async () => { + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + }).rejects.toThrow("Batch job failed: Batch processing failed") + }) + + it("should show progress updates during batch processing", async () => { + const handlerWithBatch = new AnthropicHandler({ + ...mockOptions, + anthropicUseBatchApi: true, + }) + + // Mock delay to return immediately + const mockDelay = vitest.mocked(delay) + mockDelay.mockResolvedValue(undefined as any) + + let callCount = 0 + mockFetch + // First call: Create batch job + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + id: "batch-123", + status: "processing", + created_at: "2024-01-01T00:00:00Z", + }), + }) + // Multiple status checks + .mockImplementation(() => { + callCount++ + if (callCount <= 5) { + return Promise.resolve({ + ok: true, + json: async () => ({ + id: "batch-123", + status: "processing", + created_at: "2024-01-01T00:00:00Z", + }), + }) + } else if (callCount === 6) { + return Promise.resolve({ + ok: true, + json: async () => ({ + id: "batch-123", + status: "ended", + created_at: "2024-01-01T00:00:00Z", + ended_at: "2024-01-01T00:00:30Z", + }), + }) + } else { + // Results + return Promise.resolve({ + ok: true, + json: async () => ({ + results: [ + { + custom_id: "req_123", + result: { + type: "succeeded", + message: { + content: [{ type: "text", text: "Batch response" }], + usage: { + input_tokens: 100, + output_tokens: 50, + }, + }, + }, + }, + ], + }), + }) + } + }) + + // Mock Date.now for progress updates + const originalDateNow = Date.now + let currentTime = originalDateNow() + Date.now = vitest.fn(() => { + currentTime += 21000 // Add 21 seconds each call to trigger progress updates + return currentTime + }) + + const systemPrompt = "You are a helpful assistant" + const messages = [{ role: "user" as const, content: "Hello" }] + + const stream = handlerWithBatch.createMessage(systemPrompt, messages) + + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Verify progress messages + const textChunks = chunks.filter((chunk) => chunk.type === "text") + expect(textChunks.some((chunk) => chunk.text.includes("Creating batch job"))).toBe(true) + expect(textChunks.some((chunk) => chunk.text.includes("[Batch API] Processing"))).toBe(true) + expect(textChunks.some((chunk) => chunk.text.includes("Retrieving batch results"))).toBe(true) + + // Restore Date.now + Date.now = originalDateNow + }) + + it("should use regular streaming API when batch API is disabled", async () => { + const handlerWithoutBatch = new AnthropicHandler({ + ...mockOptions, + anthropicUseBatchApi: false, + }) + + const systemPrompt = "You are a helpful assistant" + const messages = [ + { + role: "user" as const, + content: [{ type: "text" as const, text: "Hello" }], + }, + ] + + const stream = handlerWithoutBatch.createMessage(systemPrompt, messages) + + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + // Should use regular API (mockCreate), not batch API (fetch) + expect(mockCreate).toHaveBeenCalled() + expect(mockFetch).not.toHaveBeenCalled() + + // Verify regular streaming response + const textChunks = chunks.filter((chunk) => chunk.type === "text") + expect(textChunks).toHaveLength(2) + expect(textChunks[0].text).toBe("Hello") + expect(textChunks[1].text).toBe(" world") + }) + }) }) diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index 3fb60c0e4fd1..29818963c2cd 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -1,6 +1,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming" import { CacheControlEphemeral } from "@anthropic-ai/sdk/resources" +import delay from "delay" import { type ModelInfo, @@ -19,9 +20,41 @@ import { BaseProvider } from "./base-provider" import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" import { calculateApiCostAnthropic } from "../../shared/cost" +// Batch API types +interface BatchRequest { + custom_id: string + params: Anthropic.Messages.MessageCreateParams +} + +interface BatchJob { + id: string + status: "creating" | "processing" | "ended" | "canceling" | "canceled" | "expired" | "failed" + created_at: string + processing_began_at?: string + ended_at?: string + error?: { + type: string + message: string + } +} + +interface BatchResult { + custom_id: string + result: { + type: "succeeded" | "failed" | "canceled" | "expired" + message?: Anthropic.Message + error?: { + type: string + message: string + } + } +} + export class AnthropicHandler extends BaseProvider implements SingleCompletionHandler { private options: ApiHandlerOptions private client: Anthropic + private static readonly BATCH_POLLING_INTERVAL = 20000 // 20 seconds + private static readonly BATCH_TIMEOUT = 10 * 60 * 1000 // 10 minutes constructor(options: ApiHandlerOptions) { super() @@ -36,13 +69,198 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa }) } + /** + * Creates a batch job for processing messages asynchronously + */ + private async createBatchJob(requests: BatchRequest[]): Promise { + const response = await fetch(`${this.client.baseURL || "https://api.anthropic.com"}/v1/messages/batches`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-api-key": this.options.apiKey || "", + "anthropic-version": "2023-06-01", + "anthropic-beta": "message-batches-2024-09-24", + }, + body: JSON.stringify({ requests }), + }) + + if (!response.ok) { + const error = await response.text() + throw new Error(`Failed to create batch job: ${error}`) + } + + const job: BatchJob = await response.json() + return job.id + } + + /** + * Polls for batch job status + */ + private async getBatchJobStatus(jobId: string): Promise { + const response = await fetch( + `${this.client.baseURL || "https://api.anthropic.com"}/v1/messages/batches/${jobId}`, + { + method: "GET", + headers: { + "x-api-key": this.options.apiKey || "", + "anthropic-version": "2023-06-01", + "anthropic-beta": "message-batches-2024-09-24", + }, + }, + ) + + if (!response.ok) { + const error = await response.text() + throw new Error(`Failed to get batch job status: ${error}`) + } + + return response.json() + } + + /** + * Retrieves batch job results + */ + private async getBatchResults(jobId: string): Promise { + const response = await fetch( + `${this.client.baseURL || "https://api.anthropic.com"}/v1/messages/batches/${jobId}/results`, + { + method: "GET", + headers: { + "x-api-key": this.options.apiKey || "", + "anthropic-version": "2023-06-01", + "anthropic-beta": "message-batches-2024-09-24", + }, + }, + ) + + if (!response.ok) { + const error = await response.text() + throw new Error(`Failed to get batch results: ${error}`) + } + + const data = await response.json() + return data.results || [] + } + + /** + * Process message using batch API with polling + */ + private async *processBatchMessage( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + modelId: string, + maxTokens: number | undefined, + temperature: number | undefined, + thinking: any, + ): ApiStream { + // Create batch request + const batchRequest: BatchRequest = { + custom_id: `req_${Date.now()}`, + params: { + model: modelId, + max_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS, + temperature, + system: systemPrompt, + messages, + }, + } + + // Add thinking parameter if applicable + if (thinking !== undefined) { + ;(batchRequest.params as any).thinking = thinking + } + + // Create batch job + yield { type: "text", text: "Creating batch job for processing (50% cost savings)...\n" } + const jobId = await this.createBatchJob([batchRequest]) + + // Poll for completion + const startTime = Date.now() + let lastUpdateTime = startTime + let job: BatchJob + + while (true) { + // Check for timeout + if (Date.now() - startTime > AnthropicHandler.BATCH_TIMEOUT) { + throw new Error("Batch job timed out after 10 minutes") + } + + job = await this.getBatchJobStatus(jobId) + + // Update progress every 20 seconds + const now = Date.now() + if (now - lastUpdateTime >= AnthropicHandler.BATCH_POLLING_INTERVAL) { + const elapsed = Math.floor((now - startTime) / 1000) + yield { + type: "text", + text: `[Batch API] Processing... (${elapsed}s elapsed, status: ${job.status})\n`, + } + lastUpdateTime = now + } + + if (job.status === "ended") { + break + } else if (job.status === "failed" || job.status === "canceled" || job.status === "expired") { + throw new Error(`Batch job ${job.status}: ${job.error?.message || "Unknown error"}`) + } + + // Wait before next poll + await delay(5000) // Poll every 5 seconds internally, but only show updates every 20s + } + + // Get results + yield { type: "text", text: "Retrieving batch results...\n\n" } + const results = await this.getBatchResults(jobId) + + if (results.length === 0) { + throw new Error("No results returned from batch job") + } + + const result = results[0] + if (result.result.type !== "succeeded" || !result.result.message) { + throw new Error(`Batch request failed: ${result.result.error?.message || "Unknown error"}`) + } + + const message = result.result.message + + // Extract content from the message + for (const content of message.content) { + if (content.type === "text") { + yield { type: "text", text: content.text } + } + } + + // Calculate and report usage (with 50% discount) + const usage = message.usage + if (usage) { + const { input_tokens = 0, output_tokens = 0 } = usage + const modelInfo = this.getModel().info + + // Calculate cost with 50% discount for batch API + const baseCost = calculateApiCostAnthropic( + modelInfo, + input_tokens, + output_tokens, + 0, // No cache writes for batch API + 0, // No cache reads for batch API + ) + + const discountedCost = baseCost * 0.5 + + yield { + type: "usage", + inputTokens: input_tokens, + outputTokens: output_tokens, + totalCost: discountedCost, + } + } + } + async *createMessage( systemPrompt: string, messages: Anthropic.Messages.MessageParam[], metadata?: ApiHandlerCreateMessageMetadata, ): ApiStream { - let stream: AnthropicStream - const cacheControl: CacheControlEphemeral = { type: "ephemeral" } let { id: modelId, betas = [], maxTokens, temperature, reasoning: thinking } = this.getModel() // Add 1M context beta flag if enabled for Claude Sonnet 4 and 4.5 @@ -53,6 +271,16 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa betas.push("context-1m-2025-08-07") } + // Use batch API if enabled + if (this.options.anthropicUseBatchApi) { + yield* this.processBatchMessage(systemPrompt, messages, modelId, maxTokens, temperature, thinking) + return + } + + // Regular streaming implementation + let stream: AnthropicStream + const cacheControl: CacheControlEphemeral = { type: "ephemeral" } + switch (modelId) { case "claude-sonnet-4-5": case "claude-sonnet-4-20250514": diff --git a/webview-ui/src/components/settings/providers/Anthropic.tsx b/webview-ui/src/components/settings/providers/Anthropic.tsx index feef788d49ea..df4a6a8f098d 100644 --- a/webview-ui/src/components/settings/providers/Anthropic.tsx +++ b/webview-ui/src/components/settings/providers/Anthropic.tsx @@ -99,6 +99,18 @@ export const Anthropic = ({ apiConfiguration, setApiConfigurationField }: Anthro )} +
+ { + setApiConfigurationField("anthropicUseBatchApi", checked) + }}> + {t("settings:providers.anthropicBatchApiLabel")} + +
+ {t("settings:providers.anthropicBatchApiDescription")} +
+
) } diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 611159069b29..31dc1480aa35 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -479,7 +479,9 @@ "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." - } + }, + "anthropicBatchApiLabel": "Use Batch API (50% cost savings)", + "anthropicBatchApiDescription": "Process requests asynchronously with 50% cost savings. Responses may take seconds to minutes. Best for non-urgent tasks like documentation, refactoring, and analysis." }, "browser": { "enable": { diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 00827751b0fd..d9d5bf2c06cc 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -479,7 +479,9 @@ "placeholder": "Standard: claude", "maxTokensLabel": "Maximale Ausgabe-Tokens", "maxTokensDescription": "Maximale Anzahl an Ausgabe-Tokens für Claude Code-Antworten. Standard ist 8000." - } + }, + "anthropicBatchApiLabel": "Use Batch API (50% cost savings)", + "anthropicBatchApiDescription": "Process requests asynchronously with 50% cost savings. Responses may take seconds to minutes. Best for non-urgent tasks like documentation, refactoring, and analysis." }, "browser": { "enable": { diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index dfccc49cc4ce..83b53fe0ceec 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -278,6 +278,8 @@ "anthropicUseAuthToken": "Pass Anthropic API Key as Authorization header instead of X-Api-Key", "anthropic1MContextBetaLabel": "Enable 1M context window (Beta)", "anthropic1MContextBetaDescription": "Extends context window to 1 million tokens for Claude Sonnet 4", + "anthropicBatchApiLabel": "Use Batch API (50% cost savings)", + "anthropicBatchApiDescription": "Process requests asynchronously with 50% cost savings. Responses may take seconds to minutes. Best for non-urgent tasks like documentation, refactoring, and analysis.", "awsBedrock1MContextBetaLabel": "Enable 1M context window (Beta)", "awsBedrock1MContextBetaDescription": "Extends context window to 1 million tokens for Claude Sonnet 4", "cerebrasApiKey": "Cerebras API Key", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index c1271df82740..c2dd2c223528 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -479,7 +479,9 @@ "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." - } + }, + "anthropicBatchApiLabel": "Use Batch API (50% cost savings)", + "anthropicBatchApiDescription": "Process requests asynchronously with 50% cost savings. Responses may take seconds to minutes. Best for non-urgent tasks like documentation, refactoring, and analysis." }, "browser": { "enable": { diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index abcf401d6402..d443cbc06672 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -479,7 +479,9 @@ "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." - } + }, + "anthropicBatchApiLabel": "Use Batch API (50% cost savings)", + "anthropicBatchApiDescription": "Process requests asynchronously with 50% cost savings. Responses may take seconds to minutes. Best for non-urgent tasks like documentation, refactoring, and analysis." }, "browser": { "enable": { diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 975e35411eea..2d6565feef89 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -479,7 +479,9 @@ "placeholder": "डिफ़ॉल्ट: claude", "maxTokensLabel": "अधिकतम आउटपुट टोकन", "maxTokensDescription": "Claude Code प्रतिक्रियाओं के लिए आउटपुट टोकन की अधिकतम संख्या। डिफ़ॉल्ट 8000 है।" - } + }, + "anthropicBatchApiLabel": "Use Batch API (50% cost savings)", + "anthropicBatchApiDescription": "Process requests asynchronously with 50% cost savings. Responses may take seconds to minutes. Best for non-urgent tasks like documentation, refactoring, and analysis." }, "browser": { "enable": { diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index aa2c1172119a..d3df8a78f6b6 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -483,7 +483,9 @@ "placeholder": "Default: claude", "maxTokensLabel": "Token Output Maks", "maxTokensDescription": "Jumlah maksimum token output untuk respons Claude Code. Default adalah 8000." - } + }, + "anthropicBatchApiLabel": "Use Batch API (50% cost savings)", + "anthropicBatchApiDescription": "Process requests asynchronously with 50% cost savings. Responses may take seconds to minutes. Best for non-urgent tasks like documentation, refactoring, and analysis." }, "browser": { "enable": { diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 6f2e06bb8fd6..1eb11a5b08d1 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -479,7 +479,9 @@ "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." - } + }, + "anthropicBatchApiLabel": "Use Batch API (50% cost savings)", + "anthropicBatchApiDescription": "Process requests asynchronously with 50% cost savings. Responses may take seconds to minutes. Best for non-urgent tasks like documentation, refactoring, and analysis." }, "browser": { "enable": { diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index cc1ea09317eb..7f7091f82c44 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -479,7 +479,9 @@ "placeholder": "デフォルト:claude", "maxTokensLabel": "最大出力トークン", "maxTokensDescription": "Claude Codeレスポンスの最大出力トークン数。デフォルトは8000です。" - } + }, + "anthropicBatchApiLabel": "Use Batch API (50% cost savings)", + "anthropicBatchApiDescription": "Process requests asynchronously with 50% cost savings. Responses may take seconds to minutes. Best for non-urgent tasks like documentation, refactoring, and analysis." }, "browser": { "enable": { diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 61539cfc4d9f..2679779b2399 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -479,7 +479,9 @@ "placeholder": "기본값: claude", "maxTokensLabel": "최대 출력 토큰", "maxTokensDescription": "Claude Code 응답의 최대 출력 토큰 수. 기본값은 8000입니다." - } + }, + "anthropicBatchApiLabel": "Use Batch API (50% cost savings)", + "anthropicBatchApiDescription": "Process requests asynchronously with 50% cost savings. Responses may take seconds to minutes. Best for non-urgent tasks like documentation, refactoring, and analysis." }, "browser": { "enable": { diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 41ee3e5910bb..222a30a8d0ea 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -479,7 +479,9 @@ "placeholder": "Standaard: claude", "maxTokensLabel": "Max Output Tokens", "maxTokensDescription": "Maximaal aantal output-tokens voor Claude Code-reacties. Standaard is 8000." - } + }, + "anthropicBatchApiLabel": "Use Batch API (50% cost savings)", + "anthropicBatchApiDescription": "Process requests asynchronously with 50% cost savings. Responses may take seconds to minutes. Best for non-urgent tasks like documentation, refactoring, and analysis." }, "browser": { "enable": { diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 6862d6f7edda..ca142de438d9 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -479,7 +479,9 @@ "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." - } + }, + "anthropicBatchApiLabel": "Use Batch API (50% cost savings)", + "anthropicBatchApiDescription": "Process requests asynchronously with 50% cost savings. Responses may take seconds to minutes. Best for non-urgent tasks like documentation, refactoring, and analysis." }, "browser": { "enable": { diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index b8184777acf0..b96c24624942 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -479,7 +479,9 @@ "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." - } + }, + "anthropicBatchApiLabel": "Use Batch API (50% cost savings)", + "anthropicBatchApiDescription": "Process requests asynchronously with 50% cost savings. Responses may take seconds to minutes. Best for non-urgent tasks like documentation, refactoring, and analysis." }, "browser": { "enable": { diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index bcbd72089a45..933c87e48ff4 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -479,7 +479,9 @@ "placeholder": "По умолчанию: claude", "maxTokensLabel": "Макс. выходных токенов", "maxTokensDescription": "Максимальное количество выходных токенов для ответов Claude Code. По умолчанию 8000." - } + }, + "anthropicBatchApiLabel": "Use Batch API (50% cost savings)", + "anthropicBatchApiDescription": "Process requests asynchronously with 50% cost savings. Responses may take seconds to minutes. Best for non-urgent tasks like documentation, refactoring, and analysis." }, "browser": { "enable": { diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 4ac28f47d2f2..b2bc0333b721 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -479,7 +479,9 @@ "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." - } + }, + "anthropicBatchApiLabel": "Use Batch API (50% cost savings)", + "anthropicBatchApiDescription": "Process requests asynchronously with 50% cost savings. Responses may take seconds to minutes. Best for non-urgent tasks like documentation, refactoring, and analysis." }, "browser": { "enable": { diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 4303325d068e..02babc01a231 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -479,7 +479,9 @@ "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." - } + }, + "anthropicBatchApiLabel": "Use Batch API (50% cost savings)", + "anthropicBatchApiDescription": "Process requests asynchronously with 50% cost savings. Responses may take seconds to minutes. Best for non-urgent tasks like documentation, refactoring, and analysis." }, "browser": { "enable": { diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index f574106f4568..feb5b6d7df43 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -479,7 +479,9 @@ "placeholder": "默认:claude", "maxTokensLabel": "最大输出 Token", "maxTokensDescription": "Claude Code 响应的最大输出 Token 数量。默认为 8000。" - } + }, + "anthropicBatchApiLabel": "Use Batch API (50% cost savings)", + "anthropicBatchApiDescription": "Process requests asynchronously with 50% cost savings. Responses may take seconds to minutes. Best for non-urgent tasks like documentation, refactoring, and analysis." }, "browser": { "enable": { diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 67e8c43b60a9..27aef45f4962 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -479,7 +479,9 @@ "placeholder": "預設:claude", "maxTokensLabel": "最大輸出 Token", "maxTokensDescription": "Claude Code 回應的最大輸出 Token 數量。預設為 8000。" - } + }, + "anthropicBatchApiLabel": "Use Batch API (50% cost savings)", + "anthropicBatchApiDescription": "Process requests asynchronously with 50% cost savings. Responses may take seconds to minutes. Best for non-urgent tasks like documentation, refactoring, and analysis." }, "browser": { "enable": {