diff --git a/.changeset/big-plums-wave.md b/.changeset/big-plums-wave.md new file mode 100644 index 0000000000..a3c836e45f --- /dev/null +++ b/.changeset/big-plums-wave.md @@ -0,0 +1,5 @@ +--- +"claude-dev": minor +--- + +Adding Requesty API Provider diff --git a/.changeset/breezy-bobcats-change.md b/.changeset/breezy-bobcats-change.md new file mode 100644 index 0000000000..0a99f60a05 --- /dev/null +++ b/.changeset/breezy-bobcats-change.md @@ -0,0 +1,5 @@ +--- +"claude-dev": patch +--- + +Change default OpenRouter model to anthropic/claude-3.5-sonnet diff --git a/.changeset/green-oranges-sit.md b/.changeset/green-oranges-sit.md new file mode 100644 index 0000000000..4d2bfebfd4 --- /dev/null +++ b/.changeset/green-oranges-sit.md @@ -0,0 +1,5 @@ +--- +"claude-dev": minor +--- + +Add Together API Provider diff --git a/.changeset/little-pianos-juggle.md b/.changeset/little-pianos-juggle.md new file mode 100644 index 0000000000..02457339e9 --- /dev/null +++ b/.changeset/little-pianos-juggle.md @@ -0,0 +1,5 @@ +--- +"claude-dev": minor +--- + +Adding reasoning_effort support for openrouter and openai-native diff --git a/.changeset/tasty-readers-move.md b/.changeset/tasty-readers-move.md new file mode 100644 index 0000000000..3d0db43570 --- /dev/null +++ b/.changeset/tasty-readers-move.md @@ -0,0 +1,5 @@ +--- +"claude-dev": minor +--- + +Added support for AWS provider profiles using the AWS CLI to make the profile. enabling long lived connections to AWS bedrock diff --git a/package-lock.json b/package-lock.json index 25b4a9e508..d193735592 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-dev", - "version": "3.2.12", + "version": "3.2.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-dev", - "version": "3.2.12", + "version": "3.2.13", "license": "Apache-2.0", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", @@ -36,7 +36,7 @@ "isbinaryfile": "^5.0.2", "mammoth": "^1.8.0", "monaco-vscode-textmate-theme-converter": "^0.1.7", - "openai": "^4.82.0", + "openai": "^4.83.0", "os-name": "^6.0.0", "p-wait-for": "^5.0.2", "pdf-parse": "^1.1.1", @@ -10617,9 +10617,9 @@ } }, "node_modules/openai": { - "version": "4.82.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.82.0.tgz", - "integrity": "sha512-1bTxOVGZuVGsKKUWbh3BEwX1QxIXUftJv+9COhhGGVDTFwiaOd4gWsMynF2ewj1mg6by3/O+U8+EEHpWRdPaJg==", + "version": "4.83.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.83.0.tgz", + "integrity": "sha512-fmTsqud0uTtRKsPC7L8Lu55dkaTwYucqncDHzVvO64DKOpNTuiYwjbR/nVgpapXuYy8xSnhQQPUm+3jQaxICgw==", "license": "Apache-2.0", "dependencies": { "@types/node": "^18.11.18", diff --git a/package.json b/package.json index c2d4bfc4e4..3149914521 100644 --- a/package.json +++ b/package.json @@ -161,6 +161,16 @@ "type": "boolean", "default": true, "description": "Enables extension to save checkpoints of workspace throughout the task." + }, + "cline.modelSettings.o3Mini.reasoningEffort": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ], + "default": "medium", + "description": "Controls the reasoning effort when using the o3-mini model. Higher values may result in more thorough but slower responses." } } } @@ -239,7 +249,7 @@ "isbinaryfile": "^5.0.2", "mammoth": "^1.8.0", "monaco-vscode-textmate-theme-converter": "^0.1.7", - "openai": "^4.82.0", + "openai": "^4.83.0", "os-name": "^6.0.0", "p-wait-for": "^5.0.2", "pdf-parse": "^1.1.1", diff --git a/src/api/index.ts b/src/api/index.ts index 680eb53232..4681ec03af 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -11,6 +11,8 @@ import { GeminiHandler } from "./providers/gemini" import { OpenAiNativeHandler } from "./providers/openai-native" import { ApiStream } from "./transform/stream" import { DeepSeekHandler } from "./providers/deepseek" +import { RequestyHandler } from "./providers/requesty" +import { TogetherHandler } from "./providers/together" import { QwenHandler } from "./providers/qwen" import { MistralHandler } from "./providers/mistral" import { VsCodeLmHandler } from "./providers/vscode-lm" @@ -48,6 +50,10 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler { return new OpenAiNativeHandler(options) case "deepseek": return new DeepSeekHandler(options) + case "requesty": + return new RequestyHandler(options) + case "together": + return new TogetherHandler(options) case "qwen": return new QwenHandler(options) case "mistral": diff --git a/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts index a82a71b8ef..448190291d 100644 --- a/src/api/providers/bedrock.ts +++ b/src/api/providers/bedrock.ts @@ -3,6 +3,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import { ApiHandler } from "../" import { ApiHandlerOptions, bedrockDefaultModelId, BedrockModelId, bedrockModels, ModelInfo } from "../../shared/api" import { ApiStream } from "../transform/stream" +import { fromIni } from "@aws-sdk/credential-providers" // https://docs.anthropic.com/en/api/claude-on-amazon-bedrock export class AwsBedrockHandler implements ApiHandler { @@ -11,17 +12,31 @@ export class AwsBedrockHandler implements ApiHandler { constructor(options: ApiHandlerOptions) { this.options = options - this.client = new AnthropicBedrock({ - // Authenticate by either providing the keys below or use the default AWS credential providers, such as - // using ~/.aws/credentials or the "AWS_SECRET_ACCESS_KEY" and "AWS_ACCESS_KEY_ID" environment variables. - ...(this.options.awsAccessKey ? { awsAccessKey: this.options.awsAccessKey } : {}), - ...(this.options.awsSecretKey ? { awsSecretKey: this.options.awsSecretKey } : {}), - ...(this.options.awsSessionToken ? { awsSessionToken: this.options.awsSessionToken } : {}), - // awsRegion changes the aws region to which the request is made. By default, we read AWS_REGION, - // and if that's not present, we default to us-east-1. Note that we do not read ~/.aws/config for the region. - awsRegion: this.options.awsRegion, - }) + const clientConfig: any = { + awsRegion: this.options.awsRegion || "us-east-1", + } + + if (this.options.awsUseProfile) { + // Use profile-based credentials if enabled + if (this.options.awsProfile) { + clientConfig.credentials = fromIni({ + profile: this.options.awsProfile, + }) + } else { + // Use default profile if no specific profile is set + clientConfig.credentials = fromIni() + } + } else if (this.options.awsAccessKey && this.options.awsSecretKey) { + // Use direct credentials if provided + clientConfig.awsAccessKey = this.options.awsAccessKey + clientConfig.awsSecretKey = this.options.awsSecretKey + if (this.options.awsSessionToken) { + clientConfig.awsSessionToken = this.options.awsSessionToken + } + } + + this.client = new AnthropicBedrock(clientConfig) } async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index 63f30bd61c..c90aa6f862 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -11,6 +11,7 @@ import { } from "../../shared/api" import { convertToOpenAiMessages } from "../transform/openai-format" import { ApiStream } from "../transform/stream" +import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions.mjs" export class OpenAiNativeHandler implements ApiHandler { private options: ApiHandlerOptions @@ -51,6 +52,7 @@ export class OpenAiNativeHandler implements ApiHandler { messages: [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)], stream: true, stream_options: { include_usage: true }, + reasoning_effort: (this.options.o3MiniReasoningEffort as ChatCompletionReasoningEffort) || "medium", }) for await (const chunk of stream) { const delta = chunk.choices[0]?.delta diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 45cbd2c7ac..025c7eb1d3 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -130,6 +130,7 @@ export class OpenRouterHandler implements ApiHandler { stream: true, transforms: shouldApplyMiddleOutTransform ? ["middle-out"] : undefined, include_reasoning: true, + ...(model.id === "openai/o3-mini" ? { reasoning_effort: this.options.o3MiniReasoningEffort || "medium" } : {}), }) let genId: string | undefined diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts new file mode 100644 index 0000000000..34dd2b4d67 --- /dev/null +++ b/src/api/providers/requesty.ts @@ -0,0 +1,75 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" +import { withRetry } from "../retry" +import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api" +import { ApiHandler } from "../index" +import { convertToOpenAiMessages } from "../transform/openai-format" +import { ApiStream } from "../transform/stream" +import { convertToR1Format } from "../transform/r1-format" + +export class RequestyHandler implements ApiHandler { + private options: ApiHandlerOptions + private client: OpenAI + + constructor(options: ApiHandlerOptions) { + this.options = options + this.client = new OpenAI({ + baseURL: "https://router.requesty.ai/v1", + apiKey: this.options.requestyApiKey, + }) + } + + @withRetry() + async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { + const modelId = this.options.requestyModelId ?? "" + const isDeepseekReasoner = modelId.includes("deepseek-reasoner") + + let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [ + { role: "system", content: systemPrompt }, + ...convertToOpenAiMessages(messages), + ] + + if (isDeepseekReasoner) { + openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages]) + } + + const stream = await this.client.chat.completions.create({ + model: modelId, + 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 (delta && "reasoning_content" in delta && delta.reasoning_content) { + yield { + type: "reasoning", + reasoning: (delta.reasoning_content as string | undefined) || "", + } + } + + 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.requestyModelId ?? "", + info: openAiModelInfoSaneDefaults, + } + } +} diff --git a/src/api/providers/together.ts b/src/api/providers/together.ts new file mode 100644 index 0000000000..38ae26dd5b --- /dev/null +++ b/src/api/providers/together.ts @@ -0,0 +1,75 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" +import { withRetry } from "../retry" +import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api" +import { ApiHandler } from "../index" +import { convertToOpenAiMessages } from "../transform/openai-format" +import { ApiStream } from "../transform/stream" +import { convertToR1Format } from "../transform/r1-format" + +export class TogetherHandler implements ApiHandler { + private options: ApiHandlerOptions + private client: OpenAI + + constructor(options: ApiHandlerOptions) { + this.options = options + this.client = new OpenAI({ + baseURL: "https://api.together.xyz/v1", + apiKey: this.options.togetherApiKey, + }) + } + + @withRetry() + async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { + const modelId = this.options.togetherModelId ?? "" + const isDeepseekReasoner = modelId.includes("deepseek-reasoner") + + let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [ + { role: "system", content: systemPrompt }, + ...convertToOpenAiMessages(messages), + ] + + if (isDeepseekReasoner) { + openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages]) + } + + const stream = await this.client.chat.completions.create({ + model: modelId, + 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 (delta && "reasoning_content" in delta && delta.reasoning_content) { + yield { + type: "reasoning", + reasoning: (delta.reasoning_content as string | undefined) || "", + } + } + + 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.togetherModelId ?? "", + info: openAiModelInfoSaneDefaults, + } + } +} diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 415656ffe6..0f4758ff92 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -45,6 +45,8 @@ type SecretKey = | "geminiApiKey" | "openAiNativeApiKey" | "deepSeekApiKey" + | "requestyApiKey" + | "togetherApiKey" | "qwenApiKey" | "mistralApiKey" | "authToken" @@ -54,6 +56,8 @@ type GlobalStateKey = | "apiModelId" | "awsRegion" | "awsUseCrossRegionInference" + | "awsProfile" + | "awsUseProfile" | "vertexProjectId" | "vertexRegion" | "lastShownAnnouncementId" @@ -80,6 +84,8 @@ type GlobalStateKey = | "liteLlmBaseUrl" | "liteLlmModelId" | "qwenApiLine" + | "requestyModelId" + | "togetherModelId" export const GlobalFileNames = { apiConversationHistory: "api_conversation_history.json", @@ -429,6 +435,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { awsSessionToken, awsRegion, awsUseCrossRegionInference, + awsProfile, + awsUseProfile, vertexProjectId, vertexRegion, openAiBaseUrl, @@ -442,6 +450,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { geminiApiKey, openAiNativeApiKey, deepSeekApiKey, + requestyApiKey, + requestyModelId, + togetherApiKey, + togetherModelId, qwenApiKey, mistralApiKey, azureApiVersion, @@ -461,6 +473,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.storeSecret("awsSessionToken", awsSessionToken) await this.updateGlobalState("awsRegion", awsRegion) await this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference) + await this.updateGlobalState("awsProfile", awsProfile) + await this.updateGlobalState("awsUseProfile", awsUseProfile) await this.updateGlobalState("vertexProjectId", vertexProjectId) await this.updateGlobalState("vertexRegion", vertexRegion) await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl) @@ -474,6 +488,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.storeSecret("geminiApiKey", geminiApiKey) await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey) await this.storeSecret("deepSeekApiKey", deepSeekApiKey) + await this.storeSecret("requestyApiKey", requestyApiKey) + await this.storeSecret("togetherApiKey", togetherApiKey) await this.storeSecret("qwenApiKey", qwenApiKey) await this.storeSecret("mistralApiKey", mistralApiKey) await this.updateGlobalState("azureApiVersion", azureApiVersion) @@ -483,6 +499,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("liteLlmBaseUrl", liteLlmBaseUrl) await this.updateGlobalState("liteLlmModelId", liteLlmModelId) await this.updateGlobalState("qwenApiLine", qwenApiLine) + await this.updateGlobalState("requestyModelId", requestyModelId) + await this.updateGlobalState("togetherModelId", togetherModelId) if (this.cline) { this.cline.api = buildApiHandler(message.apiConfiguration) } @@ -1358,6 +1376,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { awsSessionToken, awsRegion, awsUseCrossRegionInference, + awsProfile, + awsUseProfile, vertexProjectId, vertexRegion, openAiBaseUrl, @@ -1371,6 +1391,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { geminiApiKey, openAiNativeApiKey, deepSeekApiKey, + requestyApiKey, + requestyModelId, + togetherApiKey, + togetherModelId, qwenApiKey, mistralApiKey, azureApiVersion, @@ -1401,6 +1425,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getSecret("awsSessionToken") as Promise, this.getGlobalState("awsRegion") as Promise, this.getGlobalState("awsUseCrossRegionInference") as Promise, + this.getGlobalState("awsProfile") as Promise, + this.getGlobalState("awsUseProfile") as Promise, this.getGlobalState("vertexProjectId") as Promise, this.getGlobalState("vertexRegion") as Promise, this.getGlobalState("openAiBaseUrl") as Promise, @@ -1414,6 +1440,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getSecret("geminiApiKey") as Promise, this.getSecret("openAiNativeApiKey") as Promise, this.getSecret("deepSeekApiKey") as Promise, + this.getSecret("requestyApiKey") as Promise, + this.getGlobalState("requestyModelId") as Promise, + this.getSecret("togetherApiKey") as Promise, + this.getGlobalState("togetherModelId") as Promise, this.getSecret("qwenApiKey") as Promise, this.getSecret("mistralApiKey") as Promise, this.getGlobalState("azureApiVersion") as Promise, @@ -1450,6 +1480,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { } } + const o3MiniReasoningEffort = vscode.workspace + .getConfiguration("cline.modelSettings.o3Mini") + .get("reasoningEffort", "medium") + return { apiConfiguration: { apiProvider, @@ -1461,6 +1495,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { awsSessionToken, awsRegion, awsUseCrossRegionInference, + awsProfile, + awsUseProfile, vertexProjectId, vertexRegion, openAiBaseUrl, @@ -1474,6 +1510,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { geminiApiKey, openAiNativeApiKey, deepSeekApiKey, + requestyApiKey, + requestyModelId, + togetherApiKey, + togetherModelId, qwenApiKey, qwenApiLine, mistralApiKey, @@ -1481,6 +1521,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { openRouterModelId, openRouterModelInfo, vsCodeLmModelSelector, + o3MiniReasoningEffort, liteLlmBaseUrl, liteLlmModelId, }, @@ -1571,6 +1612,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { "geminiApiKey", "openAiNativeApiKey", "deepSeekApiKey", + "requestyApiKey", + "togetherApiKey", "qwenApiKey", "mistralApiKey", "authToken", diff --git a/src/shared/api.ts b/src/shared/api.ts index 24f93e96cf..748fb54e09 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -8,6 +8,8 @@ export type ApiProvider = | "lmstudio" | "gemini" | "openai-native" + | "requesty" + | "together" | "deepseek" | "qwen" | "mistral" @@ -28,6 +30,8 @@ export interface ApiHandlerOptions { awsSessionToken?: string awsRegion?: string awsUseCrossRegionInference?: boolean + awsUseProfile?: boolean + awsProfile?: string vertexProjectId?: string vertexRegion?: string openAiBaseUrl?: string @@ -40,10 +44,15 @@ export interface ApiHandlerOptions { geminiApiKey?: string openAiNativeApiKey?: string deepSeekApiKey?: string + requestyApiKey?: string + requestyModelId?: string + togetherApiKey?: string + togetherModelId?: string qwenApiKey?: string mistralApiKey?: string azureApiVersion?: string vsCodeLmModelSelector?: any + o3MiniReasoningEffort?: string qwenApiLine?: string } @@ -172,7 +181,7 @@ export const bedrockModels = { // 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 openRouterDefaultModelId = "anthropic/claude-3.5-sonnet" // will always exist in openRouterModels export const openRouterDefaultModelInfo: ModelInfo = { maxTokens: 8192, contextWindow: 200_000, @@ -184,7 +193,7 @@ export const openRouterDefaultModelInfo: ModelInfo = { 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._", + "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", } // Vertex AI diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index cbbf18198d..f5f69517c3 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -187,6 +187,8 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is AWS Bedrock OpenAI OpenAI Compatible + Requesty + Together VS Code LM API LM Studio Ollama @@ -315,7 +317,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is {selectedProvider === "qwen" && (
- + @@ -444,30 +446,56 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is flexDirection: "column", gap: 5, }}> - - AWS Access Key - - - AWS Secret Key - - - AWS Session Token - + { + const value = (e.target as HTMLInputElement)?.value + const useProfile = value === "profile" + setApiConfiguration({ + ...apiConfiguration, + awsUseProfile: useProfile, + }) + }}> + AWS Credentials + AWS Profile + + + {apiConfiguration?.awsUseProfile ? ( + + AWS Profile Name + + ) : ( + <> + + AWS Access Key + + + AWS Secret Key + + + AWS Session Token + + + )}
)} @@ -673,6 +710,68 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is )} + {selectedProvider === "requesty" && ( +
+ + API Key + + + Model ID + +

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

+
+ )} + + {selectedProvider === "together" && ( +
+ + API Key + + + Model ID + +

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

+
+ )} + {selectedProvider === "vscode-lm" && (
diff --git a/webview-ui/src/components/settings/OpenRouterModelPicker.tsx b/webview-ui/src/components/settings/OpenRouterModelPicker.tsx index 37b0bbfad3..889a636ccb 100644 --- a/webview-ui/src/components/settings/OpenRouterModelPicker.tsx +++ b/webview-ui/src/components/settings/OpenRouterModelPicker.tsx @@ -228,8 +228,8 @@ const OpenRouterModelPicker: React.FC = ({ isPopup } If you're unsure which model to choose, Cline works best with{" "} handleModelChange("anthropic/claude-3.5-sonnet:beta")}> - anthropic/claude-3.5-sonnet:beta. + onClick={() => handleModelChange("anthropic/claude-3.5-sonnet")}> + anthropic/claude-3.5-sonnet. You can also try searching "free" for no-cost options currently available. diff --git a/webview-ui/src/components/settings/__tests__/APIOptions.spec.tsx b/webview-ui/src/components/settings/__tests__/APIOptions.spec.tsx new file mode 100644 index 0000000000..5a074d43b8 --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/APIOptions.spec.tsx @@ -0,0 +1,96 @@ +import { render, screen } from "@testing-library/react" +import { describe, it, expect, vi } from "vitest" +import ApiOptions from "../ApiOptions" +import { ExtensionStateContextProvider } from "../../../context/ExtensionStateContext" + +vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + useExtensionState: vi.fn(() => ({ + apiConfiguration: { + apiProvider: "requesty", + requestyApiKey: "", + requestyModelId: "", + }, + setApiConfiguration: vi.fn(), + uriScheme: "vscode", + })), + } +}) + +describe("ApiOptions Component", () => { + vi.clearAllMocks() + const mockPostMessage = vi.fn() + + beforeEach(() => { + global.vscode = { postMessage: mockPostMessage } as any + }) + + it("renders Requesty API Key input", () => { + render( + + + , + ) + const apiKeyInput = screen.getByPlaceholderText("Enter API Key...") + expect(apiKeyInput).toBeInTheDocument() + }) + + it("renders Requesty Model ID input", () => { + render( + + + , + ) + const modelIdInput = screen.getByPlaceholderText("Enter Model ID...") + expect(modelIdInput).toBeInTheDocument() + }) +}) + +vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + // your mocked methods + useExtensionState: vi.fn(() => ({ + apiConfiguration: { + apiProvider: "together", + requestyApiKey: "", + requestyModelId: "", + }, + setApiConfiguration: vi.fn(), + uriScheme: "vscode", + })), + } +}) + +describe("ApiOptions Component", () => { + vi.clearAllMocks() + const mockPostMessage = vi.fn() + + beforeEach(() => { + global.vscode = { postMessage: mockPostMessage } as any + }) + + it("renders Together API Key input", () => { + render( + + + , + ) + const apiKeyInput = screen.getByPlaceholderText("Enter API Key...") + expect(apiKeyInput).toBeInTheDocument() + }) + + it("renders Together Model ID input", () => { + render( + + + , + ) + const modelIdInput = screen.getByPlaceholderText("Enter Model ID...") + expect(modelIdInput).toBeInTheDocument() + }) +}) diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 99a8cca3f9..a02b6f121a 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -67,6 +67,8 @@ export const ExtensionStateContextProvider: React.FC<{ config.geminiApiKey, config.openAiNativeApiKey, config.deepSeekApiKey, + config.requestyApiKey, + config.togetherApiKey, config.qwenApiKey, config.mistralApiKey, config.vsCodeLmModelSelector, diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 4617fb2c70..faa0434814 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -53,6 +53,16 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s return "You must provide a valid base URL, API key, and model ID." } break + case "requesty": + if (!apiConfiguration.requestyApiKey || !apiConfiguration.requestyModelId) { + return "You must provide a valid API key or choose a different provider." + } + break + case "together": + if (!apiConfiguration.togetherApiKey || !apiConfiguration.togetherModelId) { + return "You must provide a valid API key or choose a different provider." + } + break case "ollama": if (!apiConfiguration.ollamaModelId) { return "You must provide a valid model ID."