// Anthropic (Claude) provider — analysis only. // // Claude does not natively transcribe audio, so transcribeAudio() throws. // Mix-and-match users can pair this provider for analysis with Gemini // (or future OpenAI Whisper) for transcription. // // Pricing reflects standard-context rates as of 2026-04-29 (cached in // the claude-api skill). Update when Anthropic changes published rates. import Anthropic from "@anthropic-ai/sdk"; import { retryAPI } from "../util.js"; import { formatCost, ratesFor } from "./cost.js"; // Per-1M-token rates in USD. Anthropic does not expose a separate // "thinking" rate — thinking tokens are billed as output, so we let // formatCost default thinking → output by omitting the thinking field. export const ANTHROPIC_PRICING = { "claude-opus-4-7": { input: 5.00, output: 25.00 }, "claude-opus-4-6": { input: 5.00, output: 25.00 }, "claude-sonnet-4-6": { input: 3.00, output: 15.00 }, "claude-haiku-4-5": { input: 1.00, output: 5.00 }, // Fallback for unknown / future models. "default": { input: 3.00, output: 15.00 }, }; // Analysis model list. Order = default fallback chain (most capable first). export const ANTHROPIC_ANALYSIS_MODELS = [ "claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5", ]; // Analysis output cap. Generous — the topic-analysis prompt produces a // JSON document scaled to transcript length, and truncation here loses // trailing sections. const ANALYSIS_MAX_TOKENS = 16000; export function createAnthropicProvider({ apiKey, timeoutMs = 900_000 } = {}) { if (!apiKey) { throw new Error("createAnthropicProvider: apiKey is required"); } const client = new Anthropic({ apiKey, timeout: timeoutMs }); return { name: "anthropic", capabilities: { transcribe: false, analyze: true, listModels: true, }, listAnalysisModels() { return [...ANTHROPIC_ANALYSIS_MODELS]; }, listTranscriptionModels() { return []; }, async transcribeAudio() { throw new Error( "Anthropic models do not natively transcribe audio. Use Gemini or OpenAI (Whisper) for the transcription step." ); }, async analyzeText({ prompt, model, onProgress = () => {}, retries = 2, signal, }) { const result = await retryAPI( () => client.messages.create( { model, max_tokens: ANALYSIS_MAX_TOKENS, messages: [{ role: "user", content: prompt }], }, // The Anthropic SDK accepts a per-call signal as the second // arg; abort() rejects the in-flight HTTP request immediately. signal ? { signal } : undefined ), { retries, delayMs: 5000, label: "Anthropic analysis", log: (msg) => onProgress(msg), } ); const text = (result.content || []) .filter((b) => b.type === "text") .map((b) => b.text) .join(""); const usage = { inputTokens: result.usage?.input_tokens || 0, outputTokens: result.usage?.output_tokens || 0, thinkingTokens: 0, }; const cost = formatCost(ratesFor(ANTHROPIC_PRICING, model), usage); return { text, usage, cost, finishReason: result.stop_reason || null, raw: result, }; }, }; }