import { normalizeModel } from './config.js';
import { normalizeProviderKind } from './provider-kind.js';
import { classifyProviderRequestError, createProviderHttpDiagnostic, formatProviderDiagnostic, upstreamRequestIdFromHeaders, } from './provider-error.js';
const PROVIDER_TEMPLATES = [
    {
        id: 'kimi',
        provider: 'kimi',
        label: 'Kimi',
        tier: 'cloud',
        endpoint: 'https://api.kimi.com/coding/v1',
        defaultModelId: 'kimi-code',
        defaultModelLabel: 'Kimi Code',
        defaultBackendModel: 'kimi-for-coding',
        defaultAliases: ['kimi'],
        defaultContextWindow: 256000,
        headers: { 'User-Agent': 'KimiCLI/1.33.0' },
        testPath: '/chat/completions',
        testMode: 'chat',
        family: 'single-model',
        description: 'Domestic brand adapter. Paste a Kimi key; OwlCoda uses the Kimi Code route that is already proven in dogfood.',
        backendModelHint: 'Default is kimi-for-coding. Change only if your Kimi model picker shows a newer exact id.',
        featured: true,
        docs: 'https://www.kimi.com/code/docs/en/kimi-code-cli/configuration/providers-and-models.html',
    },
    {
        id: 'deepseek',
        provider: 'anthropic',
        label: 'DeepSeek',
        tier: 'cloud',
        endpoint: 'https://api.deepseek.com/anthropic',
        defaultModelId: 'deepseek',
        defaultModelLabel: 'DeepSeek',
        defaultBackendModel: 'deepseek-chat',
        defaultAliases: ['deepseek', 'ds'],
        defaultContextWindow: 128000,
        testPath: '/v1/messages',
        testMode: 'messages',
        family: 'single-model',
        description: 'Domestic brand adapter. Uses the Anthropic-compatible DeepSeek route favored by Claude Code switchers.',
        backendModelHint: 'Default is deepseek-chat.',
        featured: true,
    },
    {
        id: 'glm',
        provider: 'anthropic',
        label: 'GLM',
        tier: 'cloud',
        endpoint: 'https://api.z.ai/api/anthropic',
        defaultModelId: 'glm',
        defaultModelLabel: 'GLM',
        defaultBackendModel: 'glm-5',
        defaultAliases: ['glm', 'zai'],
        defaultContextWindow: 128000,
        testPath: '/v1/messages',
        testMode: 'messages',
        family: 'single-model',
        description: 'Domestic brand adapter for Zhipu / z.ai GLM. Paste a GLM key and keep the default model unless your console says otherwise.',
        backendModelHint: 'Default is glm-5.',
        featured: true,
        docs: 'https://z.ai',
    },
    {
        id: 'minimax',
        provider: 'anthropic',
        label: 'MiniMax',
        tier: 'cloud',
        endpoint: 'https://api.minimaxi.com/anthropic',
        defaultModelId: 'minimax-m27',
        defaultModelLabel: 'MiniMax M2.7-highspeed',
        defaultBackendModel: 'MiniMax-M2.7-highspeed',
        defaultAliases: ['minimax', 'm27'],
        defaultContextWindow: 204800,
        testPath: '/v1/messages',
        testMode: 'messages',
        family: 'single-model',
        description: 'Domestic brand adapter. Uses the MiniMax Anthropic-compatible route; paste a key and test.',
        endpointHint: 'Default is the China host api.minimaxi.com. Use the generic Anthropic-compatible channel if your account is global-only.',
        backendModelHint: 'Default is MiniMax-M2.7-highspeed.',
        featured: true,
        docs: 'https://platform.minimax.io/docs/guides/text-ai-coding-tools',
    },
    {
        id: 'gpt',
        provider: 'openai-compat',
        label: 'GPT',
        tier: 'cloud',
        endpoint: 'https://api.openai.com/v1',
        defaultModelId: 'gpt',
        defaultModelLabel: 'GPT',
        defaultBackendModel: 'gpt-5.1',
        defaultAliases: ['gpt', 'openai'],
        defaultContextWindow: 256000,
        testPath: '/chat/completions',
        testMode: 'chat',
        family: 'single-model',
        description: 'Global brand adapter for OpenAI GPT. Paste an OpenAI API key.',
        backendModelHint: 'Default is gpt-5.1; change it if your OpenAI account should use a different GPT model.',
        featured: true,
        docs: 'https://platform.openai.com/docs/models',
    },
    {
        id: 'claude',
        provider: 'anthropic',
        label: 'Claude',
        tier: 'cloud',
        endpoint: 'https://api.anthropic.com',
        defaultModelId: 'claude',
        defaultModelLabel: 'Claude',
        defaultBackendModel: 'claude-sonnet-4-5-20250929',
        defaultAliases: ['claude', 'sonnet'],
        defaultContextWindow: 200000,
        testPath: '/v1/messages',
        testMode: 'messages',
        family: 'single-model',
        description: 'Global brand adapter for Anthropic Claude. Paste an Anthropic key.',
        backendModelHint: 'Default is Claude Sonnet 4.5.',
        featured: true,
        docs: 'https://docs.anthropic.com',
    },
    {
        id: 'gemini',
        provider: 'openai-compat',
        label: 'Gemini',
        tier: 'cloud',
        endpoint: 'https://generativelanguage.googleapis.com/v1beta/openai',
        defaultModelId: 'gemini',
        defaultModelLabel: 'Gemini',
        defaultBackendModel: 'gemini-2.5-pro',
        defaultAliases: ['gemini'],
        defaultContextWindow: 1048576,
        testPath: '/chat/completions',
        testMode: 'chat',
        family: 'single-model',
        description: 'Global brand adapter for Google Gemini through the OpenAI-compatible API.',
        backendModelHint: 'Default is gemini-2.5-pro.',
        featured: true,
        docs: 'https://ai.google.dev/gemini-api/docs/openai',
    },
    {
        id: 'grok',
        provider: 'openai-compat',
        label: 'Grok',
        tier: 'cloud',
        endpoint: 'https://api.x.ai/v1',
        defaultModelId: 'grok',
        defaultModelLabel: 'Grok',
        defaultBackendModel: 'grok-4',
        defaultAliases: ['grok', 'xai'],
        defaultContextWindow: 256000,
        testPath: '/chat/completions',
        testMode: 'chat',
        family: 'single-model',
        description: 'Global brand adapter for xAI Grok through its OpenAI-compatible API.',
        backendModelHint: 'Default is grok-4.',
        featured: true,
        docs: 'https://docs.x.ai/docs/api-reference',
    },
    {
        id: 'openai-compat',
        provider: 'openai-compat',
        label: 'OpenAI-compatible',
        tier: 'custom',
        endpoint: '',
        testPath: '/chat/completions',
        testMode: 'chat',
        family: 'multi-model',
        description: 'Other providers: choose this for any OpenAI-compatible chat/completions endpoint.',
        endpointHint: 'Paste the base URL, for example api.your-gateway.com/v1. The https:// prefix is added automatically if missing.',
        backendModelHint: 'Enter the exact backend model id exposed by your provider.',
        requiresBackendModel: true,
    },
    {
        id: 'anthropic',
        provider: 'anthropic',
        label: 'Anthropic-compatible',
        tier: 'custom',
        endpoint: '',
        testPath: '/v1/messages',
        testMode: 'messages',
        family: 'multi-model',
        description: 'Other providers: choose this for any Anthropic-compatible /v1/messages endpoint.',
        endpointHint: 'Paste the base URL, for example api.your-relay.com/anthropic. The https:// prefix is added automatically if missing.',
        backendModelHint: 'Enter the exact backend model id exposed by your provider.',
        requiresBackendModel: true,
    },
    {
        id: 'ollama',
        provider: 'openai-compat',
        label: 'Ollama',
        tier: 'local',
        endpoint: 'http://localhost:11434/v1',
        testPath: '/models',
        testMode: 'models',
        family: 'multi-model',
        description: 'Local Ollama runtime. Exposes OpenAI-compatible /v1 endpoints on the default port 11434.',
        endpointHint: 'Default is http://localhost:11434/v1. Change the host or port if your Ollama listens elsewhere.',
        backendModelHint: 'Enter the exact local model id from `ollama list`, for example llama3.1:8b.',
        requiresBackendModel: true,
    },
    {
        id: 'lm-studio',
        provider: 'openai-compat',
        label: 'LM Studio',
        tier: 'local',
        endpoint: 'http://localhost:1234/v1',
        testPath: '/models',
        testMode: 'models',
        family: 'multi-model',
        description: 'Local LM Studio server. Start the LM Studio Local Server tab first; default port 1234.',
        endpointHint: 'Default is http://localhost:1234/v1. Adjust if you changed the LM Studio server port.',
        backendModelHint: 'Enter the backend model id LM Studio shows for the currently loaded model.',
        requiresBackendModel: true,
    },
    {
        id: 'vllm',
        provider: 'openai-compat',
        label: 'vLLM',
        tier: 'local',
        endpoint: 'http://localhost:8000/v1',
        testPath: '/models',
        testMode: 'models',
        family: 'multi-model',
        description: 'Local vLLM OpenAI-compatible server. Default port 8000 unless vLLM was started with --port.',
        endpointHint: 'Default is http://localhost:8000/v1. Change if vLLM was started with --port or --host.',
        backendModelHint: 'Enter the exact model id passed to vLLM --served-model-name, or the HuggingFace repo id.',
        requiresBackendModel: true,
    },
    {
        id: 'owlmlx',
        provider: 'openai-compat',
        label: 'owlmlx (Apple MLX)',
        tier: 'local',
        endpoint: 'http://localhost:8066/v1',
        testPath: '/models',
        testMode: 'models',
        family: 'multi-model',
        description: 'Local owlmlx runtime for Apple Silicon. Default port 8066, OpenAI-compatible surface.',
        endpointHint: 'Default is http://localhost:8066/v1. owlmlx exposes /v1/openai/models for visibility and /v1/chat/completions for generation.',
        backendModelHint: 'Enter the exact model id owlmlx has loaded (visible via /v1/openai/models).',
        requiresBackendModel: true,
    },
];
function defaultDeps() {
    return {
        fetch: globalThis.fetch,
        now: () => Date.now(),
    };
}
export class ProviderProbe {
    deps;
    constructor(options = {}) {
        this.deps = {
            ...defaultDeps(),
            ...options.deps,
        };
    }
    async test(input) {
        const request = normalizeProbeInput(input);
        const credentialSource = request.model.apiKeySource ?? 'unset';
        const breadcrumb = {
            provider: request.providerId,
            backendModel: request.model.backendModel ?? request.model.id ?? '',
            credentialSource,
        };
        if (!request.model.endpoint) {
            return {
                ok: false,
                status: 400,
                latencyMs: 0,
                detail: 'Missing endpoint',
                ...breadcrumb,
                endpoint: '',
            };
        }
        if (!this.deps.fetch) {
            return {
                ok: false,
                status: 500,
                latencyMs: 0,
                detail: 'Fetch API is unavailable',
                ...breadcrumb,
                endpoint: request.model.endpoint,
            };
        }
        const { url, init } = buildRequest(request);
        const start = this.deps.now();
        try {
            const response = await this.deps.fetch(url, init);
            const latencyMs = Math.max(0, this.deps.now() - start);
            const rawBody = await response.text();
            if (!response.ok) {
                const diagnostic = createProviderHttpDiagnostic(response.status, rawBody, {
                    provider: request.providerId,
                    model: request.model.id,
                    endpointUrl: request.model.endpoint,
                    headers: request.model.headers,
                    upstreamRequestId: upstreamRequestIdFromHeaders(response.headers),
                });
                return {
                    ok: false,
                    status: diagnostic.status ?? response.status,
                    latencyMs,
                    detail: formatProviderDiagnostic(diagnostic, { includeRequestId: true }),
                    ...breadcrumb,
                    endpoint: url,
                    ...(rawBody ? { bodySnippet: truncateBody(rawBody) } : {}),
                };
            }
            if (usesModelListProbe(request)) {
                const listedModels = extractModelIdsFromModelsResponse(rawBody);
                const backendModel = request.model.backendModel ?? request.model.id;
                if (backendModel && listedModels.length > 0 && !listedModels.includes(backendModel)) {
                    return {
                        ok: false,
                        status: response.status,
                        latencyMs,
                        detail: `Provider responded, but selected model "${backendModel}" is not visible from /models. No fallback model was tested.`,
                        ...breadcrumb,
                        endpoint: url,
                        ...(rawBody ? { bodySnippet: truncateBody(rawBody) } : {}),
                    };
                }
                if (backendModel && listedModels.length === 0) {
                    return {
                        ok: false,
                        status: response.status,
                        latencyMs,
                        detail: `Provider /models response did not include model ids, so OwlCoda could not verify selected model "${backendModel}". No fallback model was tested.`,
                        ...breadcrumb,
                        endpoint: url,
                        ...(rawBody ? { bodySnippet: truncateBody(rawBody) } : {}),
                    };
                }
            }
            return {
                ok: true,
                status: response.status,
                latencyMs,
                detail: `${request.providerId} probe succeeded for ${breadcrumb.backendModel || request.model.id}`,
                ...breadcrumb,
                endpoint: url,
            };
        }
        catch (error) {
            const diagnostic = classifyProviderRequestError(error, {
                provider: request.providerId,
                model: request.model.id,
                endpointUrl: request.model.endpoint,
                headers: request.model.headers,
            });
            return {
                ok: false,
                status: diagnostic.status ?? 0,
                latencyMs: Math.max(0, this.deps.now() - start),
                detail: formatProviderDiagnostic(diagnostic, { includeRequestId: true }),
                ...breadcrumb,
                endpoint: url,
            };
        }
    }
}
export function getProviderTemplates() {
    return PROVIDER_TEMPLATES.map(template => ({ ...template }));
}
function normalizeProbeInput(input) {
    if (isConfiguredModel(input)) {
        const template = inferProviderTemplate(input);
        return {
            providerId: template?.id ?? normalizeProviderKind(input),
            provider: template?.provider ?? normalizeProviderKind(input),
            model: input,
            testPath: template?.testPath,
            testMode: template?.testMode ?? inferProbeMode(input.endpoint),
        };
    }
    const template = inferProviderTemplate(input);
    const normalized = normalizeModel({
        id: input.id ?? 'dry-run-model',
        label: input.label ?? input.id ?? 'Dry Run Model',
        backendModel: input.backendModel ?? input.id ?? 'dry-run-model',
        aliases: input.aliases ?? [],
        endpoint: input.endpoint,
        apiKey: input.apiKey,
        apiKeyEnv: input.apiKeyEnv,
        headers: input.headers,
        timeoutMs: input.timeoutMs,
        role: input.role,
        contextWindow: input.contextWindow,
        tier: 'cloud',
    });
    return {
        providerId: template?.id ?? input.provider ?? normalizeProviderKind({ ...normalized, provider: input.provider }),
        provider: template?.provider ?? normalizeProviderKind({ ...normalized, provider: input.provider }),
        model: normalized,
        testPath: input.testPath ?? template?.testPath,
        testMode: input.testMode ?? template?.testMode ?? inferProbeMode(input.endpoint),
    };
}
function isConfiguredModel(input) {
    return 'tier' in input;
}
function buildRequest(request) {
    const headers = new Headers(request.model.headers ?? {});
    // Probes should be fast, but 5s is too aggressive for some cloud providers
    // (for example MiniMax anthropic-compatible endpoints often land just under 10s).
    const timeoutMs = resolveProbeTimeoutMs(request.model.timeoutMs);
    const apiKey = request.model.apiKey;
    if (usesAnthropicMessagesProbe(request)) {
        if (apiKey)
            headers.set('x-api-key', apiKey);
        headers.set('anthropic-version', '2023-06-01');
        headers.set('content-type', 'application/json');
        return {
            url: resolveTargetUrl(request.model.endpoint, request.testPath ?? '/v1/messages', 'anthropic'),
            init: {
                method: 'POST',
                headers,
                body: JSON.stringify({
                    model: request.model.backendModel,
                    max_tokens: 1,
                    messages: [{ role: 'user', content: 'ping' }],
                }),
                signal: AbortSignal.timeout(timeoutMs),
            },
        };
    }
    if (usesOpenAIChatProbe(request)) {
        if (apiKey)
            headers.set('authorization', `Bearer ${apiKey}`);
        headers.set('content-type', 'application/json');
        return {
            url: resolveTargetUrl(request.model.endpoint, request.testPath ?? defaultTestPathForRequest(request), request.provider),
            init: {
                method: 'POST',
                headers,
                body: JSON.stringify({
                    model: request.model.backendModel,
                    max_tokens: 1,
                    messages: [{ role: 'user', content: 'ping' }],
                }),
                signal: AbortSignal.timeout(timeoutMs),
            },
        };
    }
    if (apiKey)
        headers.set('authorization', `Bearer ${apiKey}`);
    return {
        url: resolveTargetUrl(request.model.endpoint, request.testPath ?? defaultTestPathForRequest(request), request.provider),
        init: {
            method: 'GET',
            headers,
            signal: AbortSignal.timeout(timeoutMs),
        },
    };
}
function resolveProbeTimeoutMs(timeoutMs) {
    return typeof timeoutMs === 'number' && Number.isFinite(timeoutMs) && timeoutMs > 0
        ? timeoutMs
        : 15_000;
}
const PROBE_BODY_SNIPPET_MAX = 500;
function truncateBody(raw) {
    const trimmed = raw.trim();
    if (trimmed.length <= PROBE_BODY_SNIPPET_MAX)
        return trimmed;
    const head = trimmed.slice(0, PROBE_BODY_SNIPPET_MAX);
    return `${head}…[truncated, ${trimmed.length - PROBE_BODY_SNIPPET_MAX} more chars]`;
}
function defaultTestPathForRequest(request) {
    if (request.testMode === 'messages')
        return '/v1/messages';
    if (request.testMode === 'chat')
        return '/chat/completions';
    switch (request.provider) {
        case 'openai-compat':
        case 'moonshot':
        case 'custom':
            return '/models';
        case 'kimi':
            return '/chat/completions';
        case 'anthropic':
            return '/v1/messages';
    }
}
function usesAnthropicMessagesProbe(request) {
    if (request.testMode === 'messages' || request.provider === 'anthropic') {
        return true;
    }
    const endpoint = request.model.endpoint?.toLowerCase() ?? '';
    return endpoint.includes('/anthropic') || endpoint.includes('/v1/messages');
}
function usesOpenAIChatProbe(request) {
    if (request.testMode === 'chat')
        return true;
    if (request.testMode === 'messages')
        return false;
    return inferProbeMode(request.model.endpoint) === 'chat';
}
function usesModelListProbe(request) {
    if (usesAnthropicMessagesProbe(request) || usesOpenAIChatProbe(request))
        return false;
    const path = (request.testPath ?? defaultTestPathForRequest(request)).trim().toLowerCase();
    return path === 'models' || path.endsWith('/models');
}
function extractModelIdsFromModelsResponse(rawBody) {
    if (!rawBody.trim())
        return [];
    try {
        const parsed = JSON.parse(rawBody);
        const ids = new Set();
        collectModelIds(parsed, ids);
        return [...ids];
    }
    catch {
        return [];
    }
}
function collectModelIds(value, ids) {
    if (!value || typeof value !== 'object')
        return;
    if (Array.isArray(value)) {
        for (const item of value)
            collectModelIds(item, ids);
        return;
    }
    const record = value;
    if (typeof record.id === 'string' && record.id.trim()) {
        ids.add(record.id);
    }
    const nested = record.data ?? record.models;
    if (Array.isArray(nested)) {
        for (const item of nested)
            collectModelIds(item, ids);
    }
}
function inferProviderTemplate(input) {
    const explicit = typeof input.provider === 'string' ? input.provider.trim().toLowerCase() : '';
    if (explicit) {
        const fromId = PROVIDER_TEMPLATES.find(template => template.id === explicit);
        if (fromId)
            return fromId;
    }
    const endpoint = input.endpoint?.toLowerCase() ?? '';
    if (endpoint.includes('api.kimi.com'))
        return PROVIDER_TEMPLATES.find(template => template.id === 'kimi');
    if (endpoint.includes('api.deepseek.com'))
        return PROVIDER_TEMPLATES.find(template => template.id === 'deepseek');
    if (endpoint.includes('api.z.ai') || endpoint.includes('open.bigmodel.cn'))
        return PROVIDER_TEMPLATES.find(template => template.id === 'glm');
    if (endpoint.includes('api.minimax.io') || endpoint.includes('api.minimaxi.com'))
        return PROVIDER_TEMPLATES.find(template => template.id === 'minimax');
    if (endpoint.includes('api.openai.com'))
        return PROVIDER_TEMPLATES.find(template => template.id === 'gpt');
    if (endpoint.includes('api.anthropic.com'))
        return PROVIDER_TEMPLATES.find(template => template.id === 'claude');
    if (endpoint.includes('generativelanguage.googleapis.com'))
        return PROVIDER_TEMPLATES.find(template => template.id === 'gemini');
    if (endpoint.includes('api.x.ai'))
        return PROVIDER_TEMPLATES.find(template => template.id === 'grok');
    const normalized = normalizeProviderKind(input);
    return PROVIDER_TEMPLATES.find(template => template.id === normalized)
        ?? PROVIDER_TEMPLATES.find(template => template.provider === normalized);
}
function inferProbeMode(endpoint) {
    const normalized = endpoint?.toLowerCase() ?? '';
    if (normalized.includes('/anthropic') || normalized.includes('/v1/messages'))
        return 'messages';
    if (normalized.includes('dashscope') && normalized.includes('compatible-mode'))
        return 'chat';
    return 'models';
}
function resolveTargetUrl(endpoint, testPath, provider) {
    const url = new URL(endpoint);
    const rawPath = testPath.trim();
    if (!rawPath) {
        return url.toString();
    }
    if (provider === 'kimi') {
        const normalized = url.pathname.replace(/\/+$/, '');
        if (normalized.endsWith('/coding/v1/chat/completions') || normalized.endsWith('/chat/completions')) {
            return url.toString();
        }
        if (normalized.endsWith('/coding/v1')) {
            url.pathname = `${normalized}/chat/completions`;
            return url.toString();
        }
        if (normalized.endsWith('/coding')) {
            url.pathname = `${normalized}/v1/chat/completions`;
            return url.toString();
        }
        if (normalized.endsWith('/v1')) {
            url.pathname = `${normalized}/chat/completions`;
            return url.toString();
        }
    }
    if (provider === 'anthropic') {
        const anthropicMessagePath = rewriteCompletionEndpointToMessages(url.pathname);
        if (anthropicMessagePath) {
            url.pathname = anthropicMessagePath;
            return url.toString();
        }
        if (url.pathname.endsWith('/messages'))
            return url.toString();
        if (url.pathname.endsWith('/v1/messages'))
            return url.toString();
        if (url.pathname === '/' || url.pathname === '') {
            url.pathname = rawPath;
            return url.toString();
        }
        if (url.pathname.endsWith('/v1')) {
            url.pathname = `${url.pathname}${rawPath.replace(/^\/v1/, '')}`;
            return url.toString();
        }
        url.pathname = `${url.pathname.replace(/\/+$/, '')}${rawPath.startsWith('/') ? rawPath : `/${rawPath}`}`;
        return url.toString();
    }
    if (url.pathname.endsWith('/models') || url.pathname.endsWith('/v1/models')) {
        return url.toString();
    }
    if (url.pathname.endsWith('/v1')) {
        url.pathname = `${url.pathname}${rawPath.startsWith('/') ? rawPath : `/${rawPath}`}`;
        return url.toString();
    }
    if (url.pathname === '/' || url.pathname === '') {
        url.pathname = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
        return url.toString();
    }
    url.pathname = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;
    return url.toString();
}
function rewriteCompletionEndpointToMessages(pathname) {
    if (pathname.endsWith('/v1/chat/completions')) {
        return `${pathname.slice(0, -'/v1/chat/completions'.length)}/v1/messages`;
    }
    if (pathname.endsWith('/chat/completions')) {
        return `${pathname.slice(0, -'/chat/completions'.length)}/messages`;
    }
    return null;
}
//# sourceMappingURL=provider-probe.js.map