From 9d752c308813f17f80d01a98d0dd9d5c766abf55 Mon Sep 17 00:00:00 2001 From: nang-dev Date: Wed, 5 Mar 2025 20:47:24 -0500 Subject: [PATCH 1/3] Added static --- src/api/providers/deepseek.ts | 162 ++++++++++++++++-- src/api/providers/pearai.ts | 99 +++++++++-- src/shared/api.ts | 74 +++++++- webview-ui/src/App.tsx | 4 +- webview-ui/src/components/chat/ChatView.tsx | 7 +- .../src/components/settings/ApiOptions.tsx | 18 +- 6 files changed, 312 insertions(+), 52 deletions(-) diff --git a/src/api/providers/deepseek.ts b/src/api/providers/deepseek.ts index 267a41bfffc..148bf31310e 100644 --- a/src/api/providers/deepseek.ts +++ b/src/api/providers/deepseek.ts @@ -1,24 +1,158 @@ -import { OpenAiHandler, OpenAiHandlerOptions } from "./openai" -import { ModelInfo } from "../../shared/api" -import { deepSeekModels, deepSeekDefaultModelId } from "../../shared/api" - -export class DeepSeekHandler extends OpenAiHandler { - constructor(options: OpenAiHandlerOptions) { - super({ - ...options, - openAiApiKey: options.deepSeekApiKey ?? "not-provided", - openAiModelId: options.apiModelId ?? deepSeekDefaultModelId, - openAiBaseUrl: options.deepSeekBaseUrl ?? "https://api.deepseek.com/v1", - openAiStreamingEnabled: true, - includeMaxTokens: true, +import { Anthropic } from "@anthropic-ai/sdk" +import { ApiHandlerOptions, ModelInfo, deepSeekModels, deepSeekDefaultModelId } from "../../shared/api" +import { ApiHandler, SingleCompletionHandler } from "../index" +import { convertToR1Format } from "../transform/r1-format" +import { convertToOpenAiMessages } from "../transform/openai-format" +import { ApiStream } from "../transform/stream" + +interface DeepSeekUsage { + prompt_tokens: number + completion_tokens: number + prompt_cache_miss_tokens?: number + prompt_cache_hit_tokens?: number +} + +export class DeepSeekHandler implements ApiHandler, SingleCompletionHandler { + private options: ApiHandlerOptions + + constructor(options: ApiHandlerOptions) { + if (!options.deepSeekApiKey) { + throw new Error("DeepSeek API key is required. Please provide it in the settings.") + } + this.options = options + } + + private get baseUrl(): string { + return this.options.deepSeekBaseUrl ?? "https://api.deepseek.com/v1" + } + + async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { + const modelInfo = this.getModel().info + const modelId = this.options.apiModelId ?? deepSeekDefaultModelId + const isReasoner = modelId.includes("deepseek-reasoner") + + const systemMessage = { role: "system", content: systemPrompt } + const formattedMessages = isReasoner + ? convertToR1Format([{ role: "user", content: systemPrompt }, ...messages]) + : [systemMessage, ...convertToOpenAiMessages(messages)] + + const response = await fetch(`${this.baseUrl}/chat/completions`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.options.deepSeekApiKey}`, + }, + body: JSON.stringify({ + model: modelId, + messages: formattedMessages, + temperature: 0, + stream: true, + max_tokens: modelInfo.maxTokens, + }), }) + + if (!response.ok) { + throw new Error(`DeepSeek API error: ${response.statusText}`) + } + + if (!response.body) { + throw new Error("No response body received from DeepSeek API") + } + + const reader = response.body.getReader() + const decoder = new TextDecoder() + let buffer = "" + + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split("\n") + buffer = lines.pop() || "" + + for (const line of lines) { + if (line.trim() === "") continue + if (!line.startsWith("data: ")) continue + + const data = line.slice(6) + if (data === "[DONE]") continue + + try { + const chunk = JSON.parse(data) + const delta = chunk.choices[0]?.delta ?? {} + + if (delta.content) { + yield { + type: "text", + text: delta.content, + } + } + + if ("reasoning_content" in delta && delta.reasoning_content) { + yield { + type: "reasoning", + text: delta.reasoning_content, + } + } + + if (chunk.usage) { + const usage = chunk.usage as DeepSeekUsage + let inputTokens = (usage.prompt_tokens || 0) - (usage.prompt_cache_hit_tokens || 0) + yield { + type: "usage", + inputTokens: inputTokens, + outputTokens: usage.completion_tokens || 0, + cacheReadTokens: usage.prompt_cache_hit_tokens || 0, + cacheWriteTokens: usage.prompt_cache_miss_tokens || 0, + } + } + } catch (error) { + console.error("Error parsing DeepSeek response:", error) + } + } + } + } finally { + reader.releaseLock() + } } - override getModel(): { id: string; info: ModelInfo } { + getModel(): { id: string; info: ModelInfo } { const modelId = this.options.apiModelId ?? deepSeekDefaultModelId return { id: modelId, info: deepSeekModels[modelId as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId], } } + + async completePrompt(prompt: string): Promise { + try { + const response = await fetch(`${this.baseUrl}/chat/completions`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.options.deepSeekApiKey}`, + }, + body: JSON.stringify({ + model: this.getModel().id, + messages: [{ role: "user", content: prompt }], + temperature: 0, + stream: false, + }), + }) + + if (!response.ok) { + throw new Error(`DeepSeek API error: ${response.statusText}`) + } + + const data = await response.json() + return data.choices[0]?.message?.content || "" + } catch (error) { + if (error instanceof Error) { + throw new Error(`DeepSeek completion error: ${error.message}`) + } + throw error + } + } } diff --git a/src/api/providers/pearai.ts b/src/api/providers/pearai.ts index ccd9e5fd281..f5396197318 100644 --- a/src/api/providers/pearai.ts +++ b/src/api/providers/pearai.ts @@ -1,36 +1,100 @@ -import { OpenAiHandler } from "./openai" import * as vscode from "vscode" -import { AnthropicModelId, ApiHandlerOptions, ModelInfo, PEARAI_URL } from "../../shared/api" +import { ApiHandlerOptions, PEARAI_URL, ModelInfo } from "../../shared/api" import { AnthropicHandler } from "./anthropic" +import { DeepSeekHandler } from "./deepseek" + +interface PearAiModelsResponse { + models: { + [key: string]: { + underlyingModel?: string + [key: string]: any + } + } + defaultModelId: string +} + +export class PearAiHandler { + private handler!: AnthropicHandler | DeepSeekHandler -export class PearAiHandler extends AnthropicHandler { constructor(options: ApiHandlerOptions) { if (!options.pearaiApiKey) { vscode.window.showErrorMessage("PearAI API key not found.", "Login to PearAI").then(async (selection) => { if (selection === "Login to PearAI") { const extensionUrl = `${vscode.env.uriScheme}://pearai.pearai/auth` const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(extensionUrl)) - vscode.env.openExternal( await vscode.env.asExternalUri( - vscode.Uri.parse( - `https://trypear.ai/signin?callback=${callbackUri.toString()}`, // Change to localhost if running locally - ), + vscode.Uri.parse(`https://trypear.ai/signin?callback=${callbackUri.toString()}`), ), ) } }) throw new Error("PearAI API key not found. Please login to PearAI.") } - super({ - ...options, - apiKey: options.pearaiApiKey, - anthropicBaseUrl: PEARAI_URL, + + this.initializeHandler(options).catch((error) => { + console.error("Failed to initialize PearAI handler:", error) + throw error }) } - override getModel(): { id: AnthropicModelId; info: ModelInfo } { - const baseModel = super.getModel() + private async initializeHandler(options: ApiHandlerOptions): Promise { + const modelId = options.apiModelId || "pearai-model" + + if (modelId === "pearai-model") { + try { + const response = await fetch(`${PEARAI_URL}/getPearAIAgentModels`) + if (!response.ok) { + throw new Error(`Failed to fetch models: ${response.statusText}`) + } + const data = (await response.json()) as PearAiModelsResponse + const underlyingModel = data.models[modelId]?.underlyingModel || "claude-3-5-sonnet-20241022" + console.dir(underlyingModel) + if (underlyingModel.startsWith("deepseek")) { + this.handler = new DeepSeekHandler({ + ...options, + deepSeekApiKey: options.pearaiApiKey, + deepSeekBaseUrl: PEARAI_URL, + apiModelId: underlyingModel, + }) + } else { + // Default to Claude + this.handler = new AnthropicHandler({ + ...options, + apiKey: options.pearaiApiKey, + anthropicBaseUrl: PEARAI_URL, + apiModelId: underlyingModel, + }) + } + } catch (error) { + console.error("Error fetching PearAI models:", error) + // Default to Claude if there's an error + this.handler = new AnthropicHandler({ + ...options, + apiKey: options.pearaiApiKey, + anthropicBaseUrl: PEARAI_URL, + apiModelId: "claude-3-5-sonnet-20241022", + }) + } + } else if (modelId.startsWith("claude")) { + this.handler = new AnthropicHandler({ + ...options, + apiKey: options.pearaiApiKey, + anthropicBaseUrl: PEARAI_URL, + }) + } else if (modelId.startsWith("deepseek")) { + this.handler = new DeepSeekHandler({ + ...options, + deepSeekApiKey: options.pearaiApiKey, + deepSeekBaseUrl: PEARAI_URL, + }) + } else { + throw new Error(`Unsupported model: ${modelId}`) + } + } + + getModel(): { id: string; info: ModelInfo } { + const baseModel = this.handler.getModel() return { id: baseModel.id, info: { @@ -42,4 +106,13 @@ export class PearAiHandler extends AnthropicHandler { }, } } + + async *createMessage(systemPrompt: string, messages: any[]): AsyncGenerator { + const generator = this.handler.createMessage(systemPrompt, messages) + yield* generator + } + + async completePrompt(prompt: string): Promise { + return this.handler.completePrompt(prompt) + } } diff --git a/src/shared/api.ts b/src/shared/api.ts index 3173d4a3a8d..3c918d6c07d 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -698,18 +698,22 @@ export const deepSeekModels = { maxTokens: 8192, contextWindow: 64_000, supportsImages: false, - supportsPromptCache: false, + supportsPromptCache: true, inputPrice: 0.014, // $0.014 per million tokens outputPrice: 0.28, // $0.28 per million tokens + cacheWritesPrice: 0.27, // $0.27 per million tokens (cache miss) + cacheReadsPrice: 0.07, // $0.07 per million tokens (cache hit) description: `DeepSeek-V3 achieves a significant breakthrough in inference speed over previous models. It tops the leaderboard among open-source models and rivals the most advanced closed-source models globally.`, }, "deepseek-reasoner": { maxTokens: 8192, contextWindow: 64_000, supportsImages: false, - supportsPromptCache: false, + supportsPromptCache: true, inputPrice: 0.55, // $0.55 per million tokens outputPrice: 2.19, // $2.19 per million tokens + cacheWritesPrice: 0.55, // $0.55 per million tokens (cache miss) + cacheReadsPrice: 0.14, // $0.14 per million tokens (cache hit) description: `DeepSeek-R1 achieves performance comparable to OpenAI-o1 across math, code, and reasoning tasks.`, }, } as const satisfies Record @@ -788,6 +792,68 @@ export const unboundDefaultModelInfo: ModelInfo = { } // CHANGE AS NEEDED FOR TESTING // PROD: -export const PEARAI_URL = "https://stingray-app-gb2an.ondigitalocean.app/pearai-server-api2/integrations/cline" +// export const PEARAI_URL = "https://stingray-app-gb2an.ondigitalocean.app/pearai-server-api2/integrations/cline" // DEV: -// export const PEARAI_URL = "http://localhost:8000/integrations/cline" +export const PEARAI_URL = "http://localhost:8000/integrations/cline" + +// PearAI +export type PearAiModelId = keyof typeof pearAiModels +export const pearAiDefaultModelId: PearAiModelId = "pearai-model" +export const pearAiModels = { + "pearai-model": { + maxTokens: 8192, + contextWindow: 64000, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 0.014, + outputPrice: 0.28, + cacheWritesPrice: 0.27, + cacheReadsPrice: 0.07, + description: + "DeepSeek-V3 achieves a significant breakthrough in inference speed over previous models. It tops the leaderboard among open-source models and rivals the most advanced closed-source models globally.", + }, + "claude-3-5-sonnet-20241022": { + maxTokens: 8192, + contextWindow: 200000, + supportsImages: true, + supportsComputerUse: true, + supportsPromptCache: true, + inputPrice: 3.0, + outputPrice: 15.0, + cacheWritesPrice: 3.75, + cacheReadsPrice: 0.3, + }, + "claude-3-5-haiku-20241022": { + maxTokens: 8192, + contextWindow: 200000, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 1.0, + outputPrice: 5.0, + cacheWritesPrice: 1.25, + cacheReadsPrice: 0.1, + }, + "deepseek-chat": { + maxTokens: 8192, + contextWindow: 64000, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 0.014, + outputPrice: 0.28, + cacheWritesPrice: 0.27, + cacheReadsPrice: 0.07, + description: + "DeepSeek-V3 achieves a significant breakthrough in inference speed over previous models. It tops the leaderboard among open-source models and rivals the most advanced closed-source models globally.", + }, + "deepseek-reasoner": { + maxTokens: 8192, + contextWindow: 64000, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 0.55, + outputPrice: 2.19, + cacheWritesPrice: 0.55, + cacheReadsPrice: 0.14, + description: "DeepSeek-R1 achieves performance comparable to OpenAI-o1 across math, code, and reasoning tasks.", + }, +} as const satisfies Record diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 9a0b01f4c55..740ceb11acc 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -67,9 +67,7 @@ const App = () => { // Do not conditionally load ChatView, it's expensive and there's state we // don't want to lose (user input, disableInput, askResponse promise, etc.) - return showWelcome ? ( - - ) : ( + return ( <> {tab === "settings" && setTab("chat")} />} {tab === "history" && switchTab("chat")} />} diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 79afd743191..9afb4b2524d 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -1011,7 +1011,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie flexDirection: "column-reverse", paddingBottom: "10px", }}> - {showAnnouncement && } {messages.length === 0 && ( <>
@@ -1037,17 +1036,17 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
)} - {/* + {/* // Flex layout explanation: // 1. Content div above uses flex: "1 1 0" to: - // - Grow to fill available space (flex-grow: 1) + // - Grow to fill available space (flex-grow: 1) // - Shrink when AutoApproveMenu needs space (flex-shrink: 1) // - Start from zero size (flex-basis: 0) to ensure proper distribution // minHeight: 0 allows it to shrink below its content height // // 2. AutoApproveMenu uses flex: "0 1 auto" to: // - Not grow beyond its content (flex-grow: 0) - // - Shrink when viewport is small (flex-shrink: 1) + // - Shrink when viewport is small (flex-shrink: 1) // - Use its content size as basis (flex-basis: auto) // This ensures it takes its natural height when there's space // but becomes scrollable when the viewport is too small diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index e79de3440d5..ca5c7f95299 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -38,6 +38,8 @@ import { unboundDefaultModelInfo, requestyDefaultModelId, requestyDefaultModelInfo, + pearAiModels, + pearAiDefaultModelId, } from "../../../../src/shared/api" import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage" @@ -58,6 +60,7 @@ const modelsByProvider: Record> = { "openai-native": openAiNativeModels, deepseek: deepSeekModels, mistral: mistralModels, + pearai: pearAiModels, } interface ApiOptionsProps { @@ -1544,20 +1547,7 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) { }, } case "pearai": { - // Get the base Anthropic model info - const baseModelInfo = anthropicModels[anthropicDefaultModelId] - const pearaiModelInfo: ModelInfo = { - ...baseModelInfo, - inputPrice: baseModelInfo.inputPrice, - outputPrice: baseModelInfo.outputPrice, - cacheWritesPrice: baseModelInfo.cacheWritesPrice ? baseModelInfo.cacheWritesPrice : undefined, - cacheReadsPrice: baseModelInfo.cacheWritesPrice ? baseModelInfo.cacheReadsPrice : undefined, - } - return { - selectedProvider: provider, - selectedModelId: apiConfiguration?.pearaiModelId || "pearai_model", - selectedModelInfo: pearaiModelInfo, - } + return getProviderData(pearAiModels, pearAiDefaultModelId) } default: return getProviderData(anthropicModels, anthropicDefaultModelId) From 3f7a2bc0927dbf58750221cdba398dacdc2ca213 Mon Sep 17 00:00:00 2001 From: nang-dev Date: Wed, 5 Mar 2025 21:16:35 -0500 Subject: [PATCH 2/3] Fixed endpoint --- src/shared/api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/api.ts b/src/shared/api.ts index 3c918d6c07d..c01179ace74 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -792,9 +792,9 @@ export const unboundDefaultModelInfo: ModelInfo = { } // CHANGE AS NEEDED FOR TESTING // PROD: -// export const PEARAI_URL = "https://stingray-app-gb2an.ondigitalocean.app/pearai-server-api2/integrations/cline" +export const PEARAI_URL = "https://stingray-app-gb2an.ondigitalocean.app/pearai-server-api2/integrations/cline" // DEV: -export const PEARAI_URL = "http://localhost:8000/integrations/cline" +// export const PEARAI_URL = "http://localhost:8000/integrations/cline" // PearAI export type PearAiModelId = keyof typeof pearAiModels From 99d8607cb4a35443bfc5f229cbef368309eee1bd Mon Sep 17 00:00:00 2001 From: nang-dev Date: Wed, 5 Mar 2025 21:30:21 -0500 Subject: [PATCH 3/3] Added fixes --- src/api/providers/pearai.ts | 11 ++++++++++- webview-ui/src/context/ExtensionStateContext.tsx | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/api/providers/pearai.ts b/src/api/providers/pearai.ts index f5396197318..deae483bf5a 100644 --- a/src/api/providers/pearai.ts +++ b/src/api/providers/pearai.ts @@ -32,9 +32,17 @@ export class PearAiHandler { throw new Error("PearAI API key not found. Please login to PearAI.") } + // Initialize with a default handler synchronously + this.handler = new AnthropicHandler({ + ...options, + apiKey: options.pearaiApiKey, + anthropicBaseUrl: PEARAI_URL, + apiModelId: "claude-3-5-sonnet-20241022", + }) + + // Then try to initialize the correct handler asynchronously this.initializeHandler(options).catch((error) => { console.error("Failed to initialize PearAI handler:", error) - throw error }) } @@ -94,6 +102,7 @@ export class PearAiHandler { } getModel(): { id: string; info: ModelInfo } { + console.dir(this.handler) const baseModel = this.handler.getModel() return { id: baseModel.id, diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 4c8e5498e6b..8570eb8c8c7 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -13,6 +13,7 @@ import { requestyDefaultModelId, requestyDefaultModelInfo, PEARAI_URL, + pearAiModels, } from "../../../src/shared/api" import { vscode } from "../utils/vscode" import { convertTextMateToHljs } from "../utils/textMateToHljs" @@ -153,6 +154,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode apiConfiguration: { apiProvider: "pearai", pearaiBaseUrl: PEARAI_URL, + pearaiModelId: "pearai-model", + pearaiModelInfo: pearAiModels["pearai-model"], }, }) }