// @crash-shield-ignore-file — config/cache reader for Planu-controlled JSON; writer is this codebase, shape guaranteed by build/seed.
// engine/model-tier-resolver.ts — SPEC-644: Dynamic model tier mapping from provider APIs
// Fetches model lists from Anthropic/OpenAI/Google/Mistral and classifies into canonical tiers.
// Caches in conventions.json with 7-day TTL. Never hardcodes model IDs.
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { join } from 'node:path';
const TTL_MS = 7 * 24 * 60 * 60 * 1000;
const FETCH_TIMEOUT_MS = 5_000;
// ---------------------------------------------------------------------------
// Provider detection (AC4: env-var based)
// ---------------------------------------------------------------------------
const PROVIDER_ENV_VARS = {
    anthropic: 'ANTHROPIC_API_KEY',
    openai: 'OPENAI_API_KEY',
    google: 'GOOGLE_API_KEY',
    mistral: 'MISTRAL_API_KEY',
};
export function detectAvailableProviders() {
    return Object.entries(PROVIDER_ENV_VARS)
        .filter(([, envVar]) => !!process.env[envVar])
        .map(([provider]) => provider);
}
// ---------------------------------------------------------------------------
// Static fallback mapping (used when API unreachable or no provider configured)
// ---------------------------------------------------------------------------
export const STATIC_TIER_MAPPING = {
    anthropic: {
        haiku: 'claude-haiku-4-5',
        sonnet: 'claude-sonnet-4-5',
        opus: 'claude-opus-4-5',
    },
    openai: {
        haiku: 'gpt-4o-mini',
        sonnet: 'gpt-4o',
        opus: 'o1',
    },
    google: {
        haiku: 'gemini-2.0-flash',
        sonnet: 'gemini-1.5-pro',
        opus: 'gemini-1.5-ultra',
    },
    mistral: {
        haiku: 'mistral-small-latest',
        sonnet: 'mistral-medium-latest',
        opus: 'mistral-large-latest',
    },
};
// ---------------------------------------------------------------------------
// Tier classification heuristics (name-based, no API response parsing)
// ---------------------------------------------------------------------------
/** Classify a model name into a canonical tier based on name patterns. */
export function classifyModelTier(modelId) {
    const id = modelId.toLowerCase();
    // opus / frontier tier keywords
    if (/opus|ultra|large|o1(?!\d)|o3/.test(id)) {
        return 'opus';
    }
    // haiku / fast tier keywords (\b avoids matching 'gemini' on 'mini')
    if (/haiku|\bmini\b|small|flash|\bnano\b|lite|quick/.test(id)) {
        return 'haiku';
    }
    // default to balanced
    return 'sonnet';
}
/**
 * Build a ModelTierMapping from a list of model IDs.
 * Prefers the highest-capability model per tier (last one wins when sorted alphabetically desc).
 */
export function buildTierMapping(modelIds) {
    const byTier = { haiku: [], sonnet: [], opus: [] };
    for (const id of modelIds) {
        byTier[classifyModelTier(id)].push(id);
    }
    // Pick one per tier — prefer the most recent (sort desc, take first)
    const pick = (ids) => ids.sort().reverse()[0] ?? '';
    return {
        haiku: pick(byTier.haiku),
        sonnet: pick(byTier.sonnet),
        opus: pick(byTier.opus),
    };
}
// ---------------------------------------------------------------------------
// Per-provider API fetch with timeout
// ---------------------------------------------------------------------------
async function withTimeout(promise, ms) {
    return Promise.race([
        promise,
        new Promise((resolve) => {
            setTimeout(() => {
                resolve(null);
            }, ms);
        }),
    ]);
}
async function fetchAnthropicModels(apiKey) {
    const resp = await fetch('https://api.anthropic.com/v1/models', {
        headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },
    });
    if (!resp.ok) {
        return [];
    }
    const data = (await resp.json());
    return (data.data ?? []).map((m) => m.id ?? '').filter(Boolean);
}
async function fetchOpenAIModels(apiKey) {
    const resp = await fetch('https://api.openai.com/v1/models', {
        headers: { Authorization: `Bearer ${apiKey}` },
    });
    if (!resp.ok) {
        return [];
    }
    const data = (await resp.json());
    return (data.data ?? []).map((m) => m.id ?? '').filter(Boolean);
}
async function fetchGoogleModels(apiKey) {
    const resp = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`);
    if (!resp.ok) {
        return [];
    }
    const data = (await resp.json());
    return (data.models ?? []).map((m) => (m.name ?? '').replace('models/', '')).filter(Boolean);
}
async function fetchMistralModels(apiKey) {
    const resp = await fetch('https://api.mistral.ai/v1/models', {
        headers: { Authorization: `Bearer ${apiKey}` },
    });
    if (!resp.ok) {
        return [];
    }
    const data = (await resp.json());
    return (data.data ?? []).map((m) => m.id ?? '').filter(Boolean);
}
const PROVIDER_FETCHERS = {
    anthropic: fetchAnthropicModels,
    openai: fetchOpenAIModels,
    google: fetchGoogleModels,
    mistral: fetchMistralModels,
};
/**
 * Fetch model IDs for a single provider. Returns static fallback on any error.
 * SPEC-644 AC3: silent degradation when API unavailable.
 */
async function fetchProviderModels(provider, apiKey) {
    const fetcher = PROVIDER_FETCHERS[provider];
    if (!fetcher) {
        return (STATIC_TIER_MAPPING[provider] ??
            STATIC_TIER_MAPPING.anthropic ?? { haiku: '', sonnet: '', opus: '' });
    }
    try {
        const models = await withTimeout(fetcher(apiKey), FETCH_TIMEOUT_MS);
        if (!models || models.length === 0) {
            return (STATIC_TIER_MAPPING[provider] ??
                STATIC_TIER_MAPPING.anthropic ?? { haiku: '', sonnet: '', opus: '' });
        }
        const mapping = buildTierMapping(models);
        // Fill any empty tier from static fallback
        const staticFallback = STATIC_TIER_MAPPING[provider];
        if (staticFallback) {
            return {
                haiku: mapping.haiku || staticFallback.haiku,
                sonnet: mapping.sonnet || staticFallback.sonnet,
                opus: mapping.opus || staticFallback.opus,
            };
        }
        return mapping;
    }
    catch {
        return (STATIC_TIER_MAPPING[provider] ??
            STATIC_TIER_MAPPING.anthropic ?? { haiku: '', sonnet: '', opus: '' });
    }
}
// ---------------------------------------------------------------------------
// conventions.json read/write
// ---------------------------------------------------------------------------
async function readConventions(projectPath) {
    try {
        const raw = await readFile(join(projectPath, 'planu', 'conventions.json'), 'utf-8');
        return JSON.parse(raw);
    }
    catch {
        return null;
    }
}
async function patchConventions(projectPath, modelMapping) {
    const planuDir = join(projectPath, 'planu');
    await mkdir(planuDir, { recursive: true });
    const filePath = join(planuDir, 'conventions.json');
    let conventions;
    try {
        const raw = await readFile(filePath, 'utf-8');
        conventions = JSON.parse(raw);
    }
    catch {
        // Build a minimal structure if file doesn't exist
        conventions = {
            detectedAt: new Date().toISOString(),
            stack: {
                language: 'unknown',
                framework: null,
                testRunner: null,
                linter: null,
                formatter: null,
                bundler: null,
            },
            conventions: {
                fileNaming: 'unknown',
                functionNaming: 'unknown',
                testLocation: 'unknown',
                importStyle: 'unknown',
                errorHandling: 'unknown',
            },
            baseline: { testCount: 0, lintWarnings: null, coveragePercent: null },
        };
    }
    conventions.modelMapping = modelMapping;
    await writeFile(filePath, JSON.stringify(conventions, null, 2) + '\n', 'utf-8');
}
// ---------------------------------------------------------------------------
// TTL check
// ---------------------------------------------------------------------------
function isModelMappingStale(cache) {
    const age = Date.now() - new Date(cache.resolvedAt).getTime();
    return age > TTL_MS;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * SPEC-644 AC1+AC4: Fetch model tier mapping from detected provider APIs.
 * Runs at init_project. Returns the built ModelMappingCache.
 * Silent failure — always returns a valid cache (uses static fallback).
 */
export async function fetchAndCacheModelMapping(projectPath) {
    const providers = detectAvailableProviders();
    const resolvedAt = new Date().toISOString();
    if (providers.length === 0) {
        // No providers configured — use Anthropic static fallback
        const cache = {
            providers: {
                anthropic: STATIC_TIER_MAPPING.anthropic ?? { haiku: '', sonnet: '', opus: '' },
            },
            resolvedAt,
        };
        await patchConventions(projectPath, cache).catch(() => {
            /* best-effort */
        });
        return cache;
    }
    const providerMappings = {};
    await Promise.allSettled(providers.map(async (provider) => {
        const apiKey = process.env[PROVIDER_ENV_VARS[provider] ?? ''] ?? '';
        providerMappings[provider] = await fetchProviderModels(provider, apiKey);
    }));
    const cache = { providers: providerMappings, resolvedAt };
    await patchConventions(projectPath, cache).catch(() => {
        /* best-effort */
    });
    return cache;
}
/**
 * SPEC-644 AC2: Trigger background TTL refresh if modelMapping is stale.
 * Fire-and-forget — never blocks the caller.
 * Called from create_spec and init_project.
 */
export function triggerModelMappingRefresh(projectPath) {
    void (async () => {
        try {
            const conventions = await readConventions(projectPath);
            if (conventions?.modelMapping && !isModelMappingStale(conventions.modelMapping)) {
                return;
            }
            await fetchAndCacheModelMapping(projectPath);
        }
        catch {
            /* best-effort — silent failure */
        }
    })();
}
/**
 * SPEC-644 AC5: Resolve the canonical tier to a provider-specific model ID.
 * Uses cached mapping if available; falls back to static Anthropic mapping.
 */
export async function resolveModelId(projectPath, tier, preferredProvider = 'anthropic') {
    try {
        const conventions = await readConventions(projectPath);
        const providers = conventions?.modelMapping?.providers;
        if (providers) {
            const firstKey = Object.keys(providers)[0] ?? '';
            const mapping = providers[preferredProvider] ?? providers[firstKey];
            if (mapping?.[tier]) {
                return mapping[tier];
            }
        }
    }
    catch {
        /* fall through to static */
    }
    return (STATIC_TIER_MAPPING[preferredProvider]?.[tier] ?? STATIC_TIER_MAPPING.anthropic?.[tier] ?? tier);
}
//# sourceMappingURL=model-tier-resolver.js.map