import { Logger } from '@nestjs/common'; import Anthropic from '@anthropic-ai/sdk'; import type { ModelRegistry } from '@mariozechner/pi-coding-agent'; import type { CompletionEvent, CompletionParams, IProviderAdapter, ModelInfo, ProviderHealth, } from '@mosaicstack/types'; /** * Anthropic provider adapter. * * Registers Claude models with the Pi ModelRegistry via the Anthropic SDK. * Configuration is driven by environment variables: * ANTHROPIC_API_KEY — Anthropic API key (required) */ export class AnthropicAdapter implements IProviderAdapter { readonly name = 'anthropic'; private readonly logger = new Logger(AnthropicAdapter.name); private client: Anthropic | null = null; private registeredModels: ModelInfo[] = []; constructor(private readonly registry: ModelRegistry) {} async register(): Promise { const apiKey = process.env['ANTHROPIC_API_KEY']; if (!apiKey) { this.logger.warn('Skipping Anthropic provider registration: ANTHROPIC_API_KEY not set'); return; } this.client = new Anthropic({ apiKey }); const models: ModelInfo[] = [ { id: 'claude-opus-4-6', provider: 'anthropic', name: 'Claude Opus 4.6', reasoning: true, contextWindow: 200000, maxTokens: 32000, inputTypes: ['text', 'image'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, }, { id: 'claude-sonnet-4-6', provider: 'anthropic', name: 'Claude Sonnet 4.6', reasoning: true, contextWindow: 200000, maxTokens: 16000, inputTypes: ['text', 'image'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, }, { id: 'claude-haiku-4-5', provider: 'anthropic', name: 'Claude Haiku 4.5', reasoning: false, contextWindow: 200000, maxTokens: 8192, inputTypes: ['text', 'image'], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, }, ]; this.registry.registerProvider('anthropic', { apiKey, baseUrl: 'https://api.anthropic.com', api: 'anthropic' as never, models: models.map((m) => ({ id: m.id, name: m.name, reasoning: m.reasoning, input: m.inputTypes as ('text' | 'image')[], cost: m.cost, contextWindow: m.contextWindow, maxTokens: m.maxTokens, })), }); this.registeredModels = models; this.logger.log( `Anthropic provider registered with models: ${models.map((m) => m.id).join(', ')}`, ); } listModels(): ModelInfo[] { return this.registeredModels; } async healthCheck(): Promise { const apiKey = process.env['ANTHROPIC_API_KEY']; if (!apiKey) { return { status: 'down', lastChecked: new Date().toISOString(), error: 'ANTHROPIC_API_KEY not configured', }; } const start = Date.now(); try { const client = this.client ?? new Anthropic({ apiKey }); await client.models.list({ limit: 1 }); const latencyMs = Date.now() - start; return { status: 'healthy', latencyMs, lastChecked: new Date().toISOString() }; } catch (err) { const latencyMs = Date.now() - start; const error = err instanceof Error ? err.message : String(err); const status = error.includes('401') || error.includes('403') ? 'degraded' : 'down'; return { status, latencyMs, lastChecked: new Date().toISOString(), error }; } } /** * Stream a completion from Anthropic using the messages API. * Maps Anthropic streaming events to the CompletionEvent format. * * Note: Currently reserved for future direct-completion use. The Pi SDK * integration routes completions through ModelRegistry / AgentSession. */ async *createCompletion(params: CompletionParams): AsyncIterable { const apiKey = process.env['ANTHROPIC_API_KEY']; if (!apiKey) { throw new Error('AnthropicAdapter: ANTHROPIC_API_KEY not configured'); } const client = this.client ?? new Anthropic({ apiKey }); // Separate system messages from user/assistant messages const systemMessages = params.messages.filter((m) => m.role === 'system'); const conversationMessages = params.messages.filter((m) => m.role !== 'system'); const systemPrompt = systemMessages.length > 0 ? systemMessages.map((m) => m.content).join('\n') : undefined; const stream = await client.messages.stream({ model: params.model, max_tokens: params.maxTokens ?? 1024, ...(systemPrompt !== undefined ? { system: systemPrompt } : {}), messages: conversationMessages.map((m) => ({ role: m.role as 'user' | 'assistant', content: m.content, })), ...(params.temperature !== undefined ? { temperature: params.temperature } : {}), ...(params.tools && params.tools.length > 0 ? { tools: params.tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.parameters as Anthropic.Tool['input_schema'], })), } : {}), }); for await (const event of stream) { if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') { yield { type: 'text_delta', content: event.delta.text }; } else if (event.type === 'content_block_delta' && event.delta.type === 'input_json_delta') { yield { type: 'tool_call', name: '', arguments: event.delta.partial_json }; } else if (event.type === 'message_delta' && event.usage) { yield { type: 'done', usage: { inputTokens: (event as { usage: { input_tokens?: number; output_tokens: number } }).usage .input_tokens ?? 0, outputTokens: event.usage.output_tokens, }, }; } } // Emit final done event with full usage from the completed message const finalMessage = await stream.finalMessage(); yield { type: 'done', usage: { inputTokens: finalMessage.usage.input_tokens, outputTokens: finalMessage.usage.output_tokens, }, }; } }