mirror of
https://github.com/cline/cline.git
synced 2026-09-11 05:47:07 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ca086d7a1 |
@@ -45,8 +45,10 @@ describe("OllamaHandler", () => {
|
||||
this.skip()
|
||||
}
|
||||
this.timeout(5000)
|
||||
// Ensure client is initialized
|
||||
const client = (handler as any).ensureClient()
|
||||
// Mock the Ollama client's chat method
|
||||
const chatStub = sinon.stub(handler["client"], "chat").resolves({
|
||||
const chatStub = sinon.stub(client, "chat").resolves({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
yield {
|
||||
message: { content: "Hello, world!" },
|
||||
@@ -139,8 +141,9 @@ describe("OllamaHandler", () => {
|
||||
// Restore real timers for this test
|
||||
clock.restore()
|
||||
|
||||
// Mock the Ollama client's chat method to fail on first call and succeed on second
|
||||
const chatStub = sinon.stub(handler["client"], "chat")
|
||||
// Ensure client is initialized and mock the Ollama client's chat method to fail on first call and succeed on second
|
||||
const client = (handler as any).ensureClient()
|
||||
const chatStub = sinon.stub(client, "chat")
|
||||
|
||||
// First call throws an error
|
||||
chatStub.onFirstCall().rejects(new Error("API Error"))
|
||||
|
||||
@@ -7,18 +7,33 @@ import { ApiStream } from "../transform/stream"
|
||||
|
||||
export class AnthropicHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: Anthropic
|
||||
private client: Anthropic | undefined
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new Anthropic({
|
||||
apiKey: this.options.apiKey,
|
||||
baseURL: this.options.anthropicBaseUrl || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
private ensureClient(): Anthropic {
|
||||
if (!this.client) {
|
||||
if (!this.options.apiKey) {
|
||||
throw new Error("Anthropic API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new Anthropic({
|
||||
apiKey: this.options.apiKey,
|
||||
baseURL: this.options.anthropicBaseUrl || undefined,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Anthropic client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
|
||||
const model = this.getModel()
|
||||
let stream: AnthropicStream<Anthropic.RawMessageStreamEvent>
|
||||
const modelId = model.id
|
||||
@@ -44,7 +59,7 @@ export class AnthropicHandler implements ApiHandler {
|
||||
)
|
||||
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
|
||||
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
|
||||
stream = await this.client.messages.create(
|
||||
stream = await client.messages.create(
|
||||
{
|
||||
model: modelId,
|
||||
thinking: reasoningOn ? { type: "enabled", budget_tokens: budget_tokens } : undefined,
|
||||
@@ -118,7 +133,7 @@ export class AnthropicHandler implements ApiHandler {
|
||||
break
|
||||
}
|
||||
default: {
|
||||
stream = await this.client.messages.create({
|
||||
stream = await client.messages.create({
|
||||
model: modelId,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
temperature: 0,
|
||||
|
||||
@@ -7,26 +7,37 @@ import { ApiStream } from "@api/transform/stream"
|
||||
|
||||
export class CerebrasHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: Cerebras
|
||||
private client: Cerebras | undefined
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
// Clean and validate the API key
|
||||
const cleanApiKey = this.options.cerebrasApiKey?.trim()
|
||||
private ensureClient(): Cerebras {
|
||||
if (!this.client) {
|
||||
// Clean and validate the API key
|
||||
const cleanApiKey = this.options.cerebrasApiKey?.trim()
|
||||
|
||||
if (!cleanApiKey) {
|
||||
throw new Error("Cerebras API key is required")
|
||||
if (!cleanApiKey) {
|
||||
throw new Error("Cerebras API key is required")
|
||||
}
|
||||
|
||||
try {
|
||||
this.client = new Cerebras({
|
||||
apiKey: cleanApiKey,
|
||||
timeout: 30000, // 30 second timeout
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Cerebras client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
this.client = new Cerebras({
|
||||
apiKey: cleanApiKey,
|
||||
timeout: 30000, // 30 second timeout
|
||||
})
|
||||
return this.client
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
|
||||
// Convert Anthropic messages to Cerebras format
|
||||
const cerebrasMessages: Array<{
|
||||
role: "system" | "user" | "assistant"
|
||||
@@ -65,7 +76,7 @@ export class CerebrasHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
try {
|
||||
const stream = await this.client.chat.completions.create({
|
||||
const stream = await client.chat.completions.create({
|
||||
model: this.getModel().id,
|
||||
messages: cerebrasMessages,
|
||||
temperature: 0,
|
||||
|
||||
+25
-11
@@ -10,28 +10,42 @@ import { withRetry } from "../retry"
|
||||
|
||||
export class ClineHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
private client: OpenAI | undefined
|
||||
lastGenerationId?: string
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.cline.bot/v1",
|
||||
apiKey: this.options.clineApiKey || "",
|
||||
defaultHeaders: {
|
||||
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on cline.bot rankings.
|
||||
"X-Title": "Cline", // Optional. Shows in rankings on cline.bot.
|
||||
"X-Task-ID": this.options.taskId || "", // Include the task ID in the request headers
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.clineApiKey) {
|
||||
throw new Error("You don't seem to be logged in to a Cline account.")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.cline.bot/v1",
|
||||
apiKey: this.options.clineApiKey || "",
|
||||
defaultHeaders: {
|
||||
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on cline.bot rankings.
|
||||
"X-Title": "Cline", // Optional. Shows in rankings on cline.bot.
|
||||
"X-Task-ID": this.options.taskId || "", // Include the task ID in the request headers
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Cline client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
this.lastGenerationId = undefined
|
||||
|
||||
const stream = await createOpenRouterStream(
|
||||
this.client,
|
||||
client,
|
||||
systemPrompt,
|
||||
messages,
|
||||
this.getModel(),
|
||||
|
||||
@@ -10,14 +10,27 @@ import { convertToR1Format } from "../transform/r1-format"
|
||||
|
||||
export class DeepSeekHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
private client: OpenAI | undefined
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.deepseek.com/v1",
|
||||
apiKey: this.options.deepSeekApiKey,
|
||||
})
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.deepSeekApiKey) {
|
||||
throw new Error("DeepSeek API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.deepseek.com/v1",
|
||||
apiKey: this.options.deepSeekApiKey,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating DeepSeek client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
|
||||
@@ -54,6 +67,7 @@ export class DeepSeekHandler implements ApiHandler {
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
|
||||
const isDeepseekReasoner = model.id.includes("deepseek-reasoner")
|
||||
@@ -67,7 +81,7 @@ export class DeepSeekHandler implements ApiHandler {
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
}
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
max_completion_tokens: model.info.maxTokens,
|
||||
messages: openAiMessages,
|
||||
|
||||
@@ -8,13 +8,26 @@ import { withRetry } from "../retry"
|
||||
|
||||
export class DoubaoHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
private client: OpenAI | undefined
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://ark.cn-beijing.volces.com/api/v3/",
|
||||
apiKey: this.options.doubaoApiKey,
|
||||
})
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.doubaoApiKey) {
|
||||
throw new Error("Doubao API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://ark.cn-beijing.volces.com/api/v3/",
|
||||
apiKey: this.options.doubaoApiKey,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Doubao client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
getModel(): { id: DoubaoModelId; info: ModelInfo } {
|
||||
@@ -31,12 +44,13 @@ export class DoubaoHandler implements ApiHandler {
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
const stream = await this.client.chat.completions.create({
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
max_completion_tokens: model.info.maxTokens,
|
||||
messages: openAiMessages,
|
||||
|
||||
@@ -15,18 +15,32 @@ import { ApiStream } from "../transform/stream"
|
||||
|
||||
export class FireworksHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
private client: OpenAI | undefined
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.fireworks.ai/inference/v1",
|
||||
apiKey: this.options.fireworksApiKey,
|
||||
})
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.fireworksApiKey) {
|
||||
throw new Error("Fireworks API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.fireworks.ai/inference/v1",
|
||||
apiKey: this.options.fireworksApiKey,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Fireworks client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const modelId = this.options.fireworksModelId ?? ""
|
||||
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
@@ -34,7 +48,7 @@ export class FireworksHandler implements ApiHandler {
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
const stream = await client.chat.completions.create({
|
||||
model: modelId,
|
||||
...(this.options.fireworksModelMaxCompletionTokens
|
||||
? { max_completion_tokens: this.options.fireworksModelMaxCompletionTokens }
|
||||
|
||||
+35
-18
@@ -38,30 +38,45 @@ interface GeminiHandlerOptions extends ApiHandlerOptions {
|
||||
*/
|
||||
export class GeminiHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: GoogleGenAI
|
||||
private client: GoogleGenAI | undefined
|
||||
|
||||
constructor(options: GeminiHandlerOptions) {
|
||||
// Store the options
|
||||
this.options = options
|
||||
}
|
||||
|
||||
if (options.isVertex) {
|
||||
// Initialize with Vertex AI configuration
|
||||
const project = this.options.vertexProjectId ?? "not-provided"
|
||||
const location = this.options.vertexRegion ?? "not-provided"
|
||||
private ensureClient(): GoogleGenAI {
|
||||
if (!this.client) {
|
||||
const options = this.options as GeminiHandlerOptions
|
||||
|
||||
this.client = new GoogleGenAI({
|
||||
vertexai: true,
|
||||
project,
|
||||
location,
|
||||
})
|
||||
} else {
|
||||
// Initialize with standard API key
|
||||
if (!options.geminiApiKey) {
|
||||
throw new Error("API key is required for Google Gemini when not using Vertex AI")
|
||||
if (options.isVertex) {
|
||||
// Initialize with Vertex AI configuration
|
||||
const project = this.options.vertexProjectId ?? "not-provided"
|
||||
const location = this.options.vertexRegion ?? "not-provided"
|
||||
|
||||
try {
|
||||
this.client = new GoogleGenAI({
|
||||
vertexai: true,
|
||||
project,
|
||||
location,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Gemini Vertex AI client: ${error.message}`)
|
||||
}
|
||||
} else {
|
||||
// Initialize with standard API key
|
||||
if (!options.geminiApiKey) {
|
||||
throw new Error("API key is required for Google Gemini when not using Vertex AI")
|
||||
}
|
||||
|
||||
try {
|
||||
this.client = new GoogleGenAI({ apiKey: options.geminiApiKey })
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Gemini client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
this.client = new GoogleGenAI({ apiKey: options.geminiApiKey })
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,6 +95,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
maxDelay: 15000,
|
||||
})
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const { id: modelId, info } = this.getModel()
|
||||
const contents = messages.map(convertAnthropicMessageToGemini)
|
||||
|
||||
@@ -117,7 +133,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
|
||||
|
||||
try {
|
||||
const result = await this.client.models.generateContentStream({
|
||||
const result = await client.models.generateContentStream({
|
||||
model: modelId,
|
||||
contents: contents,
|
||||
config: {
|
||||
@@ -351,6 +367,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
*/
|
||||
async countTokens(content: Array<any>): Promise<number> {
|
||||
try {
|
||||
const client = this.ensureClient()
|
||||
const { id: model } = this.getModel()
|
||||
|
||||
// Convert content to Gemini format
|
||||
@@ -362,7 +379,7 @@ export class GeminiHandler implements ApiHandler {
|
||||
})
|
||||
|
||||
// Use Gemini's token counting API
|
||||
const response = await this.client.models.countTokens({
|
||||
const response = await client.models.countTokens({
|
||||
model,
|
||||
contents: [{ parts: geminiContent }],
|
||||
})
|
||||
|
||||
@@ -8,21 +8,35 @@ import { withRetry } from "../retry"
|
||||
|
||||
export class LiteLlmHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
private client: OpenAI | undefined
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000",
|
||||
apiKey: this.options.liteLlmApiKey || "noop",
|
||||
})
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.liteLlmApiKey) {
|
||||
throw new Error("LiteLLM API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000",
|
||||
apiKey: this.options.liteLlmApiKey || "noop",
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating LiteLLM client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
async calculateCost(prompt_tokens: number, completion_tokens: number): Promise<number | undefined> {
|
||||
// Reference: https://github.com/BerriAI/litellm/blob/122ee634f434014267af104814022af1d9a0882f/litellm/proxy/spend_tracking/spend_management_endpoints.py#L1473
|
||||
const client = this.ensureClient()
|
||||
const modelId = this.options.liteLlmModelId || liteLlmDefaultModelId
|
||||
try {
|
||||
const response = await fetch(`${this.client.baseURL}/spend/calculate`, {
|
||||
const response = await fetch(`${client.baseURL}/spend/calculate`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -54,6 +68,7 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const formattedMessages = convertToOpenAiMessages(messages)
|
||||
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
|
||||
role: "system",
|
||||
@@ -101,7 +116,7 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
return message
|
||||
})
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
const stream = await client.chat.completions.create({
|
||||
model: this.options.liteLlmModelId || liteLlmDefaultModelId,
|
||||
messages: [enhancedSystemMessage, ...enhancedMessages],
|
||||
temperature,
|
||||
|
||||
@@ -8,25 +8,36 @@ import { withRetry } from "../retry"
|
||||
|
||||
export class LmStudioHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
private client: OpenAI | undefined
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
baseURL: (this.options.lmStudioBaseUrl || "http://localhost:1234") + "/v1",
|
||||
apiKey: "noop",
|
||||
})
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: (this.options.lmStudioBaseUrl || "http://localhost:1234") + "/v1",
|
||||
apiKey: "noop",
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating LM Studio client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
@withRetry({ retryAllErrors: true })
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
try {
|
||||
const stream = await this.client.chat.completions.create({
|
||||
const stream = await client.chat.completions.create({
|
||||
model: this.getModel().id,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
|
||||
@@ -8,18 +8,32 @@ import { ApiStream } from "../transform/stream"
|
||||
|
||||
export class MistralHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: Mistral
|
||||
private client: Mistral | undefined
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new Mistral({
|
||||
apiKey: this.options.mistralApiKey,
|
||||
})
|
||||
}
|
||||
|
||||
private ensureClient(): Mistral {
|
||||
if (!this.client) {
|
||||
if (!this.options.mistralApiKey) {
|
||||
throw new Error("Mistral API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new Mistral({
|
||||
apiKey: this.options.mistralApiKey,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Mistral client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const stream = await this.client.chat
|
||||
const client = this.ensureClient()
|
||||
const stream = await client.chat
|
||||
.stream({
|
||||
model: this.getModel().id,
|
||||
// max_completion_tokens: this.getModel().info.maxTokens,
|
||||
|
||||
@@ -8,24 +8,37 @@ import { convertToR1Format } from "../transform/r1-format"
|
||||
import { nebiusDefaultModelId, nebiusModels, type ModelInfo, type ApiHandlerOptions, type NebiusModelId } from "../../shared/api"
|
||||
|
||||
export class NebiusHandler implements ApiHandler {
|
||||
private client: OpenAI
|
||||
private client: OpenAI | undefined
|
||||
|
||||
constructor(private readonly options: ApiHandlerOptions) {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.studio.nebius.ai/v1",
|
||||
apiKey: this.options.nebiusApiKey,
|
||||
})
|
||||
constructor(private readonly options: ApiHandlerOptions) {}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.nebiusApiKey) {
|
||||
throw new Error("Nebius API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.studio.nebius.ai/v1",
|
||||
apiKey: this.options.nebiusApiKey,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Nebius client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = model.id.includes("DeepSeek-R1")
|
||||
? convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)]
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: openAiMessages,
|
||||
temperature: 0,
|
||||
|
||||
@@ -8,15 +8,26 @@ import { withRetry } from "../retry"
|
||||
|
||||
export class OllamaHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: Ollama
|
||||
private client: Ollama | undefined
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new Ollama({ host: this.options.ollamaBaseUrl || "http://localhost:11434" })
|
||||
}
|
||||
|
||||
private ensureClient(): Ollama {
|
||||
if (!this.client) {
|
||||
try {
|
||||
this.client = new Ollama({ host: this.options.ollamaBaseUrl || "http://localhost:11434" })
|
||||
} catch (error) {
|
||||
throw new Error(`Error creating Ollama client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
@withRetry({ retryAllErrors: true })
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const ollamaMessages: Message[] = [{ role: "system", content: systemPrompt }, ...convertToOllamaMessages(messages)]
|
||||
|
||||
try {
|
||||
@@ -27,7 +38,7 @@ export class OllamaHandler implements ApiHandler {
|
||||
})
|
||||
|
||||
// Create the actual API request promise
|
||||
const apiPromise = this.client.chat({
|
||||
const apiPromise = client.chat({
|
||||
model: this.getModel().id,
|
||||
messages: ollamaMessages,
|
||||
stream: true,
|
||||
|
||||
@@ -10,13 +10,26 @@ import type { ChatCompletionReasoningEffort } from "openai/resources/chat/comple
|
||||
|
||||
export class OpenAiNativeHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
private client: OpenAI | undefined
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
apiKey: this.options.openAiNativeApiKey,
|
||||
})
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.openAiNativeApiKey) {
|
||||
throw new Error("OpenAI API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
apiKey: this.options.openAiNativeApiKey,
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating OpenAI client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
|
||||
@@ -38,6 +51,7 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
|
||||
switch (model.id) {
|
||||
@@ -45,7 +59,7 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
case "o1-preview":
|
||||
case "o1-mini": {
|
||||
// o1 doesn't support streaming, non-1 temp, or system prompt
|
||||
const response = await this.client.chat.completions.create({
|
||||
const response = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: [{ role: "user", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
|
||||
})
|
||||
@@ -61,7 +75,7 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
case "o4-mini":
|
||||
case "o3":
|
||||
case "o3-mini": {
|
||||
const stream = await this.client.chat.completions.create({
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
messages: [{ role: "developer", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
|
||||
stream: true,
|
||||
@@ -85,7 +99,7 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
break
|
||||
}
|
||||
default: {
|
||||
const stream = await this.client.chat.completions.create({
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
// max_completion_tokens: this.getModel().info.maxTokens,
|
||||
temperature: 0,
|
||||
|
||||
+36
-22
@@ -10,35 +10,49 @@ import type { ChatCompletionReasoningEffort } from "openai/resources/chat/comple
|
||||
|
||||
export class OpenAiHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
private client: OpenAI | undefined
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
// Azure API shape slightly differs from the core API shape: https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai
|
||||
// Use azureApiVersion to determine if this is an Azure endpoint, since the URL may not always contain 'azure.com'
|
||||
if (
|
||||
this.options.azureApiVersion ||
|
||||
((this.options.openAiBaseUrl?.toLowerCase().includes("azure.com") ||
|
||||
this.options.openAiBaseUrl?.toLowerCase().includes("azure.us")) &&
|
||||
!this.options.openAiModelId?.toLowerCase().includes("deepseek"))
|
||||
) {
|
||||
this.client = new AzureOpenAI({
|
||||
baseURL: this.options.openAiBaseUrl,
|
||||
apiKey: this.options.openAiApiKey,
|
||||
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
|
||||
defaultHeaders: this.options.openAiHeaders,
|
||||
})
|
||||
} else {
|
||||
this.client = new OpenAI({
|
||||
baseURL: this.options.openAiBaseUrl,
|
||||
apiKey: this.options.openAiApiKey,
|
||||
defaultHeaders: this.options.openAiHeaders,
|
||||
})
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.openAiApiKey) {
|
||||
throw new Error("OpenAI API key is required")
|
||||
}
|
||||
try {
|
||||
// Azure API shape slightly differs from the core API shape: https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai
|
||||
// Use azureApiVersion to determine if this is an Azure endpoint, since the URL may not always contain 'azure.com'
|
||||
if (
|
||||
this.options.azureApiVersion ||
|
||||
((this.options.openAiBaseUrl?.toLowerCase().includes("azure.com") ||
|
||||
this.options.openAiBaseUrl?.toLowerCase().includes("azure.us")) &&
|
||||
!this.options.openAiModelId?.toLowerCase().includes("deepseek"))
|
||||
) {
|
||||
this.client = new AzureOpenAI({
|
||||
baseURL: this.options.openAiBaseUrl,
|
||||
apiKey: this.options.openAiApiKey,
|
||||
apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
|
||||
defaultHeaders: this.options.openAiHeaders,
|
||||
})
|
||||
} else {
|
||||
this.client = new OpenAI({
|
||||
baseURL: this.options.openAiBaseUrl,
|
||||
apiKey: this.options.openAiApiKey,
|
||||
defaultHeaders: this.options.openAiHeaders,
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating OpenAI client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const modelId = this.options.openAiModelId ?? ""
|
||||
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
|
||||
const isR1FormatRequired = this.options.openAiModelInfo?.isR1FormatRequired ?? false
|
||||
@@ -68,7 +82,7 @@ export class OpenAiHandler implements ApiHandler {
|
||||
reasoningEffort = (this.options.reasoningEffort as ChatCompletionReasoningEffort) || "medium"
|
||||
}
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
const stream = await client.chat.completions.create({
|
||||
model: modelId,
|
||||
messages: openAiMessages,
|
||||
temperature,
|
||||
|
||||
@@ -11,27 +11,41 @@ import { OpenRouterErrorResponse } from "./types"
|
||||
|
||||
export class OpenRouterHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
private client: OpenAI | undefined
|
||||
lastGenerationId?: string
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://openrouter.ai/api/v1",
|
||||
apiKey: this.options.openRouterApiKey,
|
||||
defaultHeaders: {
|
||||
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on openrouter.ai rankings.
|
||||
"X-Title": "Cline", // Optional. Shows in rankings on openrouter.ai.
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.openRouterApiKey) {
|
||||
throw new Error("OpenRouter API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://openrouter.ai/api/v1",
|
||||
apiKey: this.options.openRouterApiKey,
|
||||
defaultHeaders: {
|
||||
"HTTP-Referer": "https://cline.bot", // Optional, for including your app on openrouter.ai rankings.
|
||||
"X-Title": "Cline", // Optional. Shows in rankings on openrouter.ai.
|
||||
},
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating OpenRouter client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
this.lastGenerationId = undefined
|
||||
|
||||
const stream = await createOpenRouterStream(
|
||||
this.client,
|
||||
client,
|
||||
systemPrompt,
|
||||
messages,
|
||||
this.getModel(),
|
||||
|
||||
@@ -18,17 +18,30 @@ import { withRetry } from "../retry"
|
||||
|
||||
export class QwenHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
private client: OpenAI | undefined
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
baseURL:
|
||||
this.options.qwenApiLine === "china"
|
||||
? "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
apiKey: this.options.qwenApiKey,
|
||||
})
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.qwenApiKey) {
|
||||
throw new Error("Alibaba API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL:
|
||||
this.options.qwenApiLine === "china"
|
||||
? "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
apiKey: this.options.qwenApiKey,
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating Alibaba client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
getModel(): { id: MainlandQwenModelId | InternationalQwenModelId; info: ModelInfo } {
|
||||
@@ -51,6 +64,7 @@ export class QwenHandler implements ApiHandler {
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
const isDeepseekReasoner = model.id.includes("deepseek-r1")
|
||||
const isReasoningModelFamily = model.id.includes("qwen3") || ["qwen-plus-latest", "qwen-turbo-latest"].includes(model.id)
|
||||
@@ -76,7 +90,7 @@ export class QwenHandler implements ApiHandler {
|
||||
temperature = undefined
|
||||
}
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
max_completion_tokens: model.info.maxTokens,
|
||||
messages: openAiMessages,
|
||||
|
||||
@@ -19,22 +19,36 @@ interface RequestyUsage extends OpenAI.CompletionUsage {
|
||||
|
||||
export class RequestyHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
private client: OpenAI | undefined
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://router.requesty.ai/v1",
|
||||
apiKey: this.options.requestyApiKey,
|
||||
defaultHeaders: {
|
||||
"HTTP-Referer": "https://cline.bot",
|
||||
"X-Title": "Cline",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.requestyApiKey) {
|
||||
throw new Error("Requesty API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://router.requesty.ai/v1",
|
||||
apiKey: this.options.requestyApiKey,
|
||||
defaultHeaders: {
|
||||
"HTTP-Referer": "https://cline.bot",
|
||||
"X-Title": "Cline",
|
||||
},
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating Requesty client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
@@ -57,7 +71,7 @@ export class RequestyHandler implements ApiHandler {
|
||||
: {}
|
||||
|
||||
// @ts-ignore-next-line
|
||||
const stream = await this.client.chat.completions.create({
|
||||
const stream = await client.chat.completions.create({
|
||||
model: model.id,
|
||||
max_tokens: model.info.maxTokens || undefined,
|
||||
messages: openAiMessages,
|
||||
|
||||
@@ -9,18 +9,32 @@ import { convertToR1Format } from "@api/transform/r1-format"
|
||||
|
||||
export class SambanovaHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
private client: OpenAI | undefined
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.sambanova.ai/v1",
|
||||
apiKey: this.options.sambanovaApiKey,
|
||||
})
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.sambanovaApiKey) {
|
||||
throw new Error("SambaNova API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.sambanova.ai/v1",
|
||||
apiKey: this.options.sambanovaApiKey,
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating SambaNova client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const model = this.getModel()
|
||||
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
@@ -34,7 +48,7 @@ export class SambanovaHandler implements ApiHandler {
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
}
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
const stream = await client.chat.completions.create({
|
||||
model: this.getModel().id,
|
||||
messages: openAiMessages,
|
||||
temperature: 0,
|
||||
|
||||
@@ -9,18 +9,32 @@ import { convertToR1Format } from "@api/transform/r1-format"
|
||||
|
||||
export class TogetherHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
private client: OpenAI | undefined
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.together.xyz/v1",
|
||||
apiKey: this.options.togetherApiKey,
|
||||
})
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.togetherApiKey) {
|
||||
throw new Error("Together API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.together.xyz/v1",
|
||||
apiKey: this.options.togetherApiKey,
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating Together client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const modelId = this.options.togetherModelId ?? ""
|
||||
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
|
||||
|
||||
@@ -33,7 +47,7 @@ export class TogetherHandler implements ApiHandler {
|
||||
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
|
||||
}
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
const stream = await client.chat.completions.create({
|
||||
model: modelId,
|
||||
messages: openAiMessages,
|
||||
temperature: 0,
|
||||
|
||||
+43
-16
@@ -7,25 +7,49 @@ import { ApiStream } from "@api/transform/stream"
|
||||
import { GeminiHandler } from "./gemini"
|
||||
|
||||
export class VertexHandler implements ApiHandler {
|
||||
private geminiHandler: GeminiHandler
|
||||
private clientAnthropic: AnthropicVertex
|
||||
private geminiHandler: GeminiHandler | undefined
|
||||
private clientAnthropic: AnthropicVertex | undefined
|
||||
private options: ApiHandlerOptions
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
// Create a GeminiHandler with isVertex flag for Gemini models
|
||||
this.geminiHandler = new GeminiHandler({
|
||||
...options,
|
||||
isVertex: true,
|
||||
})
|
||||
private ensureGeminiHandler(): GeminiHandler {
|
||||
if (!this.geminiHandler) {
|
||||
try {
|
||||
// Create a GeminiHandler with isVertex flag for Gemini models
|
||||
this.geminiHandler = new GeminiHandler({
|
||||
...this.options,
|
||||
isVertex: true,
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating Vertex AI Gemini handler: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.geminiHandler
|
||||
}
|
||||
|
||||
// Initialize Anthropic client for Claude models
|
||||
this.clientAnthropic = new AnthropicVertex({
|
||||
projectId: this.options.vertexProjectId,
|
||||
// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions
|
||||
region: this.options.vertexRegion,
|
||||
})
|
||||
private ensureAnthropicClient(): AnthropicVertex {
|
||||
if (!this.clientAnthropic) {
|
||||
if (!this.options.vertexProjectId) {
|
||||
throw new Error("Vertex AI project ID is required")
|
||||
}
|
||||
if (!this.options.vertexRegion) {
|
||||
throw new Error("Vertex AI region is required")
|
||||
}
|
||||
try {
|
||||
// Initialize Anthropic client for Claude models
|
||||
this.clientAnthropic = new AnthropicVertex({
|
||||
projectId: this.options.vertexProjectId,
|
||||
// https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#regions
|
||||
region: this.options.vertexRegion,
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating Vertex AI Anthropic client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.clientAnthropic
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
@@ -35,10 +59,13 @@ export class VertexHandler implements ApiHandler {
|
||||
|
||||
// For Gemini models, use the GeminiHandler
|
||||
if (!modelId.includes("claude")) {
|
||||
yield* this.geminiHandler.createMessage(systemPrompt, messages)
|
||||
const geminiHandler = this.ensureGeminiHandler()
|
||||
yield* geminiHandler.createMessage(systemPrompt, messages)
|
||||
return
|
||||
}
|
||||
|
||||
const clientAnthropic = this.ensureAnthropicClient()
|
||||
|
||||
// Claude implementation
|
||||
let budget_tokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn =
|
||||
@@ -63,7 +90,7 @@ export class VertexHandler implements ApiHandler {
|
||||
)
|
||||
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
|
||||
const secondLastMsgUserIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
|
||||
stream = await this.clientAnthropic.beta.messages.create(
|
||||
stream = await clientAnthropic.beta.messages.create(
|
||||
{
|
||||
model: modelId,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
@@ -125,7 +152,7 @@ export class VertexHandler implements ApiHandler {
|
||||
break
|
||||
}
|
||||
default: {
|
||||
stream = await this.clientAnthropic.beta.messages.create({
|
||||
stream = await clientAnthropic.beta.messages.create({
|
||||
model: modelId,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
temperature: 0,
|
||||
|
||||
@@ -9,18 +9,32 @@ import { withRetry } from "../retry"
|
||||
|
||||
export class XAIHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
private client: OpenAI | undefined
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.x.ai/v1",
|
||||
apiKey: this.options.xaiApiKey,
|
||||
})
|
||||
}
|
||||
|
||||
private ensureClient(): OpenAI {
|
||||
if (!this.client) {
|
||||
if (!this.options.xaiApiKey) {
|
||||
throw new Error("xAI API key is required")
|
||||
}
|
||||
try {
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://api.x.ai/v1",
|
||||
apiKey: this.options.xaiApiKey,
|
||||
})
|
||||
} catch (error: any) {
|
||||
throw new Error(`Error creating xAI client: ${error.message}`)
|
||||
}
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const client = this.ensureClient()
|
||||
const modelId = this.getModel().id
|
||||
// ensure reasoning effort is either "low" or "high" for grok-3-mini
|
||||
let reasoningEffort: ChatCompletionReasoningEffort | undefined
|
||||
@@ -30,7 +44,7 @@ export class XAIHandler implements ApiHandler {
|
||||
reasoningEffort = undefined
|
||||
}
|
||||
}
|
||||
const stream = await this.client.chat.completions.create({
|
||||
const stream = await client.chat.completions.create({
|
||||
model: modelId,
|
||||
max_completion_tokens: this.getModel().info.maxTokens,
|
||||
temperature: 0,
|
||||
|
||||
Reference in New Issue
Block a user