/**
 * Model registry - manages built-in and custom models, provides API key resolution.
 */
import { createModels, } from "@earendil-works/pi-ai";
import { builtinProviders } from "@earendil-works/pi-ai/providers/all";
import { dirname, join } from "node:path";
import { getAgentConfigPaths } from "../config.js";
import { normalizePath } from "../utils/paths.js";
import { copilotApiBaseUrlFromToken, copilotCatalogCachePath, copilotTokenFromEnvironment, seedActiveCopilotModelCatalogFromCache } from "./copilot-model-catalog.js";
import { getModelRequestAuth, getApiKeyForProviderFromConfig, getProviderAuthStatusFromConfig } from "./model-registry-auth.js";
import { applyProviderConfigToModels, migrateLegacyRegisterProviderConfigValues, unregisterProviderRuntime, validateProviderConfig } from "./model-registry-dynamic.js";
import { loadModelRegistryModels } from "./model-registry-loader.js";
import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "./provider-display-names.js";
import { FileModelsStore, InMemoryCodingAgentModelsStore } from "./models-store.js";
import { getLegacyOAuthProvider, oauthCredentialToAuth } from "./oauth-provider-bridge.js";
import { withRemoteCatalog } from "./remote-catalog-provider.js";
import { clearConfigValueCache, isConfigValueConfigured } from "./resolve-config-value.js";
const REMOTE_CATALOG_PROVIDERS = new Set(["cursor", "github-copilot", "openrouter", "vercel-ai-gateway"]);
const OPENAI_COMPATIBLE_APIS = new Set(["openai-completions", "openai-responses"]);
let nextRegistryRegistrationId = 0;
function copilotBaseUrlFromRuntimeToken(apiKey) {
    return /(?:^|;)proxy-ep=[^;]+/.test(apiKey) ? copilotApiBaseUrlFromToken(apiKey) : undefined;
}
function overlayRuntimeApiKey(runtimeApiKey, resolvedAuth, storedOAuthAuth, runtimeBaseUrl) {
    const headers = storedOAuthAuth?.headers || resolvedAuth?.headers
        ? { ...storedOAuthAuth?.headers, ...resolvedAuth?.headers }
        : undefined;
    return {
        ...storedOAuthAuth,
        ...resolvedAuth,
        apiKey: runtimeApiKey,
        headers,
        baseUrl: runtimeBaseUrl ?? storedOAuthAuth?.baseUrl ?? resolvedAuth?.baseUrl,
    };
}
function createProviderCredentialStore(credentials) {
    return {
        ...credentials,
        read: async (providerId) => {
            const credential = await credentials.read(providerId);
            if (credential?.type !== "oauth" || !getLegacyOAuthProvider(providerId))
                return credential;
            const auth = await oauthCredentialToAuth(providerId, credential);
            return auth?.apiKey === undefined ? undefined : { type: "api_key", key: auth.apiKey };
        },
    };
}
/** Clear the config value command cache. Exported for testing. */
export const clearApiKeyCache = clearConfigValueCache;
/**
 * Model registry - loads and manages models, resolves API keys via AuthStorage.
 */
export class ModelRegistry {
    constructor(authStorage, modelsJsonPaths) {
        this.models = [];
        this.modelOverrides = new Map();
        this.providerRequestConfigs = new Map();
        this.modelRequestHeaders = new Map();
        this.registeredProviders = new Map();
        this.builtInProviders = new Set();
        this.customOpenAICompatibleProviders = new Set();
        this.loadError = undefined;
        this.refreshGeneration = 0;
        this.registrationSource = `atomic:model-registry:${++nextRegistryRegistrationId}`;
        this.authStorage = authStorage;
        this.modelsJsonPaths = modelsJsonPaths.map((path) => normalizePath(path));
        this.modelsStore = this.modelsJsonPaths.length > 0
            ? new FileModelsStore(join(dirname(this.modelsJsonPaths[0]), "models-store.json"))
            : new InMemoryCodingAgentModelsStore();
        this.credentialStore = authStorage.asCredentialStore();
        this.providerModels = createModels({
            credentials: createProviderCredentialStore(this.credentialStore),
            modelsStore: this.modelsStore,
        });
        for (const provider of builtinProviders()) {
            this.providerModels.setProvider(provider.id === "radius" ? provider : withRemoteCatalog(provider));
        }
        this.seedCopilotModelCatalogFromCache();
        this.loadModels();
    }
    seedCopilotModelCatalogFromCache() {
        if (this.modelsJsonPaths.length === 0)
            return;
        const cred = this.authStorage.get("github-copilot");
        const token = cred?.type === "oauth" && typeof cred.access === "string" ? cred.access : copilotTokenFromEnvironment();
        if (!token)
            return;
        seedActiveCopilotModelCatalogFromCache(token, copilotCatalogCachePath(dirname(this.modelsJsonPaths[0])));
    }
    static create(authStorage, modelsJsonPath = getAgentConfigPaths("models.json")) {
        return new ModelRegistry(authStorage, Array.isArray(modelsJsonPath) ? modelsJsonPath : [modelsJsonPath]);
    }
    static inMemory(authStorage) {
        return new ModelRegistry(authStorage, []);
    }
    /**
     * Reload models from disk (built-in + custom from models.json).
     */
    async refresh(options = {}) {
        const generation = ++this.refreshGeneration;
        this.loadError = undefined;
        this.rebuildProviderModels();
        const controller = new AbortController();
        const abort = () => controller.abort();
        options.signal?.addEventListener("abort", abort, { once: true });
        if (options.signal?.aborted)
            controller.abort(options.signal.reason);
        const timeout = setTimeout(abort, options.timeoutMs ?? 15_000);
        const errors = new Map();
        const aborted = new Promise((resolve) => {
            if (controller.signal.aborted)
                resolve();
            else
                controller.signal.addEventListener("abort", () => resolve(), { once: true });
        });
        try {
            const restore = this.providerModels
                .refresh({ allowNetwork: false, signal: controller.signal })
                .then((result) => {
                if (controller.signal.aborted || generation !== this.refreshGeneration)
                    return;
                for (const [provider, error] of result.errors)
                    errors.set(provider, error);
                this.publishProviderModels();
            });
            await Promise.race([restore, aborted]);
            if (options.allowNetwork !== false && !controller.signal.aborted) {
                const legacyOAuthProviders = new Set([
                    ...this.models.map((model) => model.provider),
                    ...[...this.registeredProviders]
                        .filter(([, config]) => config.oauth !== undefined)
                        .map(([providerId]) => providerId),
                ].filter((providerId) => getLegacyOAuthProvider(providerId) !== undefined));
                const refreshLegacyOAuth = Promise.all([...legacyOAuthProviders].map(async (providerId) => {
                    const credential = this.authStorage.get(providerId);
                    if (credential?.type !== "oauth" || Date.now() < credential.expires)
                        return;
                    await this.authStorage.getModelAuth(providerId, { includeFallback: false });
                }));
                await Promise.race([refreshLegacyOAuth, aborted]);
            }
            const extensionRefreshes = controller.signal.aborted ? [] : [...this.registeredProviders].map(async ([providerName, config]) => {
                if (!config.refreshModels)
                    return;
                const isCurrentExtensionRefresh = () => !controller.signal.aborted
                    && generation === this.refreshGeneration
                    && this.registeredProviders.get(providerName) === config;
                const store = {
                    read: () => this.modelsStore.read(providerName),
                    write: (entry) => this.modelsStore.writeIf(providerName, entry, isCurrentExtensionRefresh),
                    delete: () => this.modelsStore.deleteIf(providerName, isCurrentExtensionRefresh),
                };
                try {
                    let credential = await this.credentialStore.read(providerName);
                    if (credential?.type === "api_key") {
                        const effectiveApiKey = await this.authStorage.getApiKey(providerName, { includeFallback: false });
                        credential = effectiveApiKey === undefined ? undefined : { type: "api_key", key: effectiveApiKey };
                    }
                    if (credential === undefined) {
                        const configuredApiKey = await getApiKeyForProviderFromConfig(providerName, this.authStorage, this.providerRequestConfigs);
                        if (configuredApiKey !== undefined)
                            credential = { type: "api_key", key: configuredApiKey };
                    }
                    if (!isCurrentExtensionRefresh())
                        return;
                    const models = await config.refreshModels({
                        credential,
                        store,
                        allowNetwork: options.allowNetwork ?? true,
                        force: options.force,
                        signal: controller.signal,
                    });
                    if (!isCurrentExtensionRefresh())
                        return;
                    const refreshed = { ...config, models };
                    this.registeredProviders.set(providerName, refreshed);
                    this.rebuildProviderModels();
                }
                catch (error) {
                    if (generation === this.refreshGeneration && !controller.signal.aborted) {
                        errors.set(providerName, error instanceof Error ? error : new Error(String(error)));
                    }
                }
            });
            const builtinRefresh = controller.signal.aborted
                ? Promise.resolve()
                : this.providerModels
                    .refresh({ allowNetwork: options.allowNetwork ?? true, force: options.force, signal: controller.signal })
                    .then((result) => {
                    if (controller.signal.aborted || generation !== this.refreshGeneration)
                        return;
                    for (const [provider, error] of result.errors)
                        errors.set(provider, error);
                    this.publishProviderModels();
                });
            await Promise.race([Promise.all(extensionRefreshes), aborted]);
            await Promise.race([builtinRefresh, aborted]);
        }
        finally {
            clearTimeout(timeout);
            options.signal?.removeEventListener("abort", abort);
        }
        return { aborted: controller.signal.aborted, errors };
    }
    publishProviderModels() {
        this.rebuildProviderModels();
    }
    rebuildProviderModels() {
        this.loadModels(this.providerModels.getModels());
        for (const [providerName, config] of this.registeredProviders) {
            this.applyProviderConfig(providerName, config);
        }
    }
    providerRegistrationSource(providerName) {
        return `${this.registrationSource}:${providerName}`;
    }
    /**
     * Get any error from loading models.json (undefined if no error).
     */
    getError() {
        return this.loadError;
    }
    loadModels(baseModels) {
        const loaded = loadModelRegistryModels(this.authStorage, this.modelsJsonPaths, baseModels);
        this.modelOverrides = loaded.modelOverrides;
        this.models = loaded.models;
        this.providerRequestConfigs = loaded.providerRequestConfigs;
        this.modelRequestHeaders = loaded.modelRequestHeaders;
        this.builtInProviders = loaded.builtInProviders;
        this.customOpenAICompatibleProviders = loaded.customOpenAICompatibleProviders;
        this.loadError = loaded.loadError;
    }
    /**
     * Get all models (built-in + custom).
     * If models.json had errors, returns only built-in models.
     */
    getAll() {
        return this.models;
    }
    /**
     * Get only models that have auth configured.
     * This is a fast check that doesn't refresh OAuth tokens.
     */
    getAvailable() {
        const configured = this.models.filter((model) => this.hasConfiguredAuth(model));
        const allowedByProvider = new Map();
        for (const provider of this.providerModels.getProviders()) {
            const extension = this.registeredProviders.get(provider.id);
            if (!provider.filterModels || extension?.models || extension?.oauth)
                continue;
            const providerModels = configured.filter((model) => model.provider === provider.id);
            const runtimeApiKey = this.authStorage.getRuntimeApiKey(provider.id);
            const credential = runtimeApiKey === undefined
                ? this.authStorage.get(provider.id)
                : { type: "api_key", key: runtimeApiKey };
            allowedByProvider.set(provider.id, new Set(provider.filterModels(providerModels, credential).map((model) => model.id)));
        }
        return configured.filter((model) => allowedByProvider.get(model.provider)?.has(model.id) ?? true);
    }
    /**
     * Find a model by provider and ID.
     */
    find(provider, modelId) {
        return this.models.find((m) => m.provider === provider && m.id === modelId);
    }
    /** Whether an authenticated provider may reconstruct an absent saved model ID. */
    canRestoreUnknownModel(provider) {
        if (REMOTE_CATALOG_PROVIDERS.has(provider))
            return true;
        if (this.customOpenAICompatibleProviders.has(provider))
            return true;
        if (this.builtInProviders.has(provider))
            return false;
        const config = this.registeredProviders.get(provider);
        return (config?.models?.some((model) => {
            const api = model.api ?? config.api;
            return api !== undefined && OPENAI_COMPATIBLE_APIS.has(api);
        }) === true);
    }
    /**
     * Get API key for a model.
     */
    hasConfiguredAuth(model) {
        const providerApiKey = this.providerRequestConfigs.get(model.provider)?.apiKey;
        return (this.authStorage.hasAuth(model.provider) ||
            (providerApiKey !== undefined && isConfigValueConfigured(providerApiKey)));
    }
    getModelRequestKey(provider, modelId) {
        return `${provider}:${modelId}`;
    }
    storeProviderRequestConfig(providerName, config) {
        if (!config.apiKey && !config.headers && !config.authHeader) {
            return;
        }
        this.providerRequestConfigs.set(providerName, {
            apiKey: config.apiKey,
            headers: config.headers,
            authHeader: config.authHeader,
        });
    }
    storeModelHeaders(providerName, modelId, headers) {
        const key = this.getModelRequestKey(providerName, modelId);
        if (!headers || Object.keys(headers).length === 0) {
            this.modelRequestHeaders.delete(key);
            return;
        }
        this.modelRequestHeaders.set(key, headers);
    }
    /**
     * Get API key and request headers for a model.
     */
    async getApiKeyAndHeaders(model) {
        try {
            const runtimeApiKey = this.authStorage.getRuntimeApiKey(model.provider);
            const extensionReplacesOAuth = this.registeredProviders.get(model.provider)?.oauth !== undefined
                || getLegacyOAuthProvider(model.provider) !== undefined;
            const resolvedProviderAuth = this.providerModels.getProvider(model.provider) && !extensionReplacesOAuth
                ? (await this.providerModels.getAuth(model, runtimeApiKey === undefined ? undefined : { apiKey: runtimeApiKey }))?.auth
                : undefined;
            const storedCredential = this.authStorage.get(model.provider);
            const storedOAuthAuth = runtimeApiKey !== undefined && !extensionReplacesOAuth && storedCredential?.type === "oauth"
                ? await oauthCredentialToAuth(model.provider, storedCredential)
                : undefined;
            const runtimeBaseUrl = runtimeApiKey !== undefined && model.provider === "github-copilot"
                ? copilotBaseUrlFromRuntimeToken(runtimeApiKey)
                : undefined;
            const providerAuth = runtimeApiKey === undefined
                ? resolvedProviderAuth
                : overlayRuntimeApiKey(runtimeApiKey, resolvedProviderAuth, storedOAuthAuth, runtimeBaseUrl);
            return getModelRequestAuth(model, this.authStorage, this.providerRequestConfigs, this.modelRequestHeaders, providerAuth);
        }
        catch (error) {
            return { ok: false, error: error instanceof Error ? error.message : String(error) };
        }
    }
    /**
     * Return auth status for a provider, including request auth configured in models.json.
     * This intentionally does not execute command-backed config values.
     */
    getProviderAuthStatus(provider) {
        return getProviderAuthStatusFromConfig(provider, this.authStorage, this.providerRequestConfigs);
    }
    /**
     * Get display name for a provider.
     */
    getProviderDisplayName(provider) {
        const registeredProvider = this.registeredProviders.get(provider);
        const oauthProvider = this.authStorage.getOAuthProviders().find((p) => p.id === provider);
        return (registeredProvider?.name ??
            registeredProvider?.oauth?.name ??
            oauthProvider?.name ??
            BUILT_IN_PROVIDER_DISPLAY_NAMES[provider] ??
            provider);
    }
    /**
     * Get API key for a provider.
     */
    async getApiKeyForProvider(provider) {
        return getApiKeyForProviderFromConfig(provider, this.authStorage, this.providerRequestConfigs);
    }
    /**
     * Check if a model is using OAuth credentials (subscription).
     */
    isUsingOAuth(model) {
        const cred = this.authStorage.get(model.provider);
        return cred?.type === "oauth";
    }
    /**
     * Register a provider dynamically (from extensions).
     */
    registerProvider(providerName, config) {
        const migratedConfig = migrateLegacyRegisterProviderConfigValues(providerName, config);
        validateProviderConfig(providerName, migratedConfig);
        const mergedConfig = this.upsertRegisteredProvider(providerName, migratedConfig);
        unregisterProviderRuntime(this.providerRegistrationSource(providerName));
        this.rebuildProviderModels();
        this.applyProviderConfig(providerName, mergedConfig, true);
    }
    /**
     * Check whether extensions have registered custom streamSimple dispatch for an API.
     */
    hasRegisteredStreamSimpleForApi(api) {
        for (const config of this.registeredProviders.values()) {
            if (config.api === api && config.streamSimple) {
                return true;
            }
        }
        return false;
    }
    /**
     * Unregister a previously registered provider.
     */
    unregisterProvider(providerName) {
        if (!this.registeredProviders.has(providerName))
            return;
        this.registeredProviders.delete(providerName);
        unregisterProviderRuntime(this.providerRegistrationSource(providerName));
        this.rebuildProviderModels();
    }
    upsertRegisteredProvider(providerName, config) {
        const definedConfig = Object.fromEntries(Object.entries(config).filter(([, value]) => value !== undefined));
        const merged = { ...this.registeredProviders.get(providerName), ...definedConfig };
        this.registeredProviders.set(providerName, merged);
        return merged;
    }
    applyProviderConfig(providerName, config, registerRuntime = false) {
        this.models = applyProviderConfigToModels({
            registrationSource: this.providerRegistrationSource(providerName),
            registerRuntime,
            providerName,
            config,
            models: this.models,
            modelOverrides: this.modelOverrides,
            authStorage: this.authStorage,
            storeProviderRequestConfig: (name, requestConfig) => this.storeProviderRequestConfig(name, requestConfig),
            storeModelHeaders: (name, modelId, headers) => this.storeModelHeaders(name, modelId, headers),
        });
    }
}
//# sourceMappingURL=model-registry.js.map