diff --git a/package-lock.json b/package-lock.json index 42e106c3bcc..994bc46d789 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-dev", - "version": "3.0.12", + "version": "3.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-dev", - "version": "3.0.12", + "version": "3.1.5", "license": "Apache-2.0", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 58e4ba02507..79ede01b5ae 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -5,61 +5,103 @@ import { ApiHandler } from "../index" import { convertToOpenAiMessages } from "../transform/openai-format" import { ApiStream } from "../transform/stream" +export class AOAI_O1Handler implements ApiHandler { + private options: ApiHandlerOptions + private client: OpenAI + + constructor(options: ApiHandlerOptions) { + this.options = options + this.client = new OpenAI({ + baseURL: this.options.openAiBaseUrl, + apiKey: this.options.openAiApiKey, + }) + } + + async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { + const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [ + { role: "user", content: systemPrompt }, + ...convertToOpenAiMessages(messages), + ] + const response = await this.client.chat.completions.create({ + model: this.options.openAiModelId ?? "", + messages: openAiMessages, + }) + yield { + type: "text", + text: response.choices[0]?.message?.content || "", + } + if (response.usage) { + yield { + type: "usage", + inputTokens: response.usage.prompt_tokens || 0, + outputTokens: response.usage.completion_tokens || 0, + } + } + } + + getModel(): { id: string; info: ModelInfo } { + return { + id: this.options.openAiModelId ?? "", + info: openAiModelInfoSaneDefaults, + } + } +} + export class OpenAiHandler implements ApiHandler { - private options: ApiHandlerOptions - private client: OpenAI + private options: ApiHandlerOptions + private client: OpenAI - constructor(options: ApiHandlerOptions) { - this.options = options - // Azure API shape slightly differs from the core API shape: https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai - if (this.options.openAiBaseUrl?.toLowerCase().includes("azure.com")) { - this.client = new AzureOpenAI({ - baseURL: this.options.openAiBaseUrl, - apiKey: this.options.openAiApiKey, - apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion, - }) - } else { - this.client = new OpenAI({ - baseURL: this.options.openAiBaseUrl, - apiKey: this.options.openAiApiKey, - }) - } - } + constructor(options: ApiHandlerOptions) { + this.options = options + // Azure API shape slightly differs from the core API shape: https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai + if (this.options.openAiBaseUrl?.toLowerCase().includes("azure.com")) { + this.client = new AzureOpenAI({ + baseURL: this.options.openAiBaseUrl, + apiKey: this.options.openAiApiKey, + apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion, + }) + } else { + this.client = new OpenAI({ + baseURL: this.options.openAiBaseUrl, + apiKey: this.options.openAiApiKey, + }) + } + } - async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { - const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [ - { role: "system", content: systemPrompt }, - ...convertToOpenAiMessages(messages), - ] - const stream = await this.client.chat.completions.create({ - model: this.options.openAiModelId ?? "", - messages: openAiMessages, - temperature: 0, - stream: true, - stream_options: { include_usage: true }, - }) - for await (const chunk of stream) { - const delta = chunk.choices[0]?.delta - if (delta?.content) { - yield { - type: "text", - text: delta.content, - } - } - if (chunk.usage) { - yield { - type: "usage", - inputTokens: chunk.usage.prompt_tokens || 0, - outputTokens: chunk.usage.completion_tokens || 0, - } - } - } - } + async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { + const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [ + { role: "system", content: systemPrompt }, + ...convertToOpenAiMessages(messages), + ] + const stream = await this.client.chat.completions.create({ + model: this.options.openAiModelId ?? "", + messages: openAiMessages, + temperature: 0, + stream: true, + stream_options: { include_usage: true }, + }) + for await (const chunk of stream) { + const delta = chunk.choices[0]?.delta + if (delta?.content) { + yield { + type: "text", + text: delta.content, + } + } + if (chunk.usage) { + yield { + type: "usage", + inputTokens: chunk.usage.prompt_tokens || 0, + outputTokens: chunk.usage.completion_tokens || 0, + } + } + } + } - getModel(): { id: string; info: ModelInfo } { - return { - id: this.options.openAiModelId ?? "", - info: openAiModelInfoSaneDefaults, - } - } + getModel(): { id: string; info: ModelInfo } { + return { + id: this.options.openAiModelId ?? "", + info: openAiModelInfoSaneDefaults, + } + } } diff --git a/src/shared/api.ts b/src/shared/api.ts index 8229d027903..5b3c0a819e9 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -1,59 +1,61 @@ export type ApiProvider = - | "anthropic" - | "openrouter" - | "bedrock" - | "vertex" - | "openai" - | "ollama" - | "lmstudio" - | "gemini" - | "openai-native" - | "deepseek" + | "anthropic" + | "openrouter" + | "bedrock" + | "vertex" + | "openai" + | "ollama" + | "lmstudio" + | "gemini" + | "openai-native" + | "deepseek" + | "aoai_o1" export interface ApiHandlerOptions { - apiModelId?: string - apiKey?: string // anthropic - anthropicBaseUrl?: string - openRouterApiKey?: string - openRouterModelId?: string - openRouterModelInfo?: ModelInfo - awsAccessKey?: string - awsSecretKey?: string - awsSessionToken?: string - awsRegion?: string - awsUseCrossRegionInference?: boolean - vertexProjectId?: string - vertexRegion?: string - openAiBaseUrl?: string - openAiApiKey?: string - openAiModelId?: string - ollamaModelId?: string - ollamaBaseUrl?: string - lmStudioModelId?: string - lmStudioBaseUrl?: string - geminiApiKey?: string - openAiNativeApiKey?: string - deepSeekApiKey?: string - azureApiVersion?: string + apiModelId?: string + apiKey?: string // anthropic + anthropicBaseUrl?: string + openRouterApiKey?: string + openRouterModelId?: string + openRouterModelInfo?: ModelInfo + awsAccessKey?: string + awsSecretKey?: string + awsSessionToken?: string + awsRegion?: string + awsUseCrossRegionInference?: boolean + vertexProjectId?: string + vertexRegion?: string + openAiBaseUrl?: string + openAiApiKey?: string + openAiModelId?: string + ollamaModelId?: string + ollamaBaseUrl?: string + lmStudioModelId?: string + lmStudioBaseUrl?: string + geminiApiKey?: string + openAiNativeApiKey?: string + deepSeekApiKey?: string + azureApiVersion?: string + aoaiO1ApiKey?: string } export type ApiConfiguration = ApiHandlerOptions & { - apiProvider?: ApiProvider + apiProvider?: ApiProvider } // Models export interface ModelInfo { - maxTokens?: number - contextWindow?: number - supportsImages?: boolean - supportsComputerUse?: boolean - supportsPromptCache: boolean // this value is hardcoded for now - inputPrice?: number - outputPrice?: number - cacheWritesPrice?: number - cacheReadsPrice?: number - description?: string + maxTokens?: number + contextWindow?: number + supportsImages?: boolean + supportsComputerUse?: boolean + supportsPromptCache: boolean // this value is hardcoded for now + inputPrice?: number + outputPrice?: number + cacheWritesPrice?: number + cacheReadsPrice?: number + description?: string } // Anthropic @@ -61,47 +63,47 @@ export interface ModelInfo { export type AnthropicModelId = keyof typeof anthropicModels export const anthropicDefaultModelId: AnthropicModelId = "claude-3-5-sonnet-20241022" export const anthropicModels = { - "claude-3-5-sonnet-20241022": { - maxTokens: 8192, - contextWindow: 200_000, - supportsImages: true, - supportsComputerUse: true, - supportsPromptCache: true, - inputPrice: 3.0, // $3 per million input tokens - outputPrice: 15.0, // $15 per million output tokens - cacheWritesPrice: 3.75, // $3.75 per million tokens - cacheReadsPrice: 0.3, // $0.30 per million tokens - }, - "claude-3-5-haiku-20241022": { - maxTokens: 8192, - contextWindow: 200_000, - supportsImages: false, - supportsPromptCache: true, - inputPrice: 0.8, - outputPrice: 4.0, - cacheWritesPrice: 1.0, - cacheReadsPrice: 0.08, - }, - "claude-3-opus-20240229": { - maxTokens: 4096, - contextWindow: 200_000, - supportsImages: true, - supportsPromptCache: true, - inputPrice: 15.0, - outputPrice: 75.0, - cacheWritesPrice: 18.75, - cacheReadsPrice: 1.5, - }, - "claude-3-haiku-20240307": { - maxTokens: 4096, - contextWindow: 200_000, - supportsImages: true, - supportsPromptCache: true, - inputPrice: 0.25, - outputPrice: 1.25, - cacheWritesPrice: 0.3, - cacheReadsPrice: 0.03, - }, + "claude-3-5-sonnet-20241022": { + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: true, + supportsComputerUse: true, + supportsPromptCache: true, + inputPrice: 3.0, // $3 per million input tokens + outputPrice: 15.0, // $15 per million output tokens + cacheWritesPrice: 3.75, // $3.75 per million tokens + cacheReadsPrice: 0.3, // $0.30 per million tokens + }, + "claude-3-5-haiku-20241022": { + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 0.8, + outputPrice: 4.0, + cacheWritesPrice: 1.0, + cacheReadsPrice: 0.08, + }, + "claude-3-opus-20240229": { + maxTokens: 4096, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 15.0, + outputPrice: 75.0, + cacheWritesPrice: 18.75, + cacheReadsPrice: 1.5, + }, + "claude-3-haiku-20240307": { + maxTokens: 4096, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 0.25, + outputPrice: 1.25, + cacheWritesPrice: 0.3, + cacheReadsPrice: 0.03, + }, } as const satisfies Record // as const assertion makes the object deeply readonly // AWS Bedrock @@ -109,72 +111,72 @@ export const anthropicModels = { export type BedrockModelId = keyof typeof bedrockModels export const bedrockDefaultModelId: BedrockModelId = "anthropic.claude-3-5-sonnet-20241022-v2:0" export const bedrockModels = { - "anthropic.claude-3-5-sonnet-20241022-v2:0": { - maxTokens: 8192, - contextWindow: 200_000, - supportsImages: true, - supportsComputerUse: true, - supportsPromptCache: false, - inputPrice: 3.0, - outputPrice: 15.0, - }, - "anthropic.claude-3-5-haiku-20241022-v1:0": { - maxTokens: 8192, - contextWindow: 200_000, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 1.0, - outputPrice: 5.0, - }, - "anthropic.claude-3-5-sonnet-20240620-v1:0": { - maxTokens: 8192, - contextWindow: 200_000, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 3.0, - outputPrice: 15.0, - }, - "anthropic.claude-3-opus-20240229-v1:0": { - maxTokens: 4096, - contextWindow: 200_000, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 15.0, - outputPrice: 75.0, - }, - "anthropic.claude-3-sonnet-20240229-v1:0": { - maxTokens: 4096, - contextWindow: 200_000, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 3.0, - outputPrice: 15.0, - }, - "anthropic.claude-3-haiku-20240307-v1:0": { - maxTokens: 4096, - contextWindow: 200_000, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0.25, - outputPrice: 1.25, - }, + "anthropic.claude-3-5-sonnet-20241022-v2:0": { + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: true, + supportsComputerUse: true, + supportsPromptCache: false, + inputPrice: 3.0, + outputPrice: 15.0, + }, + "anthropic.claude-3-5-haiku-20241022-v1:0": { + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 1.0, + outputPrice: 5.0, + }, + "anthropic.claude-3-5-sonnet-20240620-v1:0": { + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 3.0, + outputPrice: 15.0, + }, + "anthropic.claude-3-opus-20240229-v1:0": { + maxTokens: 4096, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 15.0, + outputPrice: 75.0, + }, + "anthropic.claude-3-sonnet-20240229-v1:0": { + maxTokens: 4096, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 3.0, + outputPrice: 15.0, + }, + "anthropic.claude-3-haiku-20240307-v1:0": { + maxTokens: 4096, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0.25, + outputPrice: 1.25, + }, } as const satisfies Record // OpenRouter // https://openrouter.ai/models?order=newest&supported_parameters=tools export const openRouterDefaultModelId = "anthropic/claude-3.5-sonnet:beta" // will always exist in openRouterModels export const openRouterDefaultModelInfo: ModelInfo = { - maxTokens: 8192, - contextWindow: 200_000, - supportsImages: true, - supportsComputerUse: true, - supportsPromptCache: true, - inputPrice: 3.0, - outputPrice: 15.0, - cacheWritesPrice: 3.75, - cacheReadsPrice: 0.3, - description: - "The new Claude 3.5 Sonnet delivers better-than-Opus capabilities, faster-than-Sonnet speeds, at the same Sonnet prices. Sonnet is particularly good at:\n\n- Coding: New Sonnet scores ~49% on SWE-Bench Verified, higher than the last best score, and without any fancy prompt scaffolding\n- Data science: Augments human data science expertise; navigates unstructured data while using multiple tools for insights\n- Visual processing: excelling at interpreting charts, graphs, and images, accurately transcribing text to derive insights beyond just the text alone\n- Agentic tasks: exceptional tool use, making it great at agentic tasks (i.e. complex, multi-step problem solving tasks that require engaging with other systems)\n\n#multimodal\n\n_This is a faster endpoint, made available in collaboration with Anthropic, that is self-moderated: response moderation happens on the provider's side instead of OpenRouter's. For requests that pass moderation, it's identical to the [Standard](/anthropic/claude-3.5-sonnet) variant._", + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: true, + supportsComputerUse: true, + supportsPromptCache: true, + inputPrice: 3.0, + outputPrice: 15.0, + cacheWritesPrice: 3.75, + cacheReadsPrice: 0.3, + description: + "The new Claude 3.5 Sonnet delivers better-than-Opus capabilities, faster-than-Sonnet speeds, at the same Sonnet prices. Sonnet is particularly good at:\n\n- Coding: New Sonnet scores ~49% on SWE-Bench Verified, higher than the last best score, and without any fancy prompt scaffolding\n- Data science: Augments human data science expertise; navigates unstructured data while using multiple tools for insights\n- Visual processing: excelling at interpreting charts, graphs, and images, accurately transcribing text to derive insights beyond just the text alone\n- Agentic tasks: exceptional tool use, making it great at agentic tasks (i.e. complex, multi-step problem solving tasks that require engaging with other systems)\n\n#multimodal\n\n_This is a faster endpoint, made available in collaboration with Anthropic, that is self-moderated: response moderation happens on the provider's side instead of OpenRouter's. For requests that pass moderation, it's identical to the [Standard](/anthropic/claude-3.5-sonnet) variant._", } // Vertex AI @@ -182,56 +184,66 @@ export const openRouterDefaultModelInfo: ModelInfo = { export type VertexModelId = keyof typeof vertexModels export const vertexDefaultModelId: VertexModelId = "claude-3-5-sonnet-v2@20241022" export const vertexModels = { - "claude-3-5-sonnet-v2@20241022": { - maxTokens: 8192, - contextWindow: 200_000, - supportsImages: true, - supportsComputerUse: true, - supportsPromptCache: false, - inputPrice: 3.0, - outputPrice: 15.0, - }, - "claude-3-5-sonnet@20240620": { - maxTokens: 8192, - contextWindow: 200_000, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 3.0, - outputPrice: 15.0, - }, - "claude-3-5-haiku@20241022": { - maxTokens: 8192, - contextWindow: 200_000, - supportsImages: false, - supportsPromptCache: false, - inputPrice: 1.0, - outputPrice: 5.0, - }, - "claude-3-opus@20240229": { - maxTokens: 4096, - contextWindow: 200_000, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 15.0, - outputPrice: 75.0, - }, - "claude-3-haiku@20240307": { - maxTokens: 4096, - contextWindow: 200_000, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0.25, - outputPrice: 1.25, - }, + "claude-3-5-sonnet-v2@20241022": { + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: true, + supportsComputerUse: true, + supportsPromptCache: false, + inputPrice: 3.0, + outputPrice: 15.0, + }, + "claude-3-5-sonnet@20240620": { + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 3.0, + outputPrice: 15.0, + }, + "claude-3-5-haiku@20241022": { + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 1.0, + outputPrice: 5.0, + }, + "claude-3-opus@20240229": { + maxTokens: 4096, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 15.0, + outputPrice: 75.0, + }, + "claude-3-haiku@20240307": { + maxTokens: 4096, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0.25, + outputPrice: 1.25, + }, } as const satisfies Record export const openAiModelInfoSaneDefaults: ModelInfo = { - maxTokens: -1, - contextWindow: 128_000, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0, - outputPrice: 0, + maxTokens: -1, + contextWindow: 128_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, +} + +export const aoaiO1ModelInfo: ModelInfo = { + maxTokens: 10000, // Token limit per minute + contextWindow: 128_000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 15, // $0.002 per 1,000 tokens for prompts + outputPrice: 60, // $0.002 per 1,000 tokens for completions + description: "Azure OpenAI O1 model with no streaming, no temperature, and no system prompt (use user instead). Rate limit: 50 queries per week.", } // Gemini @@ -239,70 +251,70 @@ export const openAiModelInfoSaneDefaults: ModelInfo = { export type GeminiModelId = keyof typeof geminiModels export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-thinking-exp-1219" export const geminiModels = { - "gemini-2.0-flash-thinking-exp-1219": { - maxTokens: 8192, - contextWindow: 32_767, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0, - outputPrice: 0, - }, - "gemini-2.0-flash-exp": { - maxTokens: 8192, - contextWindow: 1_048_576, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0, - outputPrice: 0, - }, - "gemini-exp-1206": { - maxTokens: 8192, - contextWindow: 2_097_152, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0, - outputPrice: 0, - }, - "gemini-1.5-flash-002": { - maxTokens: 8192, - contextWindow: 1_048_576, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0, - outputPrice: 0, - }, - "gemini-1.5-flash-exp-0827": { - maxTokens: 8192, - contextWindow: 1_048_576, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0, - outputPrice: 0, - }, - "gemini-1.5-flash-8b-exp-0827": { - maxTokens: 8192, - contextWindow: 1_048_576, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0, - outputPrice: 0, - }, - "gemini-1.5-pro-002": { - maxTokens: 8192, - contextWindow: 2_097_152, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0, - outputPrice: 0, - }, - "gemini-1.5-pro-exp-0827": { - maxTokens: 8192, - contextWindow: 2_097_152, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0, - outputPrice: 0, - }, + "gemini-2.0-flash-thinking-exp-1219": { + maxTokens: 8192, + contextWindow: 32_767, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, + "gemini-2.0-flash-exp": { + maxTokens: 8192, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, + "gemini-exp-1206": { + maxTokens: 8192, + contextWindow: 2_097_152, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, + "gemini-1.5-flash-002": { + maxTokens: 8192, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, + "gemini-1.5-flash-exp-0827": { + maxTokens: 8192, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, + "gemini-1.5-flash-8b-exp-0827": { + maxTokens: 8192, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, + "gemini-1.5-pro-002": { + maxTokens: 8192, + contextWindow: 2_097_152, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, + "gemini-1.5-pro-exp-0827": { + maxTokens: 8192, + contextWindow: 2_097_152, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + }, } as const satisfies Record // OpenAI Native @@ -310,47 +322,47 @@ export const geminiModels = { export type OpenAiNativeModelId = keyof typeof openAiNativeModels export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-4o" export const openAiNativeModels = { - // don't support tool use yet - o1: { - maxTokens: 100_000, - contextWindow: 200_000, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 15, - outputPrice: 60, - }, - "o1-preview": { - maxTokens: 32_768, - contextWindow: 128_000, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 15, - outputPrice: 60, - }, - "o1-mini": { - maxTokens: 65_536, - contextWindow: 128_000, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 3, - outputPrice: 12, - }, - "gpt-4o": { - maxTokens: 4_096, - contextWindow: 128_000, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 5, - outputPrice: 15, - }, - "gpt-4o-mini": { - maxTokens: 16_384, - contextWindow: 128_000, - supportsImages: true, - supportsPromptCache: false, - inputPrice: 0.15, - outputPrice: 0.6, - }, + // don't support tool use yet + o1: { + maxTokens: 100_000, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 15, + outputPrice: 60, + }, + "o1-preview": { + maxTokens: 32_768, + contextWindow: 128_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 15, + outputPrice: 60, + }, + "o1-mini": { + maxTokens: 65_536, + contextWindow: 128_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 3, + outputPrice: 12, + }, + "gpt-4o": { + maxTokens: 4_096, + contextWindow: 128_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 5, + outputPrice: 15, + }, + "gpt-4o-mini": { + maxTokens: 16_384, + contextWindow: 128_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0.15, + outputPrice: 0.6, + }, } as const satisfies Record // Azure OpenAI @@ -363,14 +375,14 @@ export const azureOpenAiDefaultApiVersion = "2024-08-01-preview" export type DeepSeekModelId = keyof typeof deepSeekModels export const deepSeekDefaultModelId: DeepSeekModelId = "deepseek-chat" export const deepSeekModels = { - "deepseek-chat": { - maxTokens: 8_000, - contextWindow: 64_000, - supportsImages: false, - supportsPromptCache: true, // supports context caching, but not in the way anthropic does it (deepseek reports input tokens and reads/writes in the same usage report) FIXME: we need to show users cache stats how deepseek does it - inputPrice: 0, // technically there is no input price, it's all either a cache hit or miss (ApiOptions will not show this) - outputPrice: 0.28, - cacheWritesPrice: 0.14, - cacheReadsPrice: 0.014, - }, + "deepseek-chat": { + maxTokens: 8_000, + contextWindow: 64_000, + supportsImages: false, + supportsPromptCache: true, // supports context caching, but not in the way anthropic does it (deepseek reports input tokens and reads/writes in the same usage report) FIXME: we need to show users cache stats how deepseek does it + inputPrice: 0, // technically there is no input price, it's all either a cache hit or miss (ApiOptions will not show this) + outputPrice: 0.28, + cacheWritesPrice: 0.14, + cacheReadsPrice: 0.014, + }, } as const satisfies Record diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index fa00598a8e2..4635bca26b7 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -1,33 +1,33 @@ import { - VSCodeCheckbox, - VSCodeDropdown, - VSCodeLink, - VSCodeOption, - VSCodeRadio, - VSCodeRadioGroup, - VSCodeTextField, + VSCodeCheckbox, + VSCodeDropdown, + VSCodeLink, + VSCodeOption, + VSCodeRadio, + VSCodeRadioGroup, + VSCodeTextField, } from "@vscode/webview-ui-toolkit/react" import { Fragment, memo, useCallback, useEffect, useMemo, useState } from "react" import { useEvent, useInterval } from "react-use" import { - ApiConfiguration, - ModelInfo, - anthropicDefaultModelId, - anthropicModels, - azureOpenAiDefaultApiVersion, - bedrockDefaultModelId, - bedrockModels, - deepSeekDefaultModelId, - deepSeekModels, - geminiDefaultModelId, - geminiModels, - openAiModelInfoSaneDefaults, - openAiNativeDefaultModelId, - openAiNativeModels, - openRouterDefaultModelId, - openRouterDefaultModelInfo, - vertexDefaultModelId, - vertexModels, + ApiConfiguration, + ModelInfo, + anthropicDefaultModelId, + anthropicModels, + azureOpenAiDefaultApiVersion, + bedrockDefaultModelId, + bedrockModels, + deepSeekDefaultModelId, + deepSeekModels, + geminiDefaultModelId, + geminiModels, + openAiModelInfoSaneDefaults, + openAiNativeDefaultModelId, + openAiNativeModels, + openRouterDefaultModelId, + openRouterDefaultModelInfo, + vertexDefaultModelId, + vertexModels, } from "../../../../src/shared/api" import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage" import { useExtensionState } from "../../context/ExtensionStateContext" @@ -36,890 +36,922 @@ import VSCodeButtonLink from "../common/VSCodeButtonLink" import OpenRouterModelPicker, { ModelDescriptionMarkdown, OPENROUTER_MODEL_PICKER_Z_INDEX } from "./OpenRouterModelPicker" interface ApiOptionsProps { - showModelOptions: boolean - apiErrorMessage?: string - modelIdErrorMessage?: string + showModelOptions: boolean + apiErrorMessage?: string + modelIdErrorMessage?: string } const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) => { - const { apiConfiguration, setApiConfiguration, uriScheme } = useExtensionState() - const [ollamaModels, setOllamaModels] = useState([]) - const [lmStudioModels, setLmStudioModels] = useState([]) - const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl) - const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion) - const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false) - - const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => { - setApiConfiguration({ - ...apiConfiguration, - [field]: event.target.value, - }) - } - - const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => { - return normalizeApiConfiguration(apiConfiguration) - }, [apiConfiguration]) - - // Poll ollama/lmstudio models - const requestLocalModels = useCallback(() => { - if (selectedProvider === "ollama") { - vscode.postMessage({ - type: "requestOllamaModels", - text: apiConfiguration?.ollamaBaseUrl, - }) - } else if (selectedProvider === "lmstudio") { - vscode.postMessage({ - type: "requestLmStudioModels", - text: apiConfiguration?.lmStudioBaseUrl, - }) - } - }, [selectedProvider, apiConfiguration?.ollamaBaseUrl, apiConfiguration?.lmStudioBaseUrl]) - useEffect(() => { - if (selectedProvider === "ollama" || selectedProvider === "lmstudio") { - requestLocalModels() - } - }, [selectedProvider, requestLocalModels]) - useInterval(requestLocalModels, selectedProvider === "ollama" || selectedProvider === "lmstudio" ? 2000 : null) - - const handleMessage = useCallback((event: MessageEvent) => { - const message: ExtensionMessage = event.data - if (message.type === "ollamaModels" && message.ollamaModels) { - setOllamaModels(message.ollamaModels) - } else if (message.type === "lmStudioModels" && message.lmStudioModels) { - setLmStudioModels(message.lmStudioModels) - } - }, []) - useEvent("message", handleMessage) - - /* - VSCodeDropdown has an open bug where dynamically rendered options don't auto select the provided value prop. You can see this for yourself by comparing it with normal select/option elements, which work as expected. - https://github.com/microsoft/vscode-webview-ui-toolkit/issues/433 - - In our case, when the user switches between providers, we recalculate the selectedModelId depending on the provider, the default model for that provider, and a modelId that the user may have selected. Unfortunately, the VSCodeDropdown component wouldn't select this calculated value, and would default to the first "Select a model..." option instead, which makes it seem like the model was cleared out when it wasn't. - - As a workaround, we create separate instances of the dropdown for each provider, and then conditionally render the one that matches the current provider. - */ - const createDropdown = (models: Record) => { - return ( - - Select a model... - {Object.keys(models).map((modelId) => ( - - {modelId} - - ))} - - ) - } - - return ( -
-
- - - OpenRouter - Anthropic - Google Gemini - DeepSeek - GCP Vertex AI - AWS Bedrock - OpenAI - OpenAI Compatible - LM Studio - Ollama - -
- - {selectedProvider === "anthropic" && ( -
- - Anthropic API Key - - - { - const isChecked = e.target.checked === true - setAnthropicBaseUrlSelected(isChecked) - if (!isChecked) { - setApiConfiguration({ - ...apiConfiguration, - anthropicBaseUrl: "", - }) - } - }}> - Use custom base URL - - - {anthropicBaseUrlSelected && ( - - )} - -

- This key is stored locally and only used to make API requests from this extension. - {!apiConfiguration?.apiKey && ( - - You can get an Anthropic API key by signing up here. - - )} -

-
- )} - - {selectedProvider === "openai-native" && ( -
- - OpenAI API Key - -

- This key is stored locally and only used to make API requests from this extension. - {!apiConfiguration?.openAiNativeApiKey && ( - - You can get an OpenAI API key by signing up here. - - )} -

-
- )} - - {selectedProvider === "deepseek" && ( -
- - DeepSeek API Key - -

- This key is stored locally and only used to make API requests from this extension. - {!apiConfiguration?.deepSeekApiKey && ( - - You can get a DeepSeek API key by signing up here. - - )} -

-
- )} - - {selectedProvider === "openrouter" && ( -
- - OpenRouter API Key - - {!apiConfiguration?.openRouterApiKey && ( - - Get OpenRouter API Key - - )} -

- This key is stored locally and only used to make API requests from this extension.{" "} - {/* {!apiConfiguration?.openRouterApiKey && ( - - (Note: OpenRouter is recommended for high rate - limits, prompt caching, and wider selection of models.) - - )} */} -

-
- )} - - {selectedProvider === "bedrock" && ( -
- - AWS Access Key - - - AWS Secret Key - - - AWS Session Token - -
- - - Select a region... - {/* The user will have to choose a region that supports the model they use, but this shouldn't be a problem since they'd have to request access for it in that region in the first place. */} - us-east-1 - us-east-2 - {/* us-west-1 */} - us-west-2 - {/* af-south-1 */} - {/* ap-east-1 */} - ap-south-1 - ap-northeast-1 - ap-northeast-2 - {/* ap-northeast-3 */} - ap-southeast-1 - ap-southeast-2 - ca-central-1 - eu-central-1 - eu-central-2 - eu-west-1 - eu-west-2 - eu-west-3 - {/* eu-north-1 */} - {/* me-south-1 */} - sa-east-1 - us-gov-east-1 - us-gov-west-1 - {/* us-gov-east-1 */} - -
- { - const isChecked = e.target.checked === true - setApiConfiguration({ - ...apiConfiguration, - awsUseCrossRegionInference: isChecked, - }) - }}> - Use cross-region inference - -

- Authenticate by either providing the keys above or use the default AWS credential providers, i.e. - ~/.aws/credentials or environment variables. These credentials are only used locally to make API requests - from this extension. -

-
- )} - - {apiConfiguration?.apiProvider === "vertex" && ( -
- - Google Cloud Project ID - -
- - - Select a region... - us-east5 - us-central1 - europe-west1 - europe-west4 - asia-southeast1 - -
-

- To use Google Cloud Vertex AI, you need to - - {"1) create a Google Cloud account › enable the Vertex AI API › enable the desired Claude models,"} - {" "} - - {"2) install the Google Cloud CLI › configure Application Default Credentials."} - -

-
- )} - - {selectedProvider === "gemini" && ( -
- - Gemini API Key - -

- This key is stored locally and only used to make API requests from this extension. - {!apiConfiguration?.geminiApiKey && ( - - You can get a Gemini API key by signing up here. - - )} -

-
- )} - - {selectedProvider === "openai" && ( -
- - Base URL - - - API Key - - - Model ID - - { - const isChecked = e.target.checked === true - setAzureApiVersionSelected(isChecked) - if (!isChecked) { - setApiConfiguration({ - ...apiConfiguration, - azureApiVersion: "", - }) - } - }}> - Set Azure API version - - {azureApiVersionSelected && ( - - )} -

- - (Note: Cline uses complex prompts and works best with Claude - models. Less capable models may not work as expected.) - -

-
- )} - - {selectedProvider === "lmstudio" && ( -
- - Base URL (optional) - - - Model ID - - {lmStudioModels.length > 0 && ( - { - const value = (e.target as HTMLInputElement)?.value - // need to check value first since radio group returns empty string sometimes - if (value) { - handleInputChange("lmStudioModelId")({ - target: { value }, - }) - } - }}> - {lmStudioModels.map((model) => ( - - {model} - - ))} - - )} -

- LM Studio allows you to run models locally on your computer. For instructions on how to get started, see - their - - quickstart guide. - - You will also need to start LM Studio's{" "} - - local server - {" "} - feature to use it with this extension.{" "} - - (Note: Cline uses complex prompts and works best with Claude - models. Less capable models may not work as expected.) - -

-
- )} - - {selectedProvider === "ollama" && ( -
- - Base URL (optional) - - - Model ID - - {ollamaModels.length > 0 && ( - { - const value = (e.target as HTMLInputElement)?.value - // need to check value first since radio group returns empty string sometimes - if (value) { - handleInputChange("ollamaModelId")({ - target: { value }, - }) - } - }}> - {ollamaModels.map((model) => ( - - {model} - - ))} - - )} -

- Ollama allows you to run models locally on your computer. For instructions on how to get started, see - their - - quickstart guide. - - - (Note: Cline uses complex prompts and works best with Claude - models. Less capable models may not work as expected.) - -

-
- )} - - {apiErrorMessage && ( -

- {apiErrorMessage} -

- )} - - {selectedProvider === "openrouter" && showModelOptions && } - - {selectedProvider !== "openrouter" && - selectedProvider !== "openai" && - selectedProvider !== "ollama" && - selectedProvider !== "lmstudio" && - showModelOptions && ( - <> -
- - {selectedProvider === "anthropic" && createDropdown(anthropicModels)} - {selectedProvider === "bedrock" && createDropdown(bedrockModels)} - {selectedProvider === "vertex" && createDropdown(vertexModels)} - {selectedProvider === "gemini" && createDropdown(geminiModels)} - {selectedProvider === "openai-native" && createDropdown(openAiNativeModels)} - {selectedProvider === "deepseek" && createDropdown(deepSeekModels)} -
- - - - )} - - {modelIdErrorMessage && ( -

- {modelIdErrorMessage} -

- )} -
- ) + const { apiConfiguration, setApiConfiguration, uriScheme } = useExtensionState() + const [ollamaModels, setOllamaModels] = useState([]) + const [lmStudioModels, setLmStudioModels] = useState([]) + const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl) + const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion) + const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false) + + const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => { + setApiConfiguration({ + ...apiConfiguration, + [field]: event.target.value, + }) + } + + const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => { + return normalizeApiConfiguration(apiConfiguration) + }, [apiConfiguration]) + + // Poll ollama/lmstudio models + const requestLocalModels = useCallback(() => { + if (selectedProvider === "ollama") { + vscode.postMessage({ + type: "requestOllamaModels", + text: apiConfiguration?.ollamaBaseUrl, + }) + } else if (selectedProvider === "lmstudio") { + vscode.postMessage({ + type: "requestLmStudioModels", + text: apiConfiguration?.lmStudioBaseUrl, + }) + } + }, [selectedProvider, apiConfiguration?.ollamaBaseUrl, apiConfiguration?.lmStudioBaseUrl]) + useEffect(() => { + if (selectedProvider === "ollama" || selectedProvider === "lmstudio") { + requestLocalModels() + } + }, [selectedProvider, requestLocalModels]) + useInterval(requestLocalModels, selectedProvider === "ollama" || selectedProvider === "lmstudio" ? 2000 : null) + + const handleMessage = useCallback((event: MessageEvent) => { + const message: ExtensionMessage = event.data + if (message.type === "ollamaModels" && message.ollamaModels) { + setOllamaModels(message.ollamaModels) + } else if (message.type === "lmStudioModels" && message.lmStudioModels) { + setLmStudioModels(message.lmStudioModels) + } + }, []) + useEvent("message", handleMessage) + + /* + VSCodeDropdown has an open bug where dynamically rendered options don't auto select the provided value prop. You can see this for yourself by comparing it with normal select/option elements, which work as expected. + https://github.com/microsoft/vscode-webview-ui-toolkit/issues/433 + + In our case, when the user switches between providers, we recalculate the selectedModelId depending on the provider, the default model for that provider, and a modelId that the user may have selected. Unfortunately, the VSCodeDropdown component wouldn't select this calculated value, and would default to the first "Select a model..." option instead, which makes it seem like the model was cleared out when it wasn't. + + As a workaround, we create separate instances of the dropdown for each provider, and then conditionally render the one that matches the current provider. + */ + const createDropdown = (models: Record) => { + return ( + + Select a model... + {Object.keys(models).map((modelId) => ( + + {modelId} + + ))} + + ) + } + + return ( +
+
+ + + OpenRouter + Anthropic + Google Gemini + DeepSeek + GCP Vertex AI + AWS Bedrock + OpenAI + OpenAI Compatible + AOAI_O1 + LM Studio + Ollama + +
+ + {selectedProvider === "anthropic" && ( +
+ + Anthropic API Key + + + { + const isChecked = e.target.checked === true + setAnthropicBaseUrlSelected(isChecked) + if (!isChecked) { + setApiConfiguration({ + ...apiConfiguration, + anthropicBaseUrl: "", + }) + } + }}> + Use custom base URL + + + {anthropicBaseUrlSelected && ( + + )} + +

+ This key is stored locally and only used to make API requests from this extension. + {!apiConfiguration?.apiKey && ( + + You can get an Anthropic API key by signing up here. + + )} +

+
+ )} + + {selectedProvider === "openai-native" && ( +
+ + OpenAI API Key + +

+ This key is stored locally and only used to make API requests from this extension. + {!apiConfiguration?.openAiNativeApiKey && ( + + You can get an OpenAI API key by signing up here. + + )} +

+
+ )} + + {selectedProvider === "aoai_o1" && ( +
+ + AOAI_O1 API Key + +

+ This key is stored locally and only used to make API requests from this extension. + {!apiConfiguration?.aoaiO1ApiKey && ( + + You can get an AOAI_O1 API key by signing up here. + + )} +

+
+ )} + + {selectedProvider === "deepseek" && ( +
+ + DeepSeek API Key + +

+ This key is stored locally and only used to make API requests from this extension. + {!apiConfiguration?.deepSeekApiKey && ( + + You can get a DeepSeek API key by signing up here. + + )} +

+
+ )} + + {selectedProvider === "openrouter" && ( +
+ + OpenRouter API Key + + {!apiConfiguration?.openRouterApiKey && ( + + Get OpenRouter API Key + + )} +

+ This key is stored locally and only used to make API requests from this extension.{" "} + {/* {!apiConfiguration?.openRouterApiKey && ( + + (Note: OpenRouter is recommended for high rate + limits, prompt caching, and wider selection of models.) + + )} */} +

+
+ )} + + {selectedProvider === "bedrock" && ( +
+ + AWS Access Key + + + AWS Secret Key + + + AWS Session Token + +
+ + + Select a region... + {/* The user will have to choose a region that supports the model they use, but this shouldn't be a problem since they'd have to request access for it in that region in the first place. */} + us-east-1 + us-east-2 + {/* us-west-1 */} + us-west-2 + {/* af-south-1 */} + {/* ap-east-1 */} + ap-south-1 + ap-northeast-1 + ap-northeast-2 + {/* ap-northeast-3 */} + ap-southeast-1 + ap-southeast-2 + ca-central-1 + eu-central-1 + eu-central-2 + eu-west-1 + eu-west-2 + eu-west-3 + {/* eu-north-1 */} + {/* me-south-1 */} + sa-east-1 + us-gov-east-1 + us-gov-west-1 + {/* us-gov-east-1 */} + +
+ { + const isChecked = e.target.checked === true + setApiConfiguration({ + ...apiConfiguration, + awsUseCrossRegionInference: isChecked, + }) + }}> + Use cross-region inference + +

+ Authenticate by either providing the keys above or use the default AWS credential providers, i.e. + ~/.aws/credentials or environment variables. These credentials are only used locally to make API requests + from this extension. +

+
+ )} + + {apiConfiguration?.apiProvider === "vertex" && ( +
+ + Google Cloud Project ID + +
+ + + Select a region... + us-east5 + us-central1 + europe-west1 + europe-west4 + asia-southeast1 + +
+

+ To use Google Cloud Vertex AI, you need to + + {"1) create a Google Cloud account › enable the Vertex AI API › enable the desired Claude models,"} + {" "} + + {"2) install the Google Cloud CLI › configure Application Default Credentials."} + +

+
+ )} + + {selectedProvider === "gemini" && ( +
+ + Gemini API Key + +

+ This key is stored locally and only used to make API requests from this extension. + {!apiConfiguration?.geminiApiKey && ( + + You can get a Gemini API key by signing up here. + + )} +

+
+ )} + + {selectedProvider === "openai" && ( +
+ + Base URL + + + API Key + + + Model ID + + { + const isChecked = e.target.checked === true + setAzureApiVersionSelected(isChecked) + if (!isChecked) { + setApiConfiguration({ + ...apiConfiguration, + azureApiVersion: "", + }) + } + }}> + Set Azure API version + + {azureApiVersionSelected && ( + + )} +

+ + (Note: Cline uses complex prompts and works best with Claude + models. Less capable models may not work as expected.) + +

+
+ )} + + {selectedProvider === "lmstudio" && ( +
+ + Base URL (optional) + + + Model ID + + {lmStudioModels.length > 0 && ( + { + const value = (e.target as HTMLInputElement)?.value + // need to check value first since radio group returns empty string sometimes + if (value) { + handleInputChange("lmStudioModelId")({ + target: { value }, + }) + } + }}> + {lmStudioModels.map((model) => ( + + {model} + + ))} + + )} +

+ LM Studio allows you to run models locally on your computer. For instructions on how to get started, see + their + + quickstart guide. + + You will also need to start LM Studio's{" "} + + local server + {" "} + feature to use it with this extension.{" "} + + (Note: Cline uses complex prompts and works best with Claude + models. Less capable models may not work as expected.) + +

+
+ )} + + {selectedProvider === "ollama" && ( +
+ + Base URL (optional) + + + Model ID + + {ollamaModels.length > 0 && ( + { + const value = (e.target as HTMLInputElement)?.value + // need to check value first since radio group returns empty string sometimes + if (value) { + handleInputChange("ollamaModelId")({ + target: { value }, + }) + } + }}> + {ollamaModels.map((model) => ( + + {model} + + ))} + + )} +

+ Ollama allows you to run models locally on your computer. For instructions on how to get started, see + their + + quickstart guide. + + + (Note: Cline uses complex prompts and works best with Claude + models. Less capable models may not work as expected.) + +

+
+ )} + + {apiErrorMessage && ( +

+ {apiErrorMessage} +

+ )} + + {selectedProvider === "openrouter" && showModelOptions && } + + {selectedProvider !== "openrouter" && + selectedProvider !== "openai" && + selectedProvider !== "ollama" && + selectedProvider !== "lmstudio" && + showModelOptions && ( + <> +
+ + {selectedProvider === "anthropic" && createDropdown(anthropicModels)} + {selectedProvider === "bedrock" && createDropdown(bedrockModels)} + {selectedProvider === "vertex" && createDropdown(vertexModels)} + {selectedProvider === "gemini" && createDropdown(geminiModels)} + {selectedProvider === "openai-native" && createDropdown(openAiNativeModels)} + {selectedProvider === "deepseek" && createDropdown(deepSeekModels)} +
+ + + + )} + + {modelIdErrorMessage && ( +

+ {modelIdErrorMessage} +

+ )} +
+ ) } export function getOpenRouterAuthUrl(uriScheme?: string) { - return `https://openrouter.ai/auth?callback_url=${uriScheme || "vscode"}://saoudrizwan.claude-dev/openrouter` + return `https://openrouter.ai/auth?callback_url=${uriScheme || "vscode"}://saoudrizwan.claude-dev/openrouter` } export const formatPrice = (price: number) => { - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }).format(price) + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(price) } export const ModelInfoView = ({ - selectedModelId, - modelInfo, - isDescriptionExpanded, - setIsDescriptionExpanded, + selectedModelId, + modelInfo, + isDescriptionExpanded, + setIsDescriptionExpanded, }: { - selectedModelId: string - modelInfo: ModelInfo - isDescriptionExpanded: boolean - setIsDescriptionExpanded: (isExpanded: boolean) => void + selectedModelId: string + modelInfo: ModelInfo + isDescriptionExpanded: boolean + setIsDescriptionExpanded: (isExpanded: boolean) => void }) => { - const isGemini = Object.keys(geminiModels).includes(selectedModelId) - - const infoItems = [ - modelInfo.description && ( - - ), - , - , - !isGemini && ( - - ), - modelInfo.maxTokens !== undefined && modelInfo.maxTokens > 0 && ( - - Max output: {modelInfo.maxTokens?.toLocaleString()} tokens - - ), - modelInfo.inputPrice !== undefined && modelInfo.inputPrice > 0 && ( - - Input price: {formatPrice(modelInfo.inputPrice)}/million tokens - - ), - modelInfo.supportsPromptCache && modelInfo.cacheWritesPrice && ( - - Cache writes price: {formatPrice(modelInfo.cacheWritesPrice || 0)} - /million tokens - - ), - modelInfo.supportsPromptCache && modelInfo.cacheReadsPrice && ( - - Cache reads price: {formatPrice(modelInfo.cacheReadsPrice || 0)}/million - tokens - - ), - modelInfo.outputPrice !== undefined && modelInfo.outputPrice > 0 && ( - - Output price: {formatPrice(modelInfo.outputPrice)}/million tokens - - ), - isGemini && ( - - * Free up to {selectedModelId && selectedModelId.includes("flash") ? "15" : "2"} requests per minute. After that, - billing depends on prompt size.{" "} - - For more info, see pricing details. - - - ), - ].filter(Boolean) - - return ( -

- {infoItems.map((item, index) => ( - - {item} - {index < infoItems.length - 1 &&
} -
- ))} -

- ) + const isGemini = Object.keys(geminiModels).includes(selectedModelId) + + const infoItems = [ + modelInfo.description && ( + + ), + , + , + !isGemini && ( + + ), + modelInfo.maxTokens !== undefined && modelInfo.maxTokens > 0 && ( + + Max output: {modelInfo.maxTokens?.toLocaleString()} tokens + + ), + modelInfo.inputPrice !== undefined && modelInfo.inputPrice > 0 && ( + + Input price: {formatPrice(modelInfo.inputPrice)}/million tokens + + ), + modelInfo.supportsPromptCache && modelInfo.cacheWritesPrice && ( + + Cache writes price: {formatPrice(modelInfo.cacheWritesPrice || 0)} + /million tokens + + ), + modelInfo.supportsPromptCache && modelInfo.cacheReadsPrice && ( + + Cache reads price: {formatPrice(modelInfo.cacheReadsPrice || 0)}/million + tokens + + ), + modelInfo.outputPrice !== undefined && modelInfo.outputPrice > 0 && ( + + Output price: {formatPrice(modelInfo.outputPrice)}/million tokens + + ), + isGemini && ( + + * Free up to {selectedModelId && selectedModelId.includes("flash") ? "15" : "2"} requests per minute. After that, + billing depends on prompt size.{" "} + + For more info, see pricing details. + + + ), + ].filter(Boolean) + + return ( +

+ {infoItems.map((item, index) => ( + + {item} + {index < infoItems.length - 1 &&
} +
+ ))} +

+ ) } const ModelInfoSupportsItem = ({ - isSupported, - supportsLabel, - doesNotSupportLabel, + isSupported, + supportsLabel, + doesNotSupportLabel, }: { - isSupported: boolean - supportsLabel: string - doesNotSupportLabel: string + isSupported: boolean + supportsLabel: string + doesNotSupportLabel: string }) => ( - - - {isSupported ? supportsLabel : doesNotSupportLabel} - + + + {isSupported ? supportsLabel : doesNotSupportLabel} + ) export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) { - const provider = apiConfiguration?.apiProvider || "anthropic" - const modelId = apiConfiguration?.apiModelId - - const getProviderData = (models: Record, defaultId: string) => { - let selectedModelId: string - let selectedModelInfo: ModelInfo - if (modelId && modelId in models) { - selectedModelId = modelId - selectedModelInfo = models[modelId] - } else { - selectedModelId = defaultId - selectedModelInfo = models[defaultId] - } - return { - selectedProvider: provider, - selectedModelId, - selectedModelInfo, - } - } - switch (provider) { - case "anthropic": - return getProviderData(anthropicModels, anthropicDefaultModelId) - case "bedrock": - return getProviderData(bedrockModels, bedrockDefaultModelId) - case "vertex": - return getProviderData(vertexModels, vertexDefaultModelId) - case "gemini": - return getProviderData(geminiModels, geminiDefaultModelId) - case "openai-native": - return getProviderData(openAiNativeModels, openAiNativeDefaultModelId) - case "deepseek": - return getProviderData(deepSeekModels, deepSeekDefaultModelId) - case "openrouter": - return { - selectedProvider: provider, - selectedModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId, - selectedModelInfo: apiConfiguration?.openRouterModelInfo || openRouterDefaultModelInfo, - } - case "openai": - return { - selectedProvider: provider, - selectedModelId: apiConfiguration?.openAiModelId || "", - selectedModelInfo: openAiModelInfoSaneDefaults, - } - case "ollama": - return { - selectedProvider: provider, - selectedModelId: apiConfiguration?.ollamaModelId || "", - selectedModelInfo: openAiModelInfoSaneDefaults, - } - case "lmstudio": - return { - selectedProvider: provider, - selectedModelId: apiConfiguration?.lmStudioModelId || "", - selectedModelInfo: openAiModelInfoSaneDefaults, - } - default: - return getProviderData(anthropicModels, anthropicDefaultModelId) - } + const provider = apiConfiguration?.apiProvider || "anthropic" + const modelId = apiConfiguration?.apiModelId + + const getProviderData = (models: Record, defaultId: string) => { + let selectedModelId: string + let selectedModelInfo: ModelInfo + if (modelId && modelId in models) { + selectedModelId = modelId + selectedModelInfo = models[modelId] + } else { + selectedModelId = defaultId + selectedModelInfo = models[defaultId] + } + return { + selectedProvider: provider, + selectedModelId, + selectedModelInfo, + } + } + switch (provider) { + case "anthropic": + return getProviderData(anthropicModels, anthropicDefaultModelId) + case "bedrock": + return getProviderData(bedrockModels, bedrockDefaultModelId) + case "vertex": + return getProviderData(vertexModels, vertexDefaultModelId) + case "gemini": + return getProviderData(geminiModels, geminiDefaultModelId) + case "openai-native": + return getProviderData(openAiNativeModels, openAiNativeDefaultModelId) + case "deepseek": + return getProviderData(deepSeekModels, deepSeekDefaultModelId) + case "openrouter": + return { + selectedProvider: provider, + selectedModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId, + selectedModelInfo: apiConfiguration?.openRouterModelInfo || openRouterDefaultModelInfo, + } + case "openai": + return { + selectedProvider: provider, + selectedModelId: apiConfiguration?.openAiModelId || "", + selectedModelInfo: openAiModelInfoSaneDefaults, + } + case "ollama": + return { + selectedProvider: provider, + selectedModelId: apiConfiguration?.ollamaModelId || "", + selectedModelInfo: openAiModelInfoSaneDefaults, + } + case "lmstudio": + return { + selectedProvider: provider, + selectedModelId: apiConfiguration?.lmStudioModelId || "", + selectedModelInfo: openAiModelInfoSaneDefaults, + } + default: + return getProviderData(anthropicModels, anthropicDefaultModelId) + } } export default memo(ApiOptions)