diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index 6fbe1f2509..bd141b1f57 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -1,6 +1,14 @@ import { Anthropic } from "@anthropic-ai/sdk" import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming" -import { anthropicDefaultModelId, AnthropicModelId, anthropicModels, ApiHandlerOptions, ModelInfo } from "../../shared/api" +import { + anthropicDefaultAdvisorModelId, + anthropicDefaultModelId, + AnthropicModelId, + anthropicModels, + ApiHandlerOptions, + ModelInfo, + ModelType, +} from "../../shared/api" import { ApiHandler } from "../index" import { ApiStream } from "../transform/stream" @@ -16,9 +24,10 @@ export class AnthropicHandler implements ApiHandler { }) } - async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { + async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[], modelType: ModelType): ApiStream { + const model = modelType === "advisor" ? this.getAdvisorModel() : this.getModel() let stream: AnthropicStream - const modelId = this.getModel().id + const modelId = model.id switch (modelId) { // 'latest' alias does not support cache_control case "claude-3-5-sonnet-20241022": @@ -37,7 +46,7 @@ export class AnthropicHandler implements ApiHandler { stream = await this.client.beta.promptCaching.messages.create( { model: modelId, - max_tokens: this.getModel().info.maxTokens || 8192, + max_tokens: model.info.maxTokens || 8192, temperature: 0, system: [ { @@ -104,7 +113,7 @@ export class AnthropicHandler implements ApiHandler { default: { stream = (await this.client.messages.create({ model: modelId, - max_tokens: this.getModel().info.maxTokens || 8192, + max_tokens: model.info.maxTokens || 8192, temperature: 0, system: [{ text: systemPrompt, type: "text" }], messages, @@ -185,4 +194,16 @@ export class AnthropicHandler implements ApiHandler { info: anthropicModels[anthropicDefaultModelId], } } + + getAdvisorModel(): { id: string; info: ModelInfo } { + const modelId = this.options.anthropicAdvisorModelId + if (modelId && modelId in anthropicModels) { + const id = modelId as AnthropicModelId + return { id, info: anthropicModels[id] } + } + return { + id: anthropicDefaultAdvisorModelId, + info: anthropicModels[anthropicDefaultAdvisorModelId], + } + } } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 12b46b997b..53a24acc0f 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -45,6 +45,7 @@ type SecretKey = type GlobalStateKey = | "apiProvider" | "apiModelId" + | "anthropicAdvisorModelId" | "awsRegion" | "awsUseCrossRegionInference" | "vertexProjectId" @@ -382,6 +383,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { const { apiProvider, apiModelId, + anthropicAdvisorModelId, apiKey, openRouterApiKey, awsAccessKey, @@ -411,6 +413,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { } = message.apiConfiguration await this.updateGlobalState("apiProvider", apiProvider) await this.updateGlobalState("apiModelId", apiModelId) + await this.updateGlobalState("anthropicAdvisorModelId", anthropicAdvisorModelId) await this.storeSecret("apiKey", apiKey) await this.storeSecret("openRouterApiKey", openRouterApiKey) await this.storeSecret("awsAccessKey", awsAccessKey) @@ -1019,6 +1022,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { const [ storedApiProvider, apiModelId, + anthropicAdvisorModelId, apiKey, openRouterApiKey, awsAccessKey, @@ -1053,6 +1057,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { ] = await Promise.all([ this.getGlobalState("apiProvider") as Promise, this.getGlobalState("apiModelId") as Promise, + this.getGlobalState("anthropicAdvisorModelId") as Promise, this.getSecret("apiKey") as Promise, this.getSecret("openRouterApiKey") as Promise, this.getSecret("awsAccessKey") as Promise, @@ -1104,6 +1109,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { apiConfiguration: { apiProvider, apiModelId, + anthropicAdvisorModelId, apiKey, openRouterApiKey, awsAccessKey, diff --git a/src/shared/api.ts b/src/shared/api.ts index 9a1c11d582..013a063777 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -14,6 +14,7 @@ export type ApiProvider = export interface ApiHandlerOptions { apiModelId?: string apiKey?: string // anthropic + anthropicAdvisorModelId?: string anthropicBaseUrl?: string openRouterApiKey?: string openRouterModelId?: string @@ -60,10 +61,13 @@ export interface ModelInfo { description?: string } +export type ModelType = "base" | "advisor" + // Anthropic // https://docs.anthropic.com/en/docs/about-claude/models // prices updated 2025-01-02 export type AnthropicModelId = keyof typeof anthropicModels export const anthropicDefaultModelId: AnthropicModelId = "claude-3-5-sonnet-20241022" +export const anthropicDefaultAdvisorModelId: AnthropicModelId = "claude-3-opus-20240229" export const anthropicModels = { "claude-3-5-sonnet-20241022": { maxTokens: 8192, @@ -192,7 +196,6 @@ export const openRouterDefaultAdvisorModelInfo: ModelInfo = { description: "The latest and strongest model family from OpenAI, o1 is designed to spend more time thinking before responding.\n\nThe o1 models are optimized for math, science, programming, and other STEM-related tasks. They consistently exhibit PhD-level accuracy on benchmarks in physics, chemistry, and biology. Learn more in the [launch announcement](https://openai.com/o1).\n\nNote: This model is currently experimental and not suitable for production use-cases, and may be heavily rate-limited.", } -export type ModelType = "base" | "advisor" // Vertex AI // https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index f242f1a868..52e7a170d8 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -13,6 +13,8 @@ import { ApiConfiguration, ApiProvider, ModelInfo, + ModelType, + anthropicDefaultAdvisorModelId, anthropicDefaultModelId, anthropicModels, azureOpenAiDefaultApiVersion, @@ -39,6 +41,7 @@ import { useExtensionState } from "../../context/ExtensionStateContext" import { vscode } from "../../utils/vscode" import VSCodeButtonLink from "../common/VSCodeButtonLink" import OpenRouterModelPicker, { ModelDescriptionMarkdown, OPENROUTER_MODEL_PICKER_Z_INDEX } from "./OpenRouterModelPicker" +import styled from "styled-components" interface ApiOptionsProps { showModelOptions: boolean @@ -52,6 +55,21 @@ const TabPanel = ({ children, isSelected }: { children: React.ReactNode; isSelec return
{children}
} +const StyledTabButton = styled.button<{ isSelected: boolean }>` + background: transparent; + border: none; + padding: 8px 16px; + color: ${(props) => (props.isSelected ? "var(--vscode-tab-activeForeground)" : "var(--vscode-tab-inactiveForeground)")}; + cursor: pointer; + border-bottom: 2px solid ${(props) => (props.isSelected ? "var(--vscode-foreground)" : "transparent")}; + font-size: 12px; + font-weight: 500; + + &:hover { + color: var(--vscode-tab-activeForeground); + } +` + const TabButton = ({ isSelected, onClick, @@ -62,20 +80,9 @@ const TabButton = ({ children: React.ReactNode }) => { return ( - + ) } @@ -95,7 +102,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, ad }) } - const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => { + const { selectedProvider, selectedModelId, selectedModelInfo, selectedAdvisorModelId } = useMemo(() => { return normalizeApiConfiguration(apiConfiguration) }, [apiConfiguration]) @@ -138,12 +145,16 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, ad 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) => { + const createDropdown = (models: Record, modelType?: ModelType) => { return ( Select a model... {Object.keys(models).map((modelId) => ( @@ -751,6 +762,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, ad )} {selectedProvider !== "openrouter" && + selectedProvider !== "anthropic" && selectedProvider !== "openai" && selectedProvider !== "ollama" && selectedProvider !== "lmstudio" && @@ -760,7 +772,6 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, ad - {selectedProvider === "anthropic" && createDropdown(anthropicModels)} {selectedProvider === "bedrock" && createDropdown(bedrockModels)} {selectedProvider === "vertex" && createDropdown(vertexModels)} {selectedProvider === "gemini" && createDropdown(geminiModels)} @@ -778,7 +789,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, ad )} - {selectedProvider !== "openrouter" && modelIdErrorMessage && ( + {selectedProvider !== "openrouter" && selectedProvider !== "anthropic" && modelIdErrorMessage && (

)} - {selectedProvider === "openrouter" && showModelOptions && ( + {(selectedProvider === "openrouter" || selectedProvider === "anthropic") && showModelOptions && (

setSelectedTab("base")}> @@ -810,7 +821,12 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, ad This is the default driver model for Cline. It will read and edit files, run commands, and more, with your permission at each step.

- + {selectedProvider === "anthropic" && ( +
+ {createDropdown(anthropicModels, "base")} +
+ )} + {selectedProvider === "openrouter" && } {modelIdErrorMessage && (

- + {selectedProvider === "anthropic" && ( +

+ {createDropdown(anthropicModels, "advisor")} +
+ )} + {selectedProvider === "openrouter" && ( + + )} {advisorModelIdErrorMessage && (