import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" import { ApiHandlerOptions, liteLlmDefaultModelId, liteLlmModelInfoSaneDefaults } from "../../shared/api" import { ApiHandler } from ".." import { ApiStream } from "../transform/stream" import { convertToOpenAiMessages } from "../transform/openai-format" export class LiteLlmHandler implements ApiHandler { private options: ApiHandlerOptions private client: OpenAI constructor(options: ApiHandlerOptions) { this.options = options this.client = new OpenAI({ baseURL: this.options.liteLlmBaseUrl || "http://localhost:4000", apiKey: this.options.liteLlmApiKey || "noop", }) } async calculateCost(prompt_tokens: number, completion_tokens: number): Promise { // Reference: https://github.com/BerriAI/litellm/blob/122ee634f434014267af104814022af1d9a0882f/litellm/proxy/spend_tracking/spend_management_endpoints.py#L1473 const modelId = this.options.liteLlmModelId || liteLlmDefaultModelId try { const response = await fetch(`${this.client.baseURL}/spend/calculate`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.options.liteLlmApiKey}`, }, body: JSON.stringify({ completion_response: { model: modelId, usage: { prompt_tokens, completion_tokens, }, }, }), }) if (response.ok) { const data: { cost: number } = await response.json() return data.cost } else { console.error("Error calculating spend:", response.statusText) return undefined } } catch (error) { console.error("Error calculating spend:", error) return undefined } } async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { const formattedMessages = convertToOpenAiMessages(messages) const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = { role: "system", content: systemPrompt, } const modelId = this.options.liteLlmModelId || liteLlmDefaultModelId const isOminiModel = modelId.includes("o1-mini") || modelId.includes("o3-mini") // Configuration for extended thinking const budgetTokens = this.options.thinkingBudgetTokens || 0 const reasoningOn = budgetTokens !== 0 ? true : false const thinkingConfig = reasoningOn ? { type: "enabled", budget_tokens: budgetTokens } : undefined let temperature: number | undefined = 0 if (isOminiModel && reasoningOn) { temperature = undefined // Thinking mode doesn't support temperature } // Define cache control object if prompt caching is enabled const cacheControl = this.options.liteLlmUsePromptCache ? { cache_control: { type: "ephemeral" } } : undefined // Add cache_control to system message if enabled const enhancedSystemMessage = { ...systemMessage, ...(cacheControl && cacheControl), } // Find the last two user messages to apply caching const userMsgIndices = formattedMessages.reduce( (acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc), [] as number[], ) const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1 const secondLastUserMsgIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1 // Apply cache_control to the last two user messages if enabled const enhancedMessages = formattedMessages.map((message, index) => { if ((index === lastUserMsgIndex || index === secondLastUserMsgIndex) && cacheControl) { return { ...message, ...cacheControl, } } return message }) const stream = await this.client.chat.completions.create({ model: this.options.liteLlmModelId || liteLlmDefaultModelId, messages: [enhancedSystemMessage, ...enhancedMessages], temperature, stream: true, stream_options: { include_usage: true }, ...(thinkingConfig && { thinking: thinkingConfig }), // Add thinking configuration when applicable }) const inputCost = (await this.calculateCost(1e6, 0)) || 0 const outputCost = (await this.calculateCost(0, 1e6)) || 0 for await (const chunk of stream) { const delta = chunk.choices[0]?.delta // Handle normal text content if (delta?.content) { yield { type: "text", text: delta.content, } } // Handle reasoning events (thinking) // Thinking is not in the standard types but may be in the response interface ThinkingDelta { thinking?: string } if ((delta as ThinkingDelta)?.thinking) { yield { type: "reasoning", reasoning: (delta as ThinkingDelta).thinking || "", } } // Handle token usage information if (chunk.usage) { const totalCost = (inputCost * chunk.usage.prompt_tokens) / 1e6 + (outputCost * chunk.usage.completion_tokens) / 1e6 // Extract cache-related information if available // Need to use type assertion since these properties are not in the standard OpenAI types const usage = chunk.usage as { prompt_tokens: number completion_tokens: number cache_creation_input_tokens?: number prompt_cache_miss_tokens?: number cache_read_input_tokens?: number prompt_cache_hit_tokens?: number } const cacheWriteTokens = usage.cache_creation_input_tokens || usage.prompt_cache_miss_tokens || 0 const cacheReadTokens = usage.cache_read_input_tokens || usage.prompt_cache_hit_tokens || 0 yield { type: "usage", inputTokens: usage.prompt_tokens || 0, outputTokens: usage.completion_tokens || 0, cacheWriteTokens: cacheWriteTokens > 0 ? cacheWriteTokens : undefined, cacheReadTokens: cacheReadTokens > 0 ? cacheReadTokens : undefined, totalCost, } } } } getModel() { return { id: this.options.liteLlmModelId || liteLlmDefaultModelId, info: liteLlmModelInfoSaneDefaults, } } }