/**
 * Model Registry — types and functions for model resolution, availability, and routing.
 * Extracted from config.ts for modularity.
 */
import { probeRuntimeSurface } from './runtime-probe.js';
import { isDefaultModelEligible, resolveEffectiveContextWindow, resolveModelCapabilities, } from './model-capabilities.js';
import { normalizeProviderKind } from './provider-kind.js';
export class LocalRuntimeProtocolUnresolvedError extends Error {
    constructor(message) {
        super(message);
        this.name = 'LocalRuntimeProtocolUnresolvedError';
    }
}
// ─── Normalization ───
export function normalizeModel(raw) {
    const id = typeof raw.id === 'string' && raw.id ? raw.id : '';
    const apiKeyEnv = typeof raw.apiKeyEnv === 'string' && raw.apiKeyEnv ? raw.apiKeyEnv : undefined;
    const envApiKey = apiKeyEnv ? process.env[apiKeyEnv] : undefined;
    const apiKeySource = typeof raw.apiKey === 'string'
        ? (raw.apiKey ? 'config' : 'unset')
        : envApiKey
            ? 'env'
            : 'unset';
    const endpoint = typeof raw.endpoint === 'string' ? raw.endpoint : undefined;
    const rawBackendModel = typeof raw.backendModel === 'string' && raw.backendModel ? raw.backendModel : id;
    const backendModel = normalizeBackendModelForEndpoint(rawBackendModel, endpoint);
    let customHeaders;
    if (raw.headers && typeof raw.headers === 'object' && !Array.isArray(raw.headers)) {
        customHeaders = {};
        for (const [key, value] of Object.entries(raw.headers)) {
            if (typeof value === 'string')
                customHeaders[key] = value;
        }
        if (Object.keys(customHeaders).length === 0)
            customHeaders = undefined;
    }
    return {
        id,
        label: typeof raw.label === 'string' && raw.label ? raw.label : id,
        backendModel,
        aliases: Array.isArray(raw.aliases) ? raw.aliases.filter((a) => typeof a === 'string') : [],
        tier: typeof raw.tier === 'string' && raw.tier ? raw.tier : 'general',
        provider: typeof raw.provider === 'string' && raw.provider.trim() ? raw.provider.trim() : undefined,
        default: raw.default === true ? true : undefined,
        channel: typeof raw.channel === 'string' ? raw.channel : undefined,
        role: typeof raw.role === 'string' ? raw.role : undefined,
        endpoint,
        apiKey: typeof raw.apiKey === 'string' ? raw.apiKey : envApiKey,
        apiKeyEnv,
        apiKeySource,
        headers: customHeaders,
        contextWindow: resolveEffectiveContextWindow({
            id,
            label: typeof raw.label === 'string' && raw.label ? raw.label : id,
            backendModel,
            aliases: Array.isArray(raw.aliases) ? raw.aliases.filter((a) => typeof a === 'string') : [],
            endpoint,
            contextWindow: typeof raw.contextWindow === 'number' ? raw.contextWindow : undefined,
        }),
        timeoutMs: typeof raw.timeoutMs === 'number' ? raw.timeoutMs : undefined,
    };
}
function normalizeBackendModelForEndpoint(backendModel, endpoint) {
    const trimmed = backendModel.trim();
    if (!endpoint?.toLowerCase().includes('xiaomimimo.com'))
        return trimmed;
    if (/^mimo[-_]v/i.test(trimmed))
        return trimmed.toLowerCase();
    return trimmed;
}
function toResolved(m) {
    return {
        id: m.id,
        label: m.label,
        backendModel: m.backendModel,
        provider: m.provider,
        endpoint: m.endpoint,
        apiKey: m.apiKey,
        apiKeySource: m.apiKeySource,
        headers: m.headers,
        contextWindow: m.contextWindow,
        timeoutMs: m.timeoutMs,
    };
}
function isOpenAICompatibleEndpoint(endpoint) {
    return /\/chat\/completions\/?$/.test(endpoint);
}
function normalizeOpenAICompatibleChatEndpoint(endpoint, provider) {
    if (provider !== 'openai-compat' && provider !== 'moonshot')
        return null;
    const url = new URL(endpoint);
    const path = url.pathname.replace(/\/+$/, '');
    if (path.endsWith('/chat/completions')) {
        url.pathname = path;
        return url.toString().replace(/\/+$/, '');
    }
    if (path.endsWith('/v1')) {
        url.pathname = `${path}/chat/completions`;
        return url.toString().replace(/\/+$/, '');
    }
    return null;
}
function normalizeKimiCodingChatEndpoint(endpoint) {
    const url = new URL(endpoint);
    const path = url.pathname.replace(/\/+$/, '');
    if (!url.hostname.includes('api.kimi.com'))
        return null;
    if (path.endsWith('/coding/v1/chat/completions') || path.endsWith('/chat/completions')) {
        url.pathname = path;
        return url.toString().replace(/\/+$/, '');
    }
    if (path.endsWith('/coding/v1')) {
        url.pathname = `${path}/chat/completions`;
        return url.toString().replace(/\/+$/, '');
    }
    if (path.endsWith('/coding')) {
        url.pathname = `${path}/v1/chat/completions`;
        return url.toString().replace(/\/+$/, '');
    }
    return null;
}
function normalizeAnthropicMessagesEndpoint(endpoint) {
    const url = new URL(endpoint);
    const path = url.pathname.replace(/\/+$/, '');
    if (path.endsWith('/v1/messages') || path.endsWith('/messages')) {
        url.pathname = path;
        return url.toString().replace(/\/+$/, '');
    }
    if (path.endsWith('/v1/chat/completions')) {
        url.pathname = `${path.slice(0, -'/v1/chat/completions'.length)}/v1/messages`;
        return url.toString().replace(/\/+$/, '');
    }
    if (path.endsWith('/chat/completions')) {
        url.pathname = `${path.slice(0, -'/chat/completions'.length)}/messages`;
        return url.toString().replace(/\/+$/, '');
    }
    if (path === '' || path === '/') {
        url.pathname = '/v1/messages';
        return url.toString().replace(/\/+$/, '');
    }
    if (path.endsWith('/v1')) {
        url.pathname = `${path}/messages`;
        return url.toString().replace(/\/+$/, '');
    }
    url.pathname = `${path}/v1/messages`;
    return url.toString().replace(/\/+$/, '');
}
export function isInteractiveChatModelName(name) {
    const lower = name.toLowerCase();
    return !lower.includes('embedding') && !lower.includes('rerank');
}
export function isInteractiveChatModel(model) {
    return isInteractiveChatModelName(model.id)
        && isInteractiveChatModelName(model.backendModel)
        && isInteractiveChatModelName(model.tier);
}
/**
 * Legacy placeholder written by older `owlcoda init` runs when no local
 * backend was reachable. It has no endpoint and a "your-…" id that no real
 * provider exposes. Treat it as never-default-eligible so a fresh cloud
 * provider added in Admin wins automatically and so launch can detect a
 * placeholder-only config and route to onboarding.
 */
export function isPlaceholderConfiguredModel(model) {
    if (model.endpoint)
        return false;
    return model.id === 'your-default-model' && model.backendModel === 'your-default-model';
}
function findConfiguredModel(config, requestModel) {
    for (const m of config.models) {
        if (m.id === requestModel)
            return m;
    }
    for (const m of config.models) {
        if (m.aliases.includes(requestModel))
            return m;
    }
    for (const m of config.models) {
        if (m.backendModel === requestModel)
            return m;
    }
    const withoutDate = requestModel.replace(/-\d{8}$/, '');
    if (withoutDate !== requestModel) {
        for (const m of config.models) {
            if (m.id === withoutDate || m.aliases.includes(withoutDate)) {
                return m;
            }
        }
    }
    const lower = requestModel.toLowerCase();
    for (const m of config.models) {
        if (m.id.toLowerCase().includes(lower)) {
            return m;
        }
    }
    return null;
}
export function resolveConfiguredModel(config, requestModel) {
    const matched = findConfiguredModel(config, requestModel);
    if (matched)
        return toResolved(matched);
    const def = getDefaultConfiguredModel(config);
    if (def)
        return def;
    return { id: requestModel, label: requestModel, backendModel: requestModel };
}
export function getDefaultConfiguredModel(config) {
    const real = config.models.filter(m => !isPlaceholderConfiguredModel(m));
    const explicitDefault = real.find(m => m.default);
    if (explicitDefault)
        return toResolved(explicitDefault);
    const eligible = real.filter(isDefaultModelEligible);
    const def = eligible[0] ?? real[0];
    if (!def)
        return null;
    return toResolved(def);
}
export function getPreferredInteractiveConfiguredModel(config) {
    const interactive = config.models.filter(m => !isPlaceholderConfiguredModel(m) && isInteractiveChatModel(m));
    const explicitDefault = interactive.find(m => m.default);
    if (explicitDefault)
        return toResolved(explicitDefault);
    const preferred = interactive.find(isDefaultModelEligible) ?? interactive[0];
    if (preferred)
        return toResolved(preferred);
    return getDefaultConfiguredModel(config);
}
export function listConfiguredModels(config) {
    return config.models;
}
export function hasResolvedLocalRuntimeProtocol(config) {
    return config.localRuntimeProtocol === 'openai_chat' || config.localRuntimeProtocol === 'anthropic_messages';
}
export function requiresResolvedLocalRuntimeProtocol(config, requestModel) {
    const resolved = resolveConfiguredModel(config, requestModel);
    return !resolved.endpoint && !hasResolvedLocalRuntimeProtocol(config);
}
// ─── Router Probing ───
export async function probeRouterModels(routerUrl, timeoutMs = 3000) {
    const result = await probeRuntimeSurface(routerUrl, timeoutMs);
    return new Set(result.modelIds);
}
// ─── Availability Overlay ───
export function overlayAvailability(config, routerModelIds) {
    if (routerModelIds.size === 0) {
        for (const m of config.models)
            m.availability = 'unknown';
        return;
    }
    const routerIds = Array.from(routerModelIds);
    for (const m of config.models) {
        if (m.endpoint) {
            m.availability = 'available';
            continue;
        }
        if (routerModelIds.has(m.id) || routerModelIds.has(m.backendModel)) {
            m.availability = 'available';
            continue;
        }
        if (m.aliases.some(a => routerModelIds.has(a))) {
            m.availability = 'available';
            continue;
        }
        const prefixMatch = m.aliases.some(alias => routerIds.some(rm => rm.startsWith(alias + '-')));
        if (prefixMatch) {
            m.availability = 'available';
            continue;
        }
        m.availability = 'unavailable';
    }
}
// ─── Response Model Name ───
export function responseModelName(config, requestModel) {
    const resolved = resolveConfiguredModel(config, requestModel);
    switch (config.responseModelStyle) {
        case 'platform':
            return resolved.id;
        case 'requested':
            return requestModel;
        default:
            return resolved.id;
    }
}
export function resolveModelContextWindow(config, requestModel) {
    return resolveModelContextCapability(config, requestModel).contextWindow;
}
export function resolveModelContextCapability(config, requestModel) {
    const matched = findConfiguredModel(config, requestModel);
    if (matched) {
        return resolveModelCapabilities({
            id: matched.id,
            label: matched.label,
            backendModel: matched.backendModel,
            aliases: matched.aliases,
            endpoint: matched.endpoint,
            contextWindow: matched.contextWindow,
        }).context;
    }
    return resolveModelCapabilities({
        id: requestModel,
        label: requestModel,
        backendModel: requestModel,
    }).context;
}
// ─── Model Routing ───
export function resolveModelRoute(config, requestModel) {
    const resolved = resolveConfiguredModel(config, requestModel);
    const headers = {
        'Content-Type': 'application/json',
        ...(resolved.headers ?? {}),
    };
    if (resolved.endpoint) {
        const normalizedEndpoint = resolved.endpoint.replace(/\/+$/, '');
        const provider = normalizeProviderKind(resolved);
        const kimiChatEndpoint = provider === 'kimi' ? normalizeKimiCodingChatEndpoint(normalizedEndpoint) : null;
        const openAIChatEndpoint = normalizeOpenAICompatibleChatEndpoint(normalizedEndpoint, provider);
        if (kimiChatEndpoint) {
            if (resolved.apiKey) {
                headers['Authorization'] = `Bearer ${resolved.apiKey}`;
            }
            return {
                backendModel: resolved.backendModel,
                endpointUrl: kimiChatEndpoint,
                headers,
                translate: true,
                timeoutMs: resolved.timeoutMs,
            };
        }
        if (openAIChatEndpoint || isOpenAICompatibleEndpoint(normalizedEndpoint)) {
            if (resolved.apiKey) {
                headers['Authorization'] = `Bearer ${resolved.apiKey}`;
            }
            return {
                backendModel: resolved.backendModel,
                endpointUrl: openAIChatEndpoint ?? normalizedEndpoint,
                headers,
                translate: true,
                timeoutMs: resolved.timeoutMs,
            };
        }
        if (resolved.apiKey) {
            headers['x-api-key'] = resolved.apiKey;
            headers['anthropic-version'] = '2023-06-01';
        }
        return {
            backendModel: resolved.backendModel,
            endpointUrl: normalizeAnthropicMessagesEndpoint(normalizedEndpoint),
            headers,
            translate: false,
            timeoutMs: resolved.timeoutMs,
        };
    }
    if (config.localRuntimeProtocol === 'anthropic_messages') {
        return {
            backendModel: resolved.backendModel,
            endpointUrl: `${config.routerUrl}/v1/messages`,
            headers,
            translate: false,
            timeoutMs: resolved.timeoutMs,
        };
    }
    if (!resolved.endpoint && config.localRuntimeProtocol === 'auto') {
        throw new LocalRuntimeProtocolUnresolvedError(`Local runtime protocol unresolved for model "${requestModel}". ` +
            'Expose /v1/openai/models (owlmlx) or /v1/models (generic OpenAI runtimes) on the local runtime, or set localRuntimeProtocol explicitly.');
    }
    return {
        backendModel: resolved.backendModel,
        endpointUrl: `${config.routerUrl}/v1/chat/completions`,
        headers,
        translate: true,
        timeoutMs: resolved.timeoutMs,
    };
}
// ─── Legacy Helpers ───
export function resolveModel(config, requestModel) {
    if (config.models.length > 0) {
        return resolveConfiguredModel(config, requestModel).backendModel;
    }
    if (config.modelMap[requestModel])
        return config.modelMap[requestModel];
    const withoutDate = requestModel.replace(/-\d{8}$/, '');
    if (withoutDate !== requestModel && config.modelMap[withoutDate])
        return config.modelMap[withoutDate];
    const localModels = new Set(Object.values(config.modelMap));
    if (localModels.has(requestModel))
        return requestModel;
    return config.defaultModel;
}
export function reverseModel(config, requestModel) {
    if (config.models.length > 0) {
        return responseModelName(config, requestModel);
    }
    if (!config.reverseMapInResponse)
        return requestModel;
    if (requestModel in config.modelMap)
        return requestModel;
    for (const [key, value] of Object.entries(config.modelMap)) {
        if (value === requestModel)
            return key;
    }
    return requestModel;
}
//# sourceMappingURL=model-registry.js.map