@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.
336 lines (285 loc) • 10.5 kB
text/typescript
import Anthropic, { ClientOptions } from '@anthropic-ai/sdk';
import { ModelProvider } from 'model-bank';
import { LobeRuntimeAI } from '../../core/BaseAI';
import { buildAnthropicMessages, buildAnthropicTools } from '../../core/contextBuilders/anthropic';
import { MODEL_PARAMETER_CONFLICTS, resolveParameters } from '../../core/parameterResolver';
import { AnthropicStream } from '../../core/streams';
import {
type ChatCompletionErrorPayload,
ChatMethodOptions,
ChatStreamPayload,
GenerateObjectOptions,
GenerateObjectPayload,
} from '../../types';
import { AgentRuntimeErrorType } from '../../types/error';
import { AgentRuntimeError } from '../../utils/createError';
import { debugStream } from '../../utils/debugStream';
import { desensitizeUrl } from '../../utils/desensitizeUrl';
import { getModelPricing } from '../../utils/getModelPricing';
import { MODEL_LIST_CONFIGS, processModelList } from '../../utils/modelParse';
import { StreamingResponse } from '../../utils/response';
import { createAnthropicGenerateObject } from './generateObject';
import { handleAnthropicError } from './handleAnthropicError';
export interface AnthropicModelCard {
created_at: string;
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';
const DEFAULT_CACHE_TTL = '5m' as const;
type CacheTTL = Anthropic.Messages.CacheControlEphemeral['ttl'];
/**
* Resolves cache TTL from Anthropic payload or request settings
* Returns the first valid TTL found in system messages or content blocks
*/
const resolveCacheTTL = (
requestPayload: ChatStreamPayload,
anthropicPayload: Anthropic.MessageCreateParams,
): CacheTTL | undefined => {
// Check system messages for cache TTL
if (Array.isArray(anthropicPayload.system)) {
for (const block of anthropicPayload.system) {
const ttl = block.cache_control?.ttl;
if (ttl) return ttl;
}
}
// Check message content blocks for cache TTL
for (const message of anthropicPayload.messages ?? []) {
if (!Array.isArray(message.content)) continue;
for (const block of message.content) {
// Message content blocks might have cache_control property
const ttl = ('cache_control' in block && block.cache_control?.ttl) as CacheTTL | undefined;
if (ttl) return ttl;
}
}
// Use default TTL if context caching is enabled
if (requestPayload.enabledContextCaching) {
return DEFAULT_CACHE_TTL;
}
return undefined;
};
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,
defaultHeaders,
...res
}: AnthropicAIParams = {}) {
if (!apiKey) throw AgentRuntimeError.createError(AgentRuntimeErrorType.InvalidProviderAPIKey);
const betaHeaders = process.env.ANTHROPIC_BETA_HEADERS;
this.client = new Anthropic({
apiKey,
baseURL,
defaultHeaders: { ...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);
}
const pricing = await getModelPricing(payload.model, this.id);
const cacheTTL = resolveCacheTTL(payload, anthropicPayload);
const pricingOptions = cacheTTL ? { lookupParams: { ttl: cacheTTL } } : undefined;
return StreamingResponse(
AnthropicStream(prod, {
callbacks: options?.callback,
inputStartAt,
payload: { model: payload.model, pricing, pricingOptions, provider: this.id },
}),
{
headers: options?.headers,
},
);
} catch (error) {
throw this.handleError(error);
}
}
async generateObject(payload: GenerateObjectPayload, options?: GenerateObjectOptions) {
try {
return await createAnthropicGenerateObject(this.client, payload, options);
} 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 { anthropic: anthropicModels } = await import('model-bank');
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;
}
// Resolve temperature and top_p parameters based on model constraints
const hasConflict = MODEL_PARAMETER_CONFLICTS.ANTHROPIC_CLAUDE_4_PLUS.has(model);
const resolvedParams = resolveParameters(
{ temperature, top_p },
{ hasConflict, normalizeTemperature: true, preferTemperature: true },
);
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: resolvedParams.temperature,
tools: postTools,
top_p: resolvedParams.top_p,
} satisfies Anthropic.MessageCreateParams;
}
async models() {
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'];
const standardModelList = modelList.map((model) => ({
created: model.created_at,
displayName: model.display_name,
id: model.id,
}));
return processModelList(standardModelList, MODEL_LIST_CONFIGS.anthropic, 'anthropic');
}
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;