/**
 * src/models/anthropic.ts
 * Anthropic Claude provider — Opus 4.x, Sonnet 4.x, Haiku 4.x.
 *
 * Anthropic's API is NOT OpenAI-compatible (different message shape, system
 * prompt as top-level field, tool-use schema differs). We translate at this
 * boundary so the rest of Muassis sees the standard ChatMessage/ChatResponse
 * surface.
 *
 * Reads ANTHROPIC_API_KEY from env. Default model: claude-sonnet-4-6 (best
 * coding-and-reasoning per dollar in late 2025/2026 lineup).
 */
import { config } from '../config.js';
import { ModelError, } from './provider.js';
const ANTHROPIC_BASE_URL = 'https://api.anthropic.com/v1/messages';
const API_VERSION = '2023-06-01'; // Anthropic's stable version header
// Pricing (USD per 1M tokens) and capabilities. Update as Anthropic ships.
export const ANTHROPIC_MODELS = {
    // Opus tier (deepest reasoning, slowest, priciest)
    'claude-opus-4-7': { in: 15.00, out: 75.00, ctx: 200_000, supports_tools: true, supports_vision: true },
    'claude-opus-4-6': { in: 15.00, out: 75.00, ctx: 200_000, supports_tools: true, supports_vision: true },
    'claude-opus-4-5': { in: 15.00, out: 75.00, ctx: 200_000, supports_tools: true, supports_vision: true },
    // Sonnet tier (best coding model, balanced cost)
    'claude-sonnet-4-6': { in: 3.00, out: 15.00, ctx: 200_000, supports_tools: true, supports_vision: true },
    'claude-sonnet-4-5': { in: 3.00, out: 15.00, ctx: 200_000, supports_tools: true, supports_vision: true },
    // Haiku tier (cheap + fast)
    'claude-haiku-4-5': { in: 0.80, out: 4.00, ctx: 200_000, supports_tools: true, supports_vision: true },
    'claude-haiku-4-5-20251001': { in: 0.80, out: 4.00, ctx: 200_000, supports_tools: true, supports_vision: true },
};
const DEFAULT_MODEL = 'claude-sonnet-4-6';
function translateMessages(messages) {
    // Anthropic puts system prompt as a top-level field, not as role:'system'.
    // Multiple system messages get joined.
    const sysParts = [];
    const out = [];
    for (const m of messages) {
        if (m.role === 'system') {
            sysParts.push(m.content);
            continue;
        }
        if (m.role === 'tool') {
            // Convert OpenAI-style tool message to Anthropic tool_result block
            out.push({
                role: 'user',
                content: [{
                        type: 'tool_result',
                        tool_use_id: m.tool_call_id ?? '',
                        content: m.content,
                    }],
            });
            continue;
        }
        if (m.role === 'assistant' && m.tool_calls && m.tool_calls.length > 0) {
            out.push({
                role: 'assistant',
                content: [
                    ...(m.content ? [{ type: 'text', text: m.content }] : []),
                    ...m.tool_calls.map(tc => ({
                        type: 'tool_use',
                        id: tc.id,
                        name: tc.function.name,
                        input: (() => { try {
                            return JSON.parse(tc.function.arguments);
                        }
                        catch {
                            return {};
                        } })(),
                    })),
                ],
            });
            continue;
        }
        out.push({ role: m.role, content: m.content });
    }
    return { system: sysParts.join('\n\n'), messages: sanitizeToolPairing(out) };
}
// Anthropic is STRICT about tool blocks: every `tool_result` must reference a
// `tool_use` that actually exists, and every `tool_use` must be answered by a
// `tool_result`. A mismatch → hard 400 ("unexpected tool_use_id found in
// tool_result blocks ... must have a corresponding tool_use block"). History
// compaction, provider failover (a turn that started on NIM/Ollama then fell
// over to Claude), and the loop-guard dropping a repeated call can all leave an
// orphaned block. OpenAI-compatible providers tolerate this; Anthropic doesn't.
// Repair the array so Claude never 400s mid-conversation:
//   1. Drop tool_result blocks whose tool_use_id has no matching tool_use.
//   2. Drop tool_use blocks that are never answered by a tool_result.
//   3. Drop any message left with empty content (Anthropic rejects those too).
function sanitizeToolPairing(out) {
    const useIds = new Set();
    const resultIds = new Set();
    for (const m of out) {
        if (Array.isArray(m.content)) {
            for (const b of m.content) {
                if (b?.type === 'tool_use' && b.id)
                    useIds.add(b.id);
                else if (b?.type === 'tool_result' && b.tool_use_id)
                    resultIds.add(b.tool_use_id);
            }
        }
    }
    const cleaned = [];
    for (const m of out) {
        if (!Array.isArray(m.content)) {
            cleaned.push(m);
            continue;
        }
        let content = m.content;
        if (m.role === 'user') {
            content = content.filter(b => b?.type !== 'tool_result' || useIds.has(b.tool_use_id));
        }
        else if (m.role === 'assistant') {
            content = content.filter(b => b?.type !== 'tool_use' || resultIds.has(b.id));
        }
        // Drop a message whose content became empty after filtering — Anthropic
        // rejects `content: []`. (A bare-text message with content '' is fine.)
        if (Array.isArray(content) && content.length === 0)
            continue;
        cleaned.push(content === m.content ? m : { ...m, content });
    }
    return cleaned;
}
function translateTools(tools) {
    if (!tools || tools.length === 0)
        return undefined;
    return tools.map(t => ({
        name: t.function.name,
        description: t.function.description,
        input_schema: t.function.parameters,
    }));
}
export class AnthropicProvider {
    name = 'anthropic';
    model = DEFAULT_MODEL;
    async chat(messages, options = {}) {
        const apiKey = config.ANTHROPIC_API_KEY;
        if (!apiKey) {
            throw new ModelError('Anthropic API key not configured (set ANTHROPIC_API_KEY)', 503, this.model);
        }
        const { system, messages: msgs } = translateMessages(messages);
        const tools = translateTools(options.tools);
        const body = {
            model: this.model,
            max_tokens: options.maxTokens ?? 4096,
            messages: msgs,
        };
        // PROMPT CACHING — the single biggest cost lever. Our system prompt is
        // ~40K tokens (identity + rules + business context) and the tool schemas
        // are large; both are nearly identical across calls. Marking the LAST
        // system block and the LAST tool with cache_control:'ephemeral' makes
        // Anthropic cache everything up to that breakpoint. Subsequent calls
        // within ~5 min read the cache at ~10% of input cost (cache writes are
        // 1.25× once; reads are 0.1×). On an always-on agent this cuts Opus input
        // spend by ~90%. Cache needs ≥1024 tokens to activate — we're well over.
        if (system) {
            body.system = [{ type: 'text', text: system, cache_control: { type: 'ephemeral' } }];
        }
        // The Claude 4.7 generation DEPRECATED sampling params — sending
        // `temperature`/`top_p` returns a 400 ("temperature is deprecated for this
        // model"), which silently knocked the brain back to the fallback provider.
        // Omit them for 4.7+; keep them for older models that still accept them.
        const samplingDeprecated = /-4-7\b/.test(this.model);
        if (!samplingDeprecated) {
            if (options.temperature !== undefined)
                body.temperature = options.temperature;
            if (options.topP !== undefined)
                body.top_p = options.topP;
        }
        if (options.stop && options.stop.length > 0)
            body.stop_sequences = options.stop;
        if (tools && tools.length > 0) {
            // Cache the tool schemas too — they're static and large. cache_control
            // on the last tool sets the breakpoint after the whole tools array.
            const cachedTools = tools.map((t, i) => i === tools.length - 1 ? { ...t, cache_control: { type: 'ephemeral' } } : t);
            body.tools = cachedTools;
        }
        if (options.toolChoice === 'none')
            body.tool_choice = { type: 'none' };
        else if (options.toolChoice === 'auto' || tools)
            body.tool_choice = { type: 'auto' };
        const start = Date.now();
        const ctrl = new AbortController();
        const timer = setTimeout(() => ctrl.abort(), options.timeoutMs ?? 60_000);
        try {
            const r = await fetch(ANTHROPIC_BASE_URL, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-API-Key': apiKey,
                    'anthropic-version': API_VERSION,
                },
                body: JSON.stringify(body),
                signal: ctrl.signal,
            });
            const text = await r.text();
            if (!r.ok) {
                throw new ModelError(`Anthropic ${r.status}: ${text.slice(0, 300)}`, r.status, this.model);
            }
            const json = JSON.parse(text);
            // Pull text content + tool calls out of the content blocks
            let content = '';
            const toolCalls = [];
            for (const block of json.content) {
                if (block.type === 'text')
                    content += block.text;
                else if (block.type === 'tool_use') {
                    toolCalls.push({
                        id: block.id,
                        type: 'function',
                        function: {
                            name: block.name,
                            arguments: JSON.stringify(block.input),
                        },
                    });
                }
            }
            return {
                content,
                tokensInput: json.usage.input_tokens,
                tokensOutput: json.usage.output_tokens,
                model: json.model,
                latencyMs: Date.now() - start,
                toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
                finishReason: json.stop_reason,
            };
        }
        finally {
            clearTimeout(timer);
        }
    }
    setModel(model) {
        if (!(model in ANTHROPIC_MODELS)) {
            throw new Error(`Unknown Anthropic model: ${model}. Available: ${Object.keys(ANTHROPIC_MODELS).join(', ')}`);
        }
        this.model = model;
    }
}
export const anthropic = new AnthropicProvider();
//# sourceMappingURL=anthropic.js.map