import Anthropic from '@anthropic-ai/sdk'; import type { Message } from '@anthropic-ai/sdk/resources/messages/messages.js'; import type { ChatRequest, ChatResponse, ChatStreamEvent, ModelClient } from './types.js'; export interface AnthropicClientConfig { apiKey?: string; // Falls back to ANTHROPIC_API_KEY env var authToken?: string; // Alternative: use auth token instead of API key model: string; maxTokens?: number; } export class AnthropicClient implements ModelClient { private client: Anthropic; private model: string; private defaultMaxTokens: number; constructor(config: AnthropicClientConfig) { this.client = new Anthropic({ apiKey: config.apiKey, authToken: config.authToken, }); this.model = config.model; this.defaultMaxTokens = config.maxTokens ?? 4096; } async chat(request: ChatRequest): Promise { const params: Record = { model: this.model, max_tokens: request.maxTokens ?? this.defaultMaxTokens, system: request.system, messages: request.messages.map((m) => ({ role: m.role, content: m.content, })), }; if (request.tools && request.tools.length > 0) { params.tools = request.tools; } const response = await this.client.messages.create(params as unknown as Parameters[0]) as Message; const textContent = response.content.find((c) => c.type === 'text'); const content = textContent?.type === 'text' ? textContent.text : ''; const toolCalls = response.content .filter((c): c is { type: 'tool_use'; id: string; name: string; input: unknown } => c.type === 'tool_use') .map(c => ({ id: c.id, name: c.name, args: c.input })); return { content, stopReason: response.stop_reason ?? 'end_turn', usage: { inputTokens: response.usage.input_tokens, outputTokens: response.usage.output_tokens, }, ...(toolCalls.length > 0 ? { toolCalls } : {}), }; } async *chatStream(request: ChatRequest): AsyncIterable { const stream = this.client.messages.stream({ model: this.model, max_tokens: request.maxTokens ?? this.defaultMaxTokens, system: request.system, messages: request.messages.map((m) => ({ role: m.role, content: m.content, })), }); try { for await (const event of stream) { if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') { yield { type: 'content', content: event.delta.text }; } } const finalMessage = await stream.finalMessage(); yield { type: 'done', usage: { inputTokens: finalMessage.usage.input_tokens, outputTokens: finalMessage.usage.output_tokens, }, }; } catch (error) { yield { type: 'error', error: error instanceof Error ? error : new Error(String(error)), }; } } }