diff --git a/src/api/index.ts b/src/api/index.ts index 2ef82f8659..e2c03e914e 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -13,6 +13,7 @@ import { ApiStream } from "./transform/stream" import { DeepSeekHandler } from "./providers/deepseek" import { MistralHandler } from "./providers/mistral" import { VsCodeLmHandler } from "./providers/vscode-lm" +import { ClineHandler } from "./providers/cline" export interface ApiHandler { createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream @@ -50,6 +51,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler { return new MistralHandler(options) case "vscode-lm": return new VsCodeLmHandler(options) + case "cline": + return new ClineHandler(options) default: return new AnthropicHandler(options) } diff --git a/src/api/providers/cline.ts b/src/api/providers/cline.ts new file mode 100644 index 0000000000..74a993f77d --- /dev/null +++ b/src/api/providers/cline.ts @@ -0,0 +1,103 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import { Stream as AnthropicStream } from "@anthropic-ai/sdk/streaming" +import { ApiHandler } from "../" +import { ApiHandlerOptions, ModelInfo } from "../../shared/api" +import { ApiStream } from "../transform/stream" + +export class ClineHandler implements ApiHandler { + private options: ApiHandlerOptions + private client: Anthropic + + constructor(options: ApiHandlerOptions) { + this.options = options + this.client = new Anthropic({ + apiKey: this.options.clineApiKey || "", + baseURL: "https://api.cline.bot/v1", + defaultHeaders: { + "X-Firebase-Token": this.options.authToken || "", + }, + }) + } + + async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { + const model = this.getModel() + const stream = await this.client.messages.create({ + model: model.id, + max_tokens: model.info.maxTokens || 8192, + temperature: 0, + system: [{ text: systemPrompt, type: "text" }], + messages, + stream: true, + }) + + for await (const chunk of stream) { + switch (chunk.type) { + case "message_start": + const usage = chunk.message.usage as any + const result: any = { + type: "usage", + inputTokens: usage.input_tokens || 0, + outputTokens: usage.output_tokens || 0, + } + if (usage.cache_creation_input_tokens) { + result.cacheWriteTokens = usage.cache_creation_input_tokens + } + if (usage.cache_read_input_tokens) { + result.cacheReadTokens = usage.cache_read_input_tokens + } + yield result + break + case "message_delta": + yield { + type: "usage", + inputTokens: 0, + outputTokens: chunk.usage.output_tokens || 0, + } + break + case "content_block_start": + switch (chunk.content_block.type) { + case "text": + if (chunk.index > 0) { + yield { + type: "text", + text: "\n", + } + } + yield { + type: "text", + text: chunk.content_block.text, + } + break + } + break + case "content_block_delta": + switch (chunk.delta.type) { + case "text_delta": + yield { + type: "text", + text: chunk.delta.text, + } + break + } + break + } + } + } + + getModel() { + return { + id: "claude-3-5-sonnet", + info: { + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: true, + supportsComputerUse: true, + supportsPromptCache: true, + inputPrice: 3.0, + outputPrice: 15.0, + cacheWritesPrice: 3.75, + cacheReadsPrice: 0.3, + }, + } + } +} diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 159d9835f3..14dcd713c7 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -139,6 +139,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { async handleSignOut() { try { await this.authManager.signOut() + await this.storeSecret("authToken", undefined) + await this.storeSecret("clineApiKey", undefined) + await this.updateGlobalState("apiProvider", "openrouter") + await this.postStateToWebview() vscode.window.showInformationMessage("Successfully logged out of Cline") } catch (error) { vscode.window.showErrorMessage("Logout failed") @@ -849,6 +853,19 @@ export class ClineProvider implements vscode.WebviewViewProvider { const clineProvider: ApiProvider = "cline" await this.updateGlobalState("apiProvider", clineProvider) + // Update API configuration with the new provider and auth token + const { apiConfiguration } = await this.getState() + const updatedConfig = { + ...apiConfiguration, + apiProvider: clineProvider, + clineApiKey: apiKey, + authToken: token, + } + + if (this.cline) { + this.cline.api = buildApiHandler(updatedConfig) + } + await this.postStateToWebview() vscode.window.showInformationMessage("Successfully logged in to Cline") } catch (error) { @@ -1236,6 +1253,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { apiModelId, apiKey, openRouterApiKey, + clineApiKey, awsAccessKey, awsSecretKey, awsSessionToken, @@ -1267,11 +1285,13 @@ export class ClineProvider implements vscode.WebviewViewProvider { vsCodeLmModelSelector, localeLanguage, userInfo, + authToken, ] = await Promise.all([ this.getGlobalState("apiProvider") as Promise, this.getGlobalState("apiModelId") as Promise, this.getSecret("apiKey") as Promise, this.getSecret("openRouterApiKey") as Promise, + this.getSecret("clineApiKey") as Promise, this.getSecret("awsAccessKey") as Promise, this.getSecret("awsSecretKey") as Promise, this.getSecret("awsSessionToken") as Promise, @@ -1303,6 +1323,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("vsCodeLmModelSelector") as Promise, this.getGlobalState("localeLanguage") as Promise, this.getGlobalState("userInfo") as Promise, + this.getSecret("authToken") as Promise, ]) let apiProvider: ApiProvider @@ -1325,6 +1346,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { apiModelId, apiKey, openRouterApiKey, + clineApiKey, awsAccessKey, awsSecretKey, awsSessionToken, @@ -1348,6 +1370,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { openRouterModelId, openRouterModelInfo, vsCodeLmModelSelector, + authToken, }, lastShownAnnouncementId, customInstructions, @@ -1434,6 +1457,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { "deepSeekApiKey", "mistralApiKey", "authToken", + "clineApiKey", ] for (const key of secretKeys) { await this.storeSecret(key, undefined) diff --git a/src/shared/api.ts b/src/shared/api.ts index d660b1bda0..c5d3a09e71 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -17,6 +17,7 @@ export interface ApiHandlerOptions { apiModelId?: string apiKey?: string // anthropic clineApiKey?: string + authToken?: string // firebase auth token for cline provider anthropicBaseUrl?: string openRouterApiKey?: string openRouterModelId?: string diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index beafc65572..33365f5fdb 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -43,6 +43,11 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s return "You must provide a valid API key or choose a different provider." } break + case "cline": + if (!apiConfiguration.clineApiKey) { + return "You must provide a valid API key or choose a different provider." + } + break case "openai": if (!apiConfiguration.openAiBaseUrl || !apiConfiguration.openAiApiKey || !apiConfiguration.openAiModelId) { return "You must provide a valid base URL, API key, and model ID."