Merge branch 'main' into pass-controller-to-services

This commit is contained in:
Evan 2025-02-07 14:38:42 -08:00
commit db3b356bb0
20 changed files with 517 additions and 49 deletions

View file

@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Adding Requesty API Provider

View file

@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Change default OpenRouter model to anthropic/claude-3.5-sonnet

View file

@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add Together API Provider

View file

@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Adding reasoning_effort support for openrouter and openai-native

View file

@ -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

12
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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":

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -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,
}
}
}

View file

@ -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,
}
}
}

View file

@ -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<string | undefined>,
this.getGlobalState("awsRegion") as Promise<string | undefined>,
this.getGlobalState("awsUseCrossRegionInference") as Promise<boolean | undefined>,
this.getGlobalState("awsProfile") as Promise<string | undefined>,
this.getGlobalState("awsUseProfile") as Promise<boolean | undefined>,
this.getGlobalState("vertexProjectId") as Promise<string | undefined>,
this.getGlobalState("vertexRegion") as Promise<string | undefined>,
this.getGlobalState("openAiBaseUrl") as Promise<string | undefined>,
@ -1414,6 +1440,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getSecret("geminiApiKey") as Promise<string | undefined>,
this.getSecret("openAiNativeApiKey") as Promise<string | undefined>,
this.getSecret("deepSeekApiKey") as Promise<string | undefined>,
this.getSecret("requestyApiKey") as Promise<string | undefined>,
this.getGlobalState("requestyModelId") as Promise<string | undefined>,
this.getSecret("togetherApiKey") as Promise<string | undefined>,
this.getGlobalState("togetherModelId") as Promise<string | undefined>,
this.getSecret("qwenApiKey") as Promise<string | undefined>,
this.getSecret("mistralApiKey") as Promise<string | undefined>,
this.getGlobalState("azureApiVersion") as Promise<string | undefined>,
@ -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",

View file

@ -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

View file

@ -187,6 +187,8 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
<VSCodeOption value="openai-native">OpenAI</VSCodeOption>
<VSCodeOption value="openai">OpenAI Compatible</VSCodeOption>
<VSCodeOption value="requesty">Requesty</VSCodeOption>
<VSCodeOption value="together">Together</VSCodeOption>
<VSCodeOption value="vscode-lm">VS Code LM API</VSCodeOption>
<VSCodeOption value="lmstudio">LM Studio</VSCodeOption>
<VSCodeOption value="ollama">Ollama</VSCodeOption>
@ -315,7 +317,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
{selectedProvider === "qwen" && (
<div>
<DropdownContainer className="dropdown-container">
<DropdownContainer className="dropdown-container" style={{ position: "inherit" }}>
<label htmlFor="qwen-line-provider">
<span style={{ fontWeight: 500, marginTop: 5 }}>Alibaba API Line</span>
</label>
@ -444,30 +446,56 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
flexDirection: "column",
gap: 5,
}}>
<VSCodeTextField
value={apiConfiguration?.awsAccessKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("awsAccessKey")}
placeholder="Enter Access Key...">
<span style={{ fontWeight: 500 }}>AWS Access Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.awsSecretKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("awsSecretKey")}
placeholder="Enter Secret Key...">
<span style={{ fontWeight: 500 }}>AWS Secret Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.awsSessionToken || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("awsSessionToken")}
placeholder="Enter Session Token...">
<span style={{ fontWeight: 500 }}>AWS Session Token</span>
</VSCodeTextField>
<VSCodeRadioGroup
value={apiConfiguration?.awsUseProfile ? "profile" : "credentials"}
onChange={(e) => {
const value = (e.target as HTMLInputElement)?.value
const useProfile = value === "profile"
setApiConfiguration({
...apiConfiguration,
awsUseProfile: useProfile,
})
}}>
<VSCodeRadio value="credentials">AWS Credentials</VSCodeRadio>
<VSCodeRadio value="profile">AWS Profile</VSCodeRadio>
</VSCodeRadioGroup>
{apiConfiguration?.awsUseProfile ? (
<VSCodeTextField
value={apiConfiguration?.awsProfile || ""}
style={{ width: "100%" }}
onInput={handleInputChange("awsProfile")}
placeholder="Enter profile name (default if empty)">
<span style={{ fontWeight: 500 }}>AWS Profile Name</span>
</VSCodeTextField>
) : (
<>
<VSCodeTextField
value={apiConfiguration?.awsAccessKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("awsAccessKey")}
placeholder="Enter Access Key...">
<span style={{ fontWeight: 500 }}>AWS Access Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.awsSecretKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("awsSecretKey")}
placeholder="Enter Secret Key...">
<span style={{ fontWeight: 500 }}>AWS Secret Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.awsSessionToken || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("awsSessionToken")}
placeholder="Enter Session Token...">
<span style={{ fontWeight: 500 }}>AWS Session Token</span>
</VSCodeTextField>
</>
)}
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 1} className="dropdown-container">
<label htmlFor="aws-region-dropdown">
<span style={{ fontWeight: 500 }}>AWS Region</span>
@ -522,9 +550,18 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
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?.awsUseProfile ? (
<>
Using AWS Profile credentials from ~/.aws/credentials. Leave profile name empty to use the default
profile. These credentials are only used locally to make API requests from this extension.
</>
) : (
<>
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.
</>
)}
</p>
</div>
)}
@ -673,6 +710,68 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
</div>
)}
{selectedProvider === "requesty" && (
<div>
<VSCodeTextField
value={apiConfiguration?.requestyApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("requestyApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>API Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.requestyModelId || ""}
style={{ width: "100%" }}
onInput={handleInputChange("requestyModelId")}
placeholder={"Enter Model ID..."}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
<span style={{ color: "var(--vscode-errorForeground)" }}>
(<span style={{ fontWeight: 500 }}>Note:</span> Cline uses complex prompts and works best with Claude
models. Less capable models may not work as expected.)
</span>
</p>
</div>
)}
{selectedProvider === "together" && (
<div>
<VSCodeTextField
value={apiConfiguration?.togetherApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("togetherApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>API Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.togetherModelId || ""}
style={{ width: "100%" }}
onInput={handleInputChange("togetherModelId")}
placeholder={"Enter Model ID..."}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
<span style={{ color: "var(--vscode-errorForeground)" }}>
(<span style={{ fontWeight: 500 }}>Note:</span> Cline uses complex prompts and works best with Claude
models. Less capable models may not work as expected.)
</span>
</p>
</div>
)}
{selectedProvider === "vscode-lm" && (
<div>
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 2} className="dropdown-container">

View file

@ -228,8 +228,8 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
If you're unsure which model to choose, Cline works best with{" "}
<VSCodeLink
style={{ display: "inline", fontSize: "inherit" }}
onClick={() => 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.
</VSCodeLink>
You can also try searching "free" for no-cost options currently available.
</>

View file

@ -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(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
</ExtensionStateContextProvider>,
)
const apiKeyInput = screen.getByPlaceholderText("Enter API Key...")
expect(apiKeyInput).toBeInTheDocument()
})
it("renders Requesty Model ID input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
</ExtensionStateContextProvider>,
)
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(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
</ExtensionStateContextProvider>,
)
const apiKeyInput = screen.getByPlaceholderText("Enter API Key...")
expect(apiKeyInput).toBeInTheDocument()
})
it("renders Together Model ID input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
</ExtensionStateContextProvider>,
)
const modelIdInput = screen.getByPlaceholderText("Enter Model ID...")
expect(modelIdInput).toBeInTheDocument()
})
})

View file

@ -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,

View file

@ -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."