@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.
284 lines (239 loc) • 8.73 kB
text/typescript
import Anthropic, { ClientOptions } from '@anthropic-ai/sdk';
import type { ChatModelCard } from '@/types/llm';
import { LobeRuntimeAI } from '../BaseAI';
import { AgentRuntimeErrorType } from '../error';
import {
type ChatCompletionErrorPayload,
ChatMethodOptions,
ChatStreamPayload,
ModelProvider,
} from '../types';
import { buildAnthropicMessages, buildAnthropicTools } from '../utils/anthropicHelpers';
import { AgentRuntimeError } from '../utils/createError';
import { debugStream } from '../utils/debugStream';
import { desensitizeUrl } from '../utils/desensitizeUrl';
import { StreamingResponse } from '../utils/response';
import { AnthropicStream } from '../utils/streams';
import { handleAnthropicError } from './handleAnthropicError';
export interface AnthropicModelCard {
display_name: string;
id: string;
}
type anthropicTools = Anthropic.Tool | Anthropic.WebSearchTool20250305;
const modelsWithSmallContextWindow = new Set(['claude-3-opus-20240229', 'claude-3-haiku-20240307']);
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;
private isDebug() {
return process.env.DEBUG_ANTHROPIC_CHAT_COMPLETION === '1';
}
constructor({ apiKey, baseURL = DEFAULT_BASE_URL, id, ...res }: AnthropicAIParams = {}) {
if (!apiKey) throw AgentRuntimeError.createError(AgentRuntimeErrorType.InvalidProviderAPIKey);
const betaHeaders = process.env.ANTHROPIC_BETA_HEADERS;
this.client = new Anthropic({
apiKey,
baseURL,
...(betaHeaders ? { defaultHeaders: { 'anthropic-beta': betaHeaders } } : {}),
...res,
});
this.baseURL = this.client.baseURL;
this.apiKey = apiKey;
this.id = id || ModelProvider.Anthropic;
}
async chat(payload: ChatStreamPayload, options?: ChatMethodOptions) {
try {
const anthropicPayload = await this.buildAnthropicPayload(payload);
const inputStartAt = Date.now();
if (this.isDebug()) {
console.log('[requestPayload]');
console.log(JSON.stringify(anthropicPayload), '\n');
}
const response = await this.client.messages.create(
{
...anthropicPayload,
metadata: options?.user ? { user_id: options?.user } : undefined,
stream: true,
},
{
signal: options?.signal,
},
);
const [prod, debug] = response.tee();
if (this.isDebug()) {
debugStream(debug.toReadableStream()).catch(console.error);
}
return StreamingResponse(
AnthropicStream(prod, { callbacks: options?.callback, inputStartAt }),
{
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,
enabledSearch,
} = payload;
const { default: anthropicModels } = await import('@/config/aiModels/anthropic');
const modelConfig = anthropicModels.find((m) => m.id === model);
const defaultMaxOutput = modelConfig?.maxOutput;
// 配置优先级:用户设置 > 模型配置 > 硬编码默认值
const getMaxTokens = () => {
if (max_tokens) return max_tokens;
if (defaultMaxOutput) return defaultMaxOutput;
return undefined;
};
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 });
let postTools: anthropicTools[] | undefined = buildAnthropicTools(tools, {
enabledContextCaching,
});
if (enabledSearch) {
// Limit the number of searches per request
const maxUses = process.env.ANTHROPIC_MAX_USES;
const webSearchTool: Anthropic.WebSearchTool20250305 = {
name: 'web_search',
type: 'web_search_20250305',
...(maxUses &&
Number.isInteger(Number(maxUses)) &&
Number(maxUses) > 0 && {
max_uses: Number(maxUses),
}),
};
// 如果已有工具,则添加到现有工具列表中;否则创建新的工具列表
if (postTools && postTools.length > 0) {
postTools = [...postTools, webSearchTool];
} else {
postTools = [webSearchTool];
}
}
if (!!thinking && thinking.type === 'enabled') {
const maxTokens = getMaxTokens() || 32_000; // Claude Opus 4 has minimum maxOutput
// `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: {
...thinking,
budget_tokens: thinking?.budget_tokens
? Math.min(thinking.budget_tokens, maxTokens - 1) // `max_tokens` must be greater than `thinking.budget_tokens`.
: 1024,
},
tools: postTools,
} satisfies Anthropic.MessageCreateParams;
}
return {
// claude 3 series model hax max output token of 4096, 3.x series has 8192
// https://docs.anthropic.com/en/docs/about-claude/models/all-models#:~:text=200K-,Max%20output,-Normal%3A
max_tokens: getMaxTokens() || (modelsWithSmallContextWindow.has(model) ? 4096 : 8192),
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;