/**
 * Provider registry + model router.
 *
 * Holds routing rules for task_type → model candidates.
 * Supports auto-fallback on 429/529 (rate-limit / overload).
 * No external HTTP here — just the routing table + receipt accounting.
 * Actual LLM calls stay in callers; they report outcomes via recordOutcome().
 */
// ─── Built-in model catalog ───────────────────────────────────────────────────
const MODELS = {
    'claude-haiku-4-5-20251001': { provider: 'anthropic', modelId: 'claude-haiku-4-5-20251001', costPer1MIn: 0.80, costPer1MOut: 4.00, contextWindow: 200_000 },
    'claude-sonnet-4-6': { provider: 'anthropic', modelId: 'claude-sonnet-4-6', costPer1MIn: 3.00, costPer1MOut: 15.00, contextWindow: 200_000 },
    'claude-opus-4-7': { provider: 'anthropic', modelId: 'claude-opus-4-7', costPer1MIn: 15.00, costPer1MOut: 75.00, contextWindow: 200_000 },
    'gpt-4o-mini': { provider: 'openai', modelId: 'gpt-4o-mini', costPer1MIn: 0.15, costPer1MOut: 0.60, contextWindow: 128_000 },
    'gpt-4o': { provider: 'openai', modelId: 'gpt-4o', costPer1MIn: 5.00, costPer1MOut: 15.00, contextWindow: 128_000 },
    'mock': { provider: 'mock', modelId: 'mock', costPer1MIn: 0, costPer1MOut: 0, contextWindow: 999_999 },
};
// ─── Default routing rules ────────────────────────────────────────────────────
const DEFAULT_RULES = [
    { taskType: 'summarize', candidates: [MODELS['claude-haiku-4-5-20251001'], MODELS['gpt-4o-mini'], MODELS['mock']] },
    { taskType: 'consolidate', candidates: [MODELS['claude-haiku-4-5-20251001'], MODELS['gpt-4o-mini'], MODELS['mock']] },
    { taskType: 'classify', candidates: [MODELS['claude-haiku-4-5-20251001'], MODELS['gpt-4o-mini'], MODELS['mock']] },
    { taskType: 'embed', candidates: [MODELS['gpt-4o-mini'], MODELS['mock']] },
    { taskType: 'plan', candidates: [MODELS['claude-sonnet-4-6'], MODELS['claude-haiku-4-5-20251001'], MODELS['mock']] },
    { taskType: 'code', candidates: [MODELS['claude-sonnet-4-6'], MODELS['gpt-4o'], MODELS['mock']] },
    { taskType: 'review', candidates: [MODELS['claude-sonnet-4-6'], MODELS['gpt-4o'], MODELS['mock']] },
    { taskType: 'orchestrate', candidates: [MODELS['claude-opus-4-7'], MODELS['claude-sonnet-4-6'], MODELS['mock']] },
    { taskType: 'generic', candidates: [MODELS['claude-haiku-4-5-20251001'], MODELS['mock']] },
];
// ─── Cooldown tracker ─────────────────────────────────────────────────────────
const COOLDOWN_MS = 60_000;
class ProviderRegistry {
    rules = new Map();
    cooldowns = new Map(); // modelId → expires
    receipts = [];
    receiptCap = 1000;
    constructor() {
        for (const rule of DEFAULT_RULES) {
            this.rules.set(rule.taskType, rule);
        }
    }
    /** Override routing rule for a task type. */
    setRule(rule) {
        this.rules.set(rule.taskType, rule);
    }
    /**
     * Pick the best available model for a task.
     * Skips models in cooldown (recent 429/529).
     */
    pickModel(taskType) {
        const rule = this.rules.get(taskType) ?? this.rules.get('generic');
        const now = Date.now();
        for (const candidate of rule.candidates) {
            const coolUntil = this.cooldowns.get(candidate.modelId) ?? 0;
            if (now >= coolUntil)
                return candidate;
        }
        // All candidates in cooldown — return null; caller must degrade gracefully.
        return null;
    }
    /**
     * Signal that a model returned 429 or 529.
     * Places it in cooldown for COOLDOWN_MS.
     */
    reportRateLimit(modelId) {
        this.cooldowns.set(modelId, Date.now() + COOLDOWN_MS);
    }
    /** Clear cooldown (e.g., after successful call). */
    clearCooldown(modelId) {
        this.cooldowns.delete(modelId);
    }
    /** Record a call receipt for observability. */
    recordReceipt(receipt) {
        this.receipts.push(receipt);
        if (this.receipts.length > this.receiptCap) {
            this.receipts.splice(0, this.receipts.length - this.receiptCap);
        }
    }
    /** Get recent receipts, optionally filtered by task type. */
    getReceipts(taskType, limit = 50) {
        const filtered = taskType
            ? this.receipts.filter(r => r.taskType === taskType)
            : this.receipts;
        return filtered.slice(-limit);
    }
    /** Aggregate cost across all receipts. */
    getTotalCost() {
        return this.receipts.reduce((sum, r) => sum + r.estimatedUsd, 0);
    }
    /** Models currently in cooldown. */
    getCooldowns() {
        const now = Date.now();
        return [...this.cooldowns.entries()]
            .filter(([, exp]) => exp > now)
            .map(([modelId, exp]) => ({ modelId, expiresIn: exp - now }));
    }
    /** Compute estimated USD for a call. */
    static estimateCost(spec, inputTokens, outputTokens) {
        return (inputTokens * spec.costPer1MIn + outputTokens * spec.costPer1MOut) / 1_000_000;
    }
}
// ─── Singleton ────────────────────────────────────────────────────────────────
let _registry = null;
export function getProviderRegistry() {
    if (!_registry)
        _registry = new ProviderRegistry();
    return _registry;
}
