Skip to content

Commit 3fa0b6a

Browse files
authored
Merge pull request #20 from trypear/pearai-models-4
Add deepseek for agent and also dynamically control defaults
2 parents 91aa9f2 + 650fdb2 commit 3fa0b6a

File tree

5 files changed

+359
-74
lines changed

5 files changed

+359
-74
lines changed

src/api/providers/deepseek.ts

Lines changed: 148 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,158 @@
1-
import { OpenAiHandler, OpenAiHandlerOptions } from "./openai"
2-
import { ModelInfo } from "../../shared/api"
3-
import { deepSeekModels, deepSeekDefaultModelId } from "../../shared/api"
4-
5-
export class DeepSeekHandler extends OpenAiHandler {
6-
constructor(options: OpenAiHandlerOptions) {
7-
super({
8-
...options,
9-
openAiApiKey: options.deepSeekApiKey ?? "not-provided",
10-
openAiModelId: options.apiModelId ?? deepSeekDefaultModelId,
11-
openAiBaseUrl: options.deepSeekBaseUrl ?? "https://api.deepseek.com/v1",
12-
openAiStreamingEnabled: true,
13-
includeMaxTokens: true,
1+
import { Anthropic } from "@anthropic-ai/sdk"
2+
import { ApiHandlerOptions, ModelInfo, deepSeekModels, deepSeekDefaultModelId } from "../../shared/api"
3+
import { ApiHandler, SingleCompletionHandler } from "../index"
4+
import { convertToR1Format } from "../transform/r1-format"
5+
import { convertToOpenAiMessages } from "../transform/openai-format"
6+
import { ApiStream } from "../transform/stream"
7+
8+
interface DeepSeekUsage {
9+
prompt_tokens: number
10+
completion_tokens: number
11+
prompt_cache_miss_tokens?: number
12+
prompt_cache_hit_tokens?: number
13+
}
14+
15+
export class DeepSeekHandler implements ApiHandler, SingleCompletionHandler {
16+
private options: ApiHandlerOptions
17+
18+
constructor(options: ApiHandlerOptions) {
19+
if (!options.deepSeekApiKey) {
20+
throw new Error("DeepSeek API key is required. Please provide it in the settings.")
21+
}
22+
this.options = options
23+
}
24+
25+
private get baseUrl(): string {
26+
return this.options.deepSeekBaseUrl ?? "https://api.deepseek.com/v1"
27+
}
28+
29+
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
30+
const modelInfo = this.getModel().info
31+
const modelId = this.options.apiModelId ?? deepSeekDefaultModelId
32+
const isReasoner = modelId.includes("deepseek-reasoner")
33+
34+
const systemMessage = { role: "system", content: systemPrompt }
35+
const formattedMessages = isReasoner
36+
? convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
37+
: [systemMessage, ...convertToOpenAiMessages(messages)]
38+
39+
const response = await fetch(`${this.baseUrl}/chat/completions`, {
40+
method: "POST",
41+
headers: {
42+
"Content-Type": "application/json",
43+
Authorization: `Bearer ${this.options.deepSeekApiKey}`,
44+
},
45+
body: JSON.stringify({
46+
model: modelId,
47+
messages: formattedMessages,
48+
temperature: 0,
49+
stream: true,
50+
max_tokens: modelInfo.maxTokens,
51+
}),
1452
})
53+
54+
if (!response.ok) {
55+
throw new Error(`DeepSeek API error: ${response.statusText}`)
56+
}
57+
58+
if (!response.body) {
59+
throw new Error("No response body received from DeepSeek API")
60+
}
61+
62+
const reader = response.body.getReader()
63+
const decoder = new TextDecoder()
64+
let buffer = ""
65+
66+
try {
67+
while (true) {
68+
const { done, value } = await reader.read()
69+
if (done) break
70+
71+
buffer += decoder.decode(value, { stream: true })
72+
const lines = buffer.split("\n")
73+
buffer = lines.pop() || ""
74+
75+
for (const line of lines) {
76+
if (line.trim() === "") continue
77+
if (!line.startsWith("data: ")) continue
78+
79+
const data = line.slice(6)
80+
if (data === "[DONE]") continue
81+
82+
try {
83+
const chunk = JSON.parse(data)
84+
const delta = chunk.choices[0]?.delta ?? {}
85+
86+
if (delta.content) {
87+
yield {
88+
type: "text",
89+
text: delta.content,
90+
}
91+
}
92+
93+
if ("reasoning_content" in delta && delta.reasoning_content) {
94+
yield {
95+
type: "reasoning",
96+
text: delta.reasoning_content,
97+
}
98+
}
99+
100+
if (chunk.usage) {
101+
const usage = chunk.usage as DeepSeekUsage
102+
let inputTokens = (usage.prompt_tokens || 0) - (usage.prompt_cache_hit_tokens || 0)
103+
yield {
104+
type: "usage",
105+
inputTokens: inputTokens,
106+
outputTokens: usage.completion_tokens || 0,
107+
cacheReadTokens: usage.prompt_cache_hit_tokens || 0,
108+
cacheWriteTokens: usage.prompt_cache_miss_tokens || 0,
109+
}
110+
}
111+
} catch (error) {
112+
console.error("Error parsing DeepSeek response:", error)
113+
}
114+
}
115+
}
116+
} finally {
117+
reader.releaseLock()
118+
}
15119
}
16120

17-
override getModel(): { id: string; info: ModelInfo } {
121+
getModel(): { id: string; info: ModelInfo } {
18122
const modelId = this.options.apiModelId ?? deepSeekDefaultModelId
19123
return {
20124
id: modelId,
21125
info: deepSeekModels[modelId as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId],
22126
}
23127
}
128+
129+
async completePrompt(prompt: string): Promise<string> {
130+
try {
131+
const response = await fetch(`${this.baseUrl}/chat/completions`, {
132+
method: "POST",
133+
headers: {
134+
"Content-Type": "application/json",
135+
Authorization: `Bearer ${this.options.deepSeekApiKey}`,
136+
},
137+
body: JSON.stringify({
138+
model: this.getModel().id,
139+
messages: [{ role: "user", content: prompt }],
140+
temperature: 0,
141+
stream: false,
142+
}),
143+
})
144+
145+
if (!response.ok) {
146+
throw new Error(`DeepSeek API error: ${response.statusText}`)
147+
}
148+
149+
const data = await response.json()
150+
return data.choices[0]?.message?.content || ""
151+
} catch (error) {
152+
if (error instanceof Error) {
153+
throw new Error(`DeepSeek completion error: ${error.message}`)
154+
}
155+
throw error
156+
}
157+
}
24158
}

src/api/providers/pearai.ts

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,100 @@
1-
import { OpenAiHandler } from "./openai"
21
import * as vscode from "vscode"
3-
import { AnthropicModelId, ApiHandlerOptions, ModelInfo, PEARAI_URL } from "../../shared/api"
2+
import { ApiHandlerOptions, PEARAI_URL, ModelInfo } from "../../shared/api"
43
import { AnthropicHandler } from "./anthropic"
4+
import { DeepSeekHandler } from "./deepseek"
5+
6+
interface PearAiModelsResponse {
7+
models: {
8+
[key: string]: {
9+
underlyingModel?: string
10+
[key: string]: any
11+
}
12+
}
13+
defaultModelId: string
14+
}
15+
16+
export class PearAiHandler {
17+
private handler!: AnthropicHandler | DeepSeekHandler
518

6-
export class PearAiHandler extends AnthropicHandler {
719
constructor(options: ApiHandlerOptions) {
820
if (!options.pearaiApiKey) {
921
vscode.window.showErrorMessage("PearAI API key not found.", "Login to PearAI").then(async (selection) => {
1022
if (selection === "Login to PearAI") {
1123
const extensionUrl = `${vscode.env.uriScheme}://pearai.pearai/auth`
1224
const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(extensionUrl))
13-
1425
vscode.env.openExternal(
1526
await vscode.env.asExternalUri(
16-
vscode.Uri.parse(
17-
`https://trypear.ai/signin?callback=${callbackUri.toString()}`, // Change to localhost if running locally
18-
),
27+
vscode.Uri.parse(`https://trypear.ai/signin?callback=${callbackUri.toString()}`),
1928
),
2029
)
2130
}
2231
})
2332
throw new Error("PearAI API key not found. Please login to PearAI.")
2433
}
25-
super({
26-
...options,
27-
apiKey: options.pearaiApiKey,
28-
anthropicBaseUrl: PEARAI_URL,
34+
35+
this.initializeHandler(options).catch((error) => {
36+
console.error("Failed to initialize PearAI handler:", error)
37+
throw error
2938
})
3039
}
3140

32-
override getModel(): { id: AnthropicModelId; info: ModelInfo } {
33-
const baseModel = super.getModel()
41+
private async initializeHandler(options: ApiHandlerOptions): Promise<void> {
42+
const modelId = options.apiModelId || ""
43+
44+
if (modelId === "pearai-model") {
45+
try {
46+
const response = await fetch(`${PEARAI_URL}/getPearAIAgentModels`)
47+
if (!response.ok) {
48+
throw new Error(`Failed to fetch models: ${response.statusText}`)
49+
}
50+
const data = (await response.json()) as PearAiModelsResponse
51+
const underlyingModel = data.models[modelId]?.underlyingModel || "claude-3-5-sonnet-20241022"
52+
53+
if (underlyingModel.startsWith("deepseek")) {
54+
this.handler = new DeepSeekHandler({
55+
...options,
56+
deepSeekApiKey: options.pearaiApiKey,
57+
deepSeekBaseUrl: PEARAI_URL,
58+
apiModelId: underlyingModel,
59+
})
60+
} else {
61+
// Default to Claude
62+
this.handler = new AnthropicHandler({
63+
...options,
64+
apiKey: options.pearaiApiKey,
65+
anthropicBaseUrl: PEARAI_URL,
66+
apiModelId: underlyingModel,
67+
})
68+
}
69+
} catch (error) {
70+
console.error("Error fetching PearAI models:", error)
71+
// Default to Claude if there's an error
72+
this.handler = new AnthropicHandler({
73+
...options,
74+
apiKey: options.pearaiApiKey,
75+
anthropicBaseUrl: PEARAI_URL,
76+
apiModelId: "claude-3-5-sonnet-20241022",
77+
})
78+
}
79+
} else if (modelId.startsWith("claude")) {
80+
this.handler = new AnthropicHandler({
81+
...options,
82+
apiKey: options.pearaiApiKey,
83+
anthropicBaseUrl: PEARAI_URL,
84+
})
85+
} else if (modelId.startsWith("deepseek")) {
86+
this.handler = new DeepSeekHandler({
87+
...options,
88+
deepSeekApiKey: options.pearaiApiKey,
89+
deepSeekBaseUrl: PEARAI_URL,
90+
})
91+
} else {
92+
throw new Error(`Unsupported model: ${modelId}`)
93+
}
94+
}
95+
96+
getModel(): { id: string; info: ModelInfo } {
97+
const baseModel = this.handler.getModel()
3498
return {
3599
id: baseModel.id,
36100
info: {
@@ -42,4 +106,13 @@ export class PearAiHandler extends AnthropicHandler {
42106
},
43107
}
44108
}
109+
110+
async *createMessage(systemPrompt: string, messages: any[]): AsyncGenerator<any> {
111+
const generator = this.handler.createMessage(systemPrompt, messages)
112+
yield* generator
113+
}
114+
115+
async completePrompt(prompt: string): Promise<string> {
116+
return this.handler.completePrompt(prompt)
117+
}
45118
}

src/core/webview/ClineProvider.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
423423
`style-src ${webview.cspSource} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,
424424
`img-src ${webview.cspSource} data:`,
425425
`script-src 'unsafe-eval' https://* http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
426-
`connect-src https://* ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
426+
`connect-src https://* ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort} http://localhost:8000`,
427427
]
428428

429429
return /*html*/ `
@@ -1691,9 +1691,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
16911691
requestyModelInfo,
16921692
modelTemperature,
16931693
modelMaxTokens,
1694-
pearaiBaseUrl,
1695-
pearaiModelId,
1696-
pearaiModelInfo,
16971694
} = apiConfiguration
16981695
await Promise.all([
16991696
this.updateGlobalState("apiProvider", apiProvider),
@@ -1743,9 +1740,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
17431740
this.updateGlobalState("requestyModelInfo", requestyModelInfo),
17441741
this.updateGlobalState("modelTemperature", modelTemperature),
17451742
this.updateGlobalState("modelMaxTokens", modelMaxTokens),
1746-
await this.updateGlobalState("pearaiBaseUrl", PEARAI_URL),
1747-
await this.updateGlobalState("pearaiModelId", pearaiModelId),
1748-
await this.updateGlobalState("pearaiModelInfo", pearaiModelInfo),
17491743
])
17501744
if (this.cline) {
17511745
this.cline.api = buildApiHandler(apiConfiguration)
@@ -2190,8 +2184,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
21902184
pearaiApiKey,
21912185
pearaiRefreshKey,
21922186
pearaiBaseUrl,
2193-
pearaiModelId,
2194-
pearaiModelInfo,
21952187
mistralCodestralUrl,
21962188
azureApiVersion,
21972189
openAiStreamingEnabled,
@@ -2279,8 +2271,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
22792271
this.getSecret("pearai-token") as Promise<string | undefined>,
22802272
this.getSecret("pearai-refresh") as Promise<string | undefined>,
22812273
this.getGlobalState("pearaiBaseUrl") as Promise<string | undefined>,
2282-
this.getGlobalState("pearaiModelId") as Promise<string | undefined>,
2283-
this.getGlobalState("pearaiModelInfo") as Promise<ModelInfo | undefined>,
22842274
this.getGlobalState("mistralCodestralUrl") as Promise<string | undefined>,
22852275
this.getGlobalState("azureApiVersion") as Promise<string | undefined>,
22862276
this.getGlobalState("openAiStreamingEnabled") as Promise<boolean | undefined>,
@@ -2384,8 +2374,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
23842374
mistralApiKey,
23852375
pearaiApiKey,
23862376
pearaiBaseUrl,
2387-
pearaiModelId,
2388-
pearaiModelInfo,
23892377
mistralCodestralUrl,
23902378
azureApiVersion,
23912379
openAiStreamingEnabled,

0 commit comments

Comments
 (0)