import Anthropic from '@anthropic-ai/sdk'; import { startSpan } from '@/remote/monitor'; import { strings } from '@/common/strings'; import { AIProvider } from './types'; const TIMEOUT_MS = 30000; const MODEL = 'claude-sonnet-4-5'; function withTimeout(promise: Promise, ms: number): Promise { return Promise.race([ promise, new Promise((_, reject) => setTimeout(() => reject(new Error(strings.addLog.detection.requestTimeout)), ms) ), ]); } export class ClaudeProvider implements AIProvider { private anthropic: Anthropic; constructor() { this.anthropic = new Anthropic({ apiKey: process.env.EXPO_PUBLIC_ANTHROPIC_API_KEY || '', dangerouslyAllowBrowser: true, }); } async query(prompt: string, base64?: string): Promise { return startSpan({ name: 'ai.claude', op: 'ai.inference' }, async span => { const start = Date.now(); const content: Anthropic.MessageCreateParams['messages'][0]['content'] = []; if (base64) { content.push({ type: 'image', source: { type: 'base64', media_type: 'image/jpeg', data: base64 }, }); } content.push({ type: 'text', text: prompt }); const response = await withTimeout( this.anthropic.messages.create({ model: MODEL, max_tokens: 1024, messages: [{ role: 'user', content }], }), TIMEOUT_MS ); span.setAttributes({ 'ai.model': MODEL, 'ai.input_tokens': response.usage.input_tokens, 'ai.output_tokens': response.usage.output_tokens, 'ai.latency_ms': Date.now() - start, 'ai.has_image': !!base64, }); const textBlock = response.content.find(b => b.type === 'text'); if (!textBlock || textBlock.type !== 'text') throw new Error(strings.addLog.detection.noResponse); return textBlock.text; }); } }