// NERF Extension — Dynamic Model Router
// Maps complexity tiers to models, enforcing downgrade-only semantics.
// The user's configured model is always the ceiling.
import { tierOrdinal } from "./complexity-classifier.js";
// ─── Known Model Tiers ───────────────────────────────────────────────────────
// Maps known model IDs to their capability tier. Used when tier_models is not
// explicitly configured to pick the best available model for each tier.
const MODEL_CAPABILITY_TIER = {
    // Light-tier models (cheapest)
    "claude-haiku-4-5": "light",
    "claude-3-5-haiku-latest": "light",
    "claude-3-haiku-20240307": "light",
    "gpt-4o-mini": "light",
    "gemini-2.0-flash": "light",
    "gemini-flash-2.0": "light",
    // Standard-tier models
    "claude-sonnet-4-6": "standard",
    "claude-sonnet-4-5-20250514": "standard",
    "claude-3-5-sonnet-latest": "standard",
    "gpt-4o": "standard",
    "gemini-2.5-pro": "standard",
    "deepseek-chat": "standard",
    // Heavy-tier models (most capable)
    "claude-opus-4-6": "heavy",
    "claude-3-opus-latest": "heavy",
    "gpt-4-turbo": "heavy",
    "o1": "heavy",
    "o3": "heavy",
};
// ─── Cost Table (per 1K input tokens, approximate USD) ───────────────────────
// Used for cross-provider cost comparison when multiple providers offer
// the same capability tier.
const MODEL_COST_PER_1K_INPUT = {
    "claude-haiku-4-5": 0.0008,
    "claude-3-5-haiku-latest": 0.0008,
    "claude-sonnet-4-6": 0.003,
    "claude-sonnet-4-5-20250514": 0.003,
    "claude-opus-4-6": 0.015,
    "gpt-4o-mini": 0.00015,
    "gpt-4o": 0.0025,
    "gemini-2.0-flash": 0.0001,
    "gemini-2.5-pro": 0.00125,
    "deepseek-chat": 0.00014,
};
// ─── Public API ──────────────────────────────────────────────────────────────
/**
 * Resolve the model to use for a given complexity tier.
 *
 * Downgrade-only: the returned model is always equal to or cheaper than
 * the user's configured primary model. Never upgrades beyond configuration.
 *
 * @param classification  The complexity classification result
 * @param phaseConfig     The user's configured model for this phase (ceiling)
 * @param routingConfig   Dynamic routing configuration
 * @param availableModelIds  List of available model IDs (from registry)
 */
export function resolveModelForComplexity(classification, phaseConfig, routingConfig, availableModelIds) {
    // If no phase config or routing disabled, pass through
    if (!phaseConfig || !routingConfig.enabled) {
        return {
            modelId: phaseConfig?.primary ?? "",
            fallbacks: phaseConfig?.fallbacks ?? [],
            tier: classification.tier,
            wasDowngraded: false,
            reason: "dynamic routing disabled or no phase config",
        };
    }
    const configuredPrimary = phaseConfig.primary;
    const configuredTier = getModelTier(configuredPrimary);
    const requestedTier = classification.tier;
    // If the configured model is unknown (not in MODEL_CAPABILITY_TIER),
    // honor the user's explicit choice — don't downgrade based on a guess.
    // Unknown models default to "heavy" in getModelTier, which makes every
    // standard/light unit get downgraded to tier_models, silently ignoring
    // the user's configuration. (#2192)
    if (!isKnownModel(configuredPrimary)) {
        return {
            modelId: configuredPrimary,
            fallbacks: phaseConfig.fallbacks,
            tier: requestedTier,
            wasDowngraded: false,
            reason: `configured model "${configuredPrimary}" is not in the known tier map — honoring explicit config`,
        };
    }
    // Downgrade-only: if requested tier >= configured tier, no change
    if (tierOrdinal(requestedTier) >= tierOrdinal(configuredTier)) {
        return {
            modelId: configuredPrimary,
            fallbacks: phaseConfig.fallbacks,
            tier: requestedTier,
            wasDowngraded: false,
            reason: `tier ${requestedTier} >= configured ${configuredTier}`,
        };
    }
    // Find the best model for the requested tier
    const targetModelId = findModelForTier(requestedTier, routingConfig, availableModelIds, routingConfig.cross_provider !== false);
    if (!targetModelId) {
        // No suitable model found — use configured primary
        return {
            modelId: configuredPrimary,
            fallbacks: phaseConfig.fallbacks,
            tier: requestedTier,
            wasDowngraded: false,
            reason: `no ${requestedTier}-tier model available`,
        };
    }
    // Build fallback chain: [downgraded_model, ...configured_fallbacks, configured_primary]
    const fallbacks = [
        ...phaseConfig.fallbacks.filter(f => f !== targetModelId),
        configuredPrimary,
    ].filter(f => f !== targetModelId);
    return {
        modelId: targetModelId,
        fallbacks,
        tier: requestedTier,
        wasDowngraded: true,
        reason: classification.reason,
    };
}
/**
 * Escalate to the next tier after a failure.
 * Returns the new tier, or null if already at heavy (max).
 */
export function escalateTier(currentTier) {
    switch (currentTier) {
        case "light": return "standard";
        case "standard": return "heavy";
        case "heavy": return null;
    }
}
/**
 * Get the default routing config (all features enabled).
 */
export function defaultRoutingConfig() {
    return {
        enabled: false,
        escalate_on_failure: true,
        budget_pressure: true,
        cross_provider: true,
        hooks: true,
    };
}
// ─── Internal ────────────────────────────────────────────────────────────────
function getModelTier(modelId) {
    // Strip provider prefix if present
    const bareId = modelId.includes("/") ? modelId.split("/").pop() : modelId;
    // Check exact match first
    if (MODEL_CAPABILITY_TIER[bareId])
        return MODEL_CAPABILITY_TIER[bareId];
    // Check if any known model ID is a prefix/suffix match
    for (const [knownId, tier] of Object.entries(MODEL_CAPABILITY_TIER)) {
        if (bareId.includes(knownId) || knownId.includes(bareId))
            return tier;
    }
    // Unknown models are assumed heavy (safest assumption)
    return "heavy";
}
/** Check if a model ID has a known capability tier mapping. (#2192) */
function isKnownModel(modelId) {
    const bareId = modelId.includes("/") ? modelId.split("/").pop() : modelId;
    if (MODEL_CAPABILITY_TIER[bareId])
        return true;
    for (const knownId of Object.keys(MODEL_CAPABILITY_TIER)) {
        if (bareId.includes(knownId) || knownId.includes(bareId))
            return true;
    }
    return false;
}
function findModelForTier(tier, config, availableModelIds, crossProvider) {
    // 1. Check explicit tier_models config
    const explicitModel = config.tier_models?.[tier];
    if (explicitModel && availableModelIds.includes(explicitModel)) {
        return explicitModel;
    }
    // Also check with provider prefix stripped
    if (explicitModel) {
        const match = availableModelIds.find(id => {
            const bareAvail = id.includes("/") ? id.split("/").pop() : id;
            const bareExplicit = explicitModel.includes("/") ? explicitModel.split("/").pop() : explicitModel;
            return bareAvail === bareExplicit;
        });
        if (match)
            return match;
    }
    // 2. Auto-detect: find the cheapest available model in the requested tier
    const candidates = availableModelIds
        .filter(id => {
        const modelTier = getModelTier(id);
        return modelTier === tier;
    })
        .sort((a, b) => {
        if (!crossProvider)
            return 0;
        const costA = getModelCost(a);
        const costB = getModelCost(b);
        return costA - costB;
    });
    return candidates[0] ?? null;
}
function getModelCost(modelId) {
    const bareId = modelId.includes("/") ? modelId.split("/").pop() : modelId;
    if (MODEL_COST_PER_1K_INPUT[bareId] !== undefined) {
        return MODEL_COST_PER_1K_INPUT[bareId];
    }
    // Check partial matches
    for (const [knownId, cost] of Object.entries(MODEL_COST_PER_1K_INPUT)) {
        if (bareId.includes(knownId) || knownId.includes(bareId))
            return cost;
    }
    // Unknown cost — assume expensive to avoid routing to unknown cheap models
    return 999;
}
