UNPKG

@lobehub/chat

Version:

Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.

215 lines (182 loc) • 6.41 kB
// sort-imports-ignore import '@anthropic-ai/sdk/shims/web'; import Anthropic from '@anthropic-ai/sdk'; import { ClientOptions } from 'openai'; import type { ChatModelCard } from '@/types/llm'; import { LobeRuntimeAI } from '../BaseAI'; import { AgentRuntimeErrorType } from '../error'; import { ChatCompetitionOptions, type ChatCompletionErrorPayload, ChatStreamPayload, ModelProvider, } from '../types'; import { AgentRuntimeError } from '../utils/createError'; import { debugStream } from '../utils/debugStream'; import { desensitizeUrl } from '../utils/desensitizeUrl'; import { buildAnthropicMessages, buildAnthropicTools } from '../utils/anthropicHelpers'; import { StreamingResponse } from '../utils/response'; import { AnthropicStream } from '../utils/streams'; import { handleAnthropicError } from './handleAnthropicError'; export interface AnthropicModelCard { display_name: string; id: string; } const DEFAULT_BASE_URL = 'https://api.anthropic.com'; interface AnthropicAIParams extends ClientOptions { id?: string; } export class LobeAnthropicAI implements LobeRuntimeAI { private client: Anthropic; baseURL: string; apiKey?: string; private id: string; constructor({ apiKey, baseURL = DEFAULT_BASE_URL, id, ...res }: AnthropicAIParams = {}) { if (!apiKey) throw AgentRuntimeError.createError(AgentRuntimeErrorType.InvalidProviderAPIKey); this.client = new Anthropic({ apiKey, baseURL, ...res }); this.baseURL = this.client.baseURL; this.apiKey = apiKey; this.id = id || ModelProvider.Anthropic; } async chat(payload: ChatStreamPayload, options?: ChatCompetitionOptions) { try { const anthropicPayload = await this.buildAnthropicPayload(payload); const response = await this.client.messages.create( { ...anthropicPayload, stream: true }, { signal: options?.signal, }, ); const [prod, debug] = response.tee(); if (process.env.DEBUG_ANTHROPIC_CHAT_COMPLETION === '1') { debugStream(debug.toReadableStream()).catch(console.error); } return StreamingResponse(AnthropicStream(prod, options?.callback), { headers: options?.headers, }); } catch (error) { throw this.handleError(error); } } private async buildAnthropicPayload(payload: ChatStreamPayload) { const { messages, model, max_tokens, temperature, top_p, tools, thinking, enabledContextCaching = true, } = payload; const system_message = messages.find((m) => m.role === 'system'); const user_messages = messages.filter((m) => m.role !== 'system'); const systemPrompts = !!system_message?.content ? ([ { cache_control: enabledContextCaching ? { type: 'ephemeral' } : undefined, text: system_message?.content as string, type: 'text', }, ] as Anthropic.TextBlockParam[]) : undefined; const postMessages = await buildAnthropicMessages(user_messages, { enabledContextCaching }); const postTools = buildAnthropicTools(tools, { enabledContextCaching }); if (!!thinking) { const maxTokens = max_tokens ?? (thinking?.budget_tokens ? thinking?.budget_tokens + 4096 : 4096); // `temperature` may only be set to 1 when thinking is enabled. // `top_p` must be unset when thinking is enabled. return { max_tokens: maxTokens, messages: postMessages, model, system: systemPrompts, thinking, tools: postTools, } satisfies Anthropic.MessageCreateParams; } return { max_tokens: max_tokens ?? 4096, messages: postMessages, model, system: systemPrompts, temperature: payload.temperature !== undefined ? temperature / 2 : undefined, tools: postTools, top_p, } satisfies Anthropic.MessageCreateParams; } async models() { const { LOBE_DEFAULT_MODEL_LIST } = await import('@/config/aiModels'); const url = `${this.baseURL}/v1/models`; const response = await fetch(url, { headers: { 'anthropic-version': '2023-06-01', 'x-api-key': `${this.apiKey}`, }, method: 'GET', }); const json = await response.json(); const modelList: AnthropicModelCard[] = json['data']; return modelList .map((model) => { const knownModel = LOBE_DEFAULT_MODEL_LIST.find( (m) => model.id.toLowerCase() === m.id.toLowerCase(), ); return { contextWindowTokens: knownModel?.contextWindowTokens ?? undefined, displayName: model.display_name, enabled: knownModel?.enabled || false, functionCall: model.id.toLowerCase().includes('claude-3') || knownModel?.abilities?.functionCall || false, id: model.id, reasoning: knownModel?.abilities?.reasoning || false, vision: (model.id.toLowerCase().includes('claude-3') && !model.id.toLowerCase().includes('claude-3-5-haiku')) || knownModel?.abilities?.vision || false, }; }) .filter(Boolean) as ChatModelCard[]; } private handleError(error: any): ChatCompletionErrorPayload { let desensitizedEndpoint = this.baseURL; if (this.baseURL !== DEFAULT_BASE_URL) { desensitizedEndpoint = desensitizeUrl(this.baseURL); } if ('status' in (error as any)) { switch ((error as Response).status) { case 401: { throw AgentRuntimeError.chat({ endpoint: desensitizedEndpoint, error: error as any, errorType: AgentRuntimeErrorType.InvalidProviderAPIKey, provider: this.id, }); } case 403: { throw AgentRuntimeError.chat({ endpoint: desensitizedEndpoint, error: error as any, errorType: AgentRuntimeErrorType.LocationNotSupportError, provider: this.id, }); } default: { break; } } } const { errorResult } = handleAnthropicError(error); throw AgentRuntimeError.chat({ endpoint: desensitizedEndpoint, error: errorResult, errorType: AgentRuntimeErrorType.ProviderBizError, provider: this.id, }); } } export default LobeAnthropicAI;