/** GitHub Copilot CAPI model catalog parsing, active state, and disk cache. */
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
/** Safety fallback when a model reports neither `max_prompt_tokens` nor `max_context_window_tokens`. */
export const COPILOT_CONTEXT_WINDOW_FALLBACK = 128_000;
export const COPILOT_CATALOG_API_VERSION = "2026-06-01";
/**
 * Headers GitHub's CAPI expects for catalog reads. Mirrors the editor headers pi-ai already sends
 * for Copilot token refresh and model-policy calls, plus the dated API version.
 */
export const COPILOT_CATALOG_HEADERS = {
    "User-Agent": "GitHubCopilotChat/0.35.0",
    "Editor-Version": "vscode/1.107.0",
    "Editor-Plugin-Version": "copilot-chat/0.35.0",
    "Copilot-Integration-Id": "vscode-chat",
    "X-GitHub-Api-Version": COPILOT_CATALOG_API_VERSION,
};
/** Default Copilot CAPI base URL for OAuth tokens when no route can be resolved. */
export const DEFAULT_COPILOT_API_BASE_URL = "https://api.individual.githubcopilot.com";
/** GitHub Copilot public routing hub for PAT-based `COPILOT_GITHUB_TOKEN` auth. */
export const COPILOT_PUBLIC_API_BASE_URL = "https://api.githubcopilot.com";
/** Enterprise Copilot CAPI host for GHES/non-github.com server URLs. */
export const COPILOT_ENTERPRISE_API_BASE_URL = "https://api.enterprise.githubcopilot.com";
const GHE_COPILOT_API_HOST_PREFIX = ["copilot", "api"].join("-");
export const COPILOT_GITHUB_TOKEN_ENV = "COPILOT_GITHUB_TOKEN";
export const COPILOT_API_TARGET_ENV = "COPILOT_API_TARGET";
export const GITHUB_COPILOT_BASE_URL_ENV = "GITHUB_COPILOT_BASE_URL";
export const GITHUB_SERVER_URL_ENV = "GITHUB_SERVER_URL";
function normalizeCopilotApiBaseUrl(value) {
    const trimmed = value?.trim();
    if (!trimmed)
        return undefined;
    const withScheme = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
    try {
        new URL(withScheme);
    }
    catch {
        return undefined;
    }
    return withScheme.replace(/\/+$/, "");
}
function explicitCopilotApiBaseUrlFromEnvironment(env) {
    return normalizeCopilotApiBaseUrl(env[COPILOT_API_TARGET_ENV]) ?? normalizeCopilotApiBaseUrl(env[GITHUB_COPILOT_BASE_URL_ENV]);
}
function copilotApiBaseUrlFromServerUrl(serverUrl) {
    const trimmed = serverUrl?.trim();
    if (!trimmed)
        return undefined;
    try {
        const parsed = new URL(trimmed.includes("://") ? trimmed : `https://${trimmed}`);
        const host = parsed.hostname.toLowerCase();
        if (!host || host === "github.com")
            return undefined;
        if (host.endsWith(".ghe.com"))
            return `https://${GHE_COPILOT_API_HOST_PREFIX}.${host}`;
        return COPILOT_ENTERPRISE_API_BASE_URL;
    }
    catch {
        return undefined;
    }
}
function isCopilotEnvironmentToken(token, env) {
    const environmentToken = copilotTokenFromEnvironment(env);
    return environmentToken !== undefined && token === environmentToken;
}
export function copilotTokenFromEnvironment(env = process.env) {
    const token = env[COPILOT_GITHUB_TOKEN_ENV]?.trim();
    return token ? token : undefined;
}
/** Disk-cache freshness window, matching the Copilot CLI's list-models cache TTL. */
export const COPILOT_CATALOG_CACHE_TTL_MS = 30 * 60 * 1000;
/** Current on-disk cache schema version. */
export const COPILOT_CATALOG_CACHE_VERSION = 5;
export function copilotApiBaseUrlFromToken(token, enterpriseDomain, env = process.env) {
    const explicitOverride = explicitCopilotApiBaseUrlFromEnvironment(env);
    if (explicitOverride)
        return explicitOverride;
    if (token) {
        const match = token.match(/proxy-ep=([^;]+)/);
        if (match) {
            return `https://${match[1].replace(/^proxy\./, "api.")}`;
        }
    }
    if (enterpriseDomain)
        return `https://${GHE_COPILOT_API_HOST_PREFIX}.${enterpriseDomain}`;
    const serverUrlOverride = copilotApiBaseUrlFromServerUrl(env[GITHUB_SERVER_URL_ENV]);
    if (serverUrlOverride)
        return serverUrlOverride;
    if (isCopilotEnvironmentToken(token, env))
        return COPILOT_PUBLIC_API_BASE_URL;
    return DEFAULT_COPILOT_API_BASE_URL;
}
function trimTrailingSlash(url) {
    return url.replace(/\/+$/, "");
}
function asRecord(value) {
    return value && typeof value === "object" ? value : undefined;
}
function toPositiveInt(value) {
    return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
}
function stringArray(value) {
    if (!Array.isArray(value))
        return undefined;
    const strings = value.filter((item) => typeof item === "string" && item.length > 0);
    return strings.length > 0 ? strings : undefined;
}
function supportedFlag(record, key) {
    const value = record?.[key];
    if (typeof value === "boolean")
        return value;
    if (typeof value === "number")
        return Number.isFinite(value) && value > 0;
    if (Array.isArray(value))
        return value.length > 0;
    return undefined;
}
function parseCopilotSupports(value) {
    const record = asRecord(value);
    if (!record)
        return undefined;
    const supports = {
        adaptiveThinking: supportedFlag(record, "adaptive_thinking"),
        maxThinkingBudget: supportedFlag(record, "max_thinking_budget"),
        minThinkingBudget: supportedFlag(record, "min_thinking_budget"),
        parallelToolCalls: supportedFlag(record, "parallel_tool_calls"),
        reasoningEffort: supportedFlag(record, "reasoning_effort"),
        reasoningEffortLevels: stringArray(record.reasoning_effort),
        streaming: supportedFlag(record, "streaming"),
        structuredOutputs: supportedFlag(record, "structured_outputs"),
        toolCalls: supportedFlag(record, "tool_calls"),
        vision: supportedFlag(record, "vision"),
    };
    return Object.values(supports).some((flag) => flag !== undefined) ? supports : undefined;
}
function parseCopilotLimits(limits, prices) {
    return {
        maxPromptTokens: toPositiveInt(limits?.max_prompt_tokens),
        maxContextWindowTokens: toPositiveInt(limits?.max_context_window_tokens),
        maxOutputTokens: toPositiveInt(limits?.max_output_tokens),
        defaultContextMax: toPositiveInt(asRecord(prices?.default)?.context_max),
        longContextMax: toPositiveInt(asRecord(prices?.long_context)?.context_max),
    };
}
export function resolveCopilotModelContext(limits) {
    const hasSignal = limits.maxPromptTokens !== undefined ||
        limits.maxContextWindowTokens !== undefined ||
        limits.defaultContextMax !== undefined ||
        limits.longContextMax !== undefined;
    if (!hasSignal)
        return undefined;
    const maxInput = limits.maxPromptTokens ?? limits.maxContextWindowTokens ?? COPILOT_CONTEXT_WINDOW_FALLBACK;
    const base = limits.defaultContextMax ?? maxInput;
    if (limits.longContextMax !== undefined && limits.longContextMax > base) {
        // Display the model's full context window as the long tier (matching openai/* and anthropic/*)
        // when CAPI advertises it; otherwise fall back to the long-context prompt threshold for
        // older/sparse payloads.
        const longWindow = limits.maxContextWindowTokens ?? limits.longContextMax;
        // The hard prompt/input cap GitHub enforces server-side: prefer max_prompt_tokens, else derive
        // it from total − output reserve, else fall back to the long-context prompt threshold.
        const derivedInputCap = limits.maxContextWindowTokens !== undefined && limits.maxOutputTokens !== undefined
            ? limits.maxContextWindowTokens - limits.maxOutputTokens
            : undefined;
        const inputCap = limits.maxPromptTokens ??
            (derivedInputCap !== undefined && derivedInputCap > 0 ? derivedInputCap : undefined) ??
            limits.longContextMax;
        // Only carry the cap when the displayed long window actually exceeds it (the branded-total
        // case); when they coincide there is no gap and the input budget is just the window.
        const resolved = longWindow > inputCap
            ? { contextWindow: base, contextWindowOptions: [base, longWindow], maxInputTokens: inputCap }
            : { contextWindow: base, contextWindowOptions: [base, longWindow] };
        return limits.maxOutputTokens !== undefined ? { ...resolved, maxTokens: limits.maxOutputTokens } : resolved;
    }
    return limits.maxOutputTokens !== undefined ? { contextWindow: base, maxTokens: limits.maxOutputTokens } : { contextWindow: base };
}
/**
 * Parse a raw CAPI `/models` response body into an input-token context-window catalog.
 */
export function parseCopilotModelCatalog(body) {
    const catalog = new Map();
    const data = asRecord(body)?.data;
    if (!Array.isArray(data))
        return catalog;
    for (const entry of data) {
        const record = asRecord(entry);
        if (!record)
            continue;
        const id = record.id;
        if (typeof id !== "string" || id.length === 0)
            continue;
        const capabilities = asRecord(record.capabilities);
        const limitsRecord = asRecord(capabilities?.limits);
        const prices = asRecord(asRecord(record.billing)?.token_prices);
        const limits = parseCopilotLimits(limitsRecord, prices);
        const context = resolveCopilotModelContext(limits);
        if (context) {
            const policy = asRecord(record.policy);
            catalog.set(id, {
                ...context,
                displayName: typeof record.name === "string" && record.name.length > 0 ? record.name : undefined,
                vendor: typeof record.vendor === "string" && record.vendor.length > 0 ? record.vendor : undefined,
                supportedEndpoints: stringArray(record.supported_endpoints),
                supports: parseCopilotSupports(capabilities?.supports),
                limits,
                modelPickerEnabled: typeof record.model_picker_enabled === "boolean" ? record.model_picker_enabled : undefined,
                policyState: typeof policy?.state === "string" ? policy.state : undefined,
                type: typeof capabilities?.type === "string" ? capabilities.type : undefined,
            });
        }
    }
    return catalog;
}
/** Fetch and parse the live Copilot model catalog from CAPI `GET {baseUrl}/models`. */
export async function fetchCopilotModelCatalog(options) {
    const fetchImpl = options.fetchImpl ?? fetch;
    const baseUrl = options.baseUrl ?? copilotApiBaseUrlFromToken(options.token, options.enterpriseDomain);
    const response = await fetchImpl(`${trimTrailingSlash(baseUrl)}/models`, {
        method: "GET",
        headers: {
            Accept: "application/json",
            Authorization: `Bearer ${options.token}`,
            ...COPILOT_CATALOG_HEADERS,
            ...options.headers,
        },
        ...(options.signal ? { signal: options.signal } : {}),
    });
    if (!response.ok) {
        throw new Error(`GitHub Copilot /models request failed: ${response.status} ${response.statusText}`);
    }
    return parseCopilotModelCatalog(await response.json());
}
// ----------------------------------------------------------------------------
// Active in-memory catalog (consulted by the model registry).
//
// Empty by default, so with no Copilot auth / no successful fetch the registry leaves Copilot
// model context windows untouched and the picker never appears.
// ----------------------------------------------------------------------------
let activeCatalog = new Map();
/** Replace the active catalog the registry derives context windows from. */
export function setActiveCopilotModelCatalog(catalog) {
    activeCatalog = catalog;
}
/** The active catalog (empty until a successful auth-gated fetch/cache load). */
export function getActiveCopilotModelCatalog() {
    return activeCatalog;
}
/** Reset the active catalog (primarily for tests). */
export function clearActiveCopilotModelCatalog() {
    activeCatalog = new Map();
}
function hostFromBaseUrl(baseUrl) {
    try {
        return new URL(baseUrl).host;
    }
    catch {
        return baseUrl;
    }
}
function sanitizeCachedSupports(value) {
    const record = asRecord(value);
    if (!record)
        return undefined;
    const supports = {
        adaptiveThinking: supportedFlag(record, "adaptiveThinking"),
        maxThinkingBudget: supportedFlag(record, "maxThinkingBudget"),
        minThinkingBudget: supportedFlag(record, "minThinkingBudget"),
        parallelToolCalls: supportedFlag(record, "parallelToolCalls"),
        reasoningEffort: supportedFlag(record, "reasoningEffort"),
        reasoningEffortLevels: stringArray(record.reasoningEffortLevels),
        streaming: supportedFlag(record, "streaming"),
        structuredOutputs: supportedFlag(record, "structuredOutputs"),
        toolCalls: supportedFlag(record, "toolCalls"),
        vision: supportedFlag(record, "vision"),
    };
    return Object.values(supports).some((flag) => flag !== undefined) ? supports : undefined;
}
function sanitizeCachedLimits(value) {
    const record = asRecord(value);
    if (!record)
        return undefined;
    const limits = {
        maxPromptTokens: toPositiveInt(record.maxPromptTokens),
        maxContextWindowTokens: toPositiveInt(record.maxContextWindowTokens),
        maxOutputTokens: toPositiveInt(record.maxOutputTokens),
        defaultContextMax: toPositiveInt(record.defaultContextMax),
        longContextMax: toPositiveInt(record.longContextMax),
    };
    return Object.values(limits).some((limit) => limit !== undefined) ? limits : undefined;
}
function sanitizeCachedContext(value) {
    const record = asRecord(value);
    const contextWindow = toPositiveInt(record?.contextWindow);
    if (contextWindow === undefined)
        return undefined;
    const maxInputTokens = toPositiveInt(record?.maxInputTokens);
    const maxTokens = toPositiveInt(record?.maxTokens);
    const rawOptions = record?.contextWindowOptions;
    const base = maxInputTokens !== undefined ? { contextWindow, maxInputTokens } : { contextWindow };
    if (maxTokens !== undefined)
        base.maxTokens = maxTokens;
    if (Array.isArray(rawOptions)) {
        const options = rawOptions.map(toPositiveInt).filter((n) => n !== undefined);
        if (options.length > 1)
            base.contextWindowOptions = options;
    }
    const displayName = record && typeof record.displayName === "string" && record.displayName.length > 0 ? record.displayName : undefined;
    const vendor = record && typeof record.vendor === "string" && record.vendor.length > 0 ? record.vendor : undefined;
    const policyState = record && typeof record.policyState === "string" ? record.policyState : undefined;
    const type = record && typeof record.type === "string" ? record.type : undefined;
    return {
        ...base,
        displayName,
        vendor,
        supportedEndpoints: stringArray(record?.supportedEndpoints),
        supports: sanitizeCachedSupports(record?.supports),
        limits: sanitizeCachedLimits(record?.limits),
        modelPickerEnabled: typeof record?.modelPickerEnabled === "boolean" ? record.modelPickerEnabled : undefined,
        policyState,
        type,
    };
}
export function readCopilotCatalogCache(path, options) {
    let parsed;
    try {
        if (!existsSync(path))
            return undefined;
        parsed = JSON.parse(readFileSync(path, "utf8"));
    }
    catch {
        return undefined;
    }
    if (!parsed || parsed.version !== COPILOT_CATALOG_CACHE_VERSION)
        return undefined;
    if (parsed.host !== options.host)
        return undefined;
    const now = options.now ?? Date.now();
    const ttlMs = options.ttlMs ?? COPILOT_CATALOG_CACHE_TTL_MS;
    if (typeof parsed.fetchedAt !== "number" || now - parsed.fetchedAt >= ttlMs)
        return undefined;
    const models = asRecord(parsed.models);
    if (!models)
        return undefined;
    const catalog = new Map();
    for (const [id, value] of Object.entries(models)) {
        const context = sanitizeCachedContext(value);
        if (context)
            catalog.set(id, context);
    }
    return catalog;
}
export function writeCopilotCatalogCache(path, baseUrl, catalog, now) {
    const payload = {
        version: COPILOT_CATALOG_CACHE_VERSION,
        host: hostFromBaseUrl(baseUrl),
        fetchedAt: now ?? Date.now(),
        models: Object.fromEntries(catalog),
    };
    try {
        mkdirSync(dirname(path), { recursive: true });
        writeFileSync(path, JSON.stringify(payload), "utf8");
    }
    catch {
        // best-effort cache; ignore write failures
    }
}
export function copilotCatalogCacheHost(baseUrl) {
    return hostFromBaseUrl(baseUrl);
}
export function copilotCatalogCachePath(agentDir) {
    return join(agentDir, "cache", "copilot-models.json");
}
/**
 * Seed the active catalog synchronously from the on-disk cache, gated on a Copilot access token.
 *
 * Called at model-registry construction so a returning user's previously selected long-context
 * window is recognized before startup validation runs — otherwise the persisted choice would warn
 * ("context window 936k is not supported…") and reset until the async refresh completes. The cache
 * TTL is intentionally ignored here: stale-but-present windows are still valid for selection, and
 * the async loader independently refetches on its own freshness window. Returns true when a catalog
 * was applied. No-op (returns false) without a token or a host-matching cached catalog.
 */
export function seedActiveCopilotModelCatalogFromCache(accessToken, cachePath, now) {
    if (typeof accessToken !== "string" || accessToken.length === 0)
        return false;
    const host = copilotCatalogCacheHost(copilotApiBaseUrlFromToken(accessToken));
    const cached = readCopilotCatalogCache(cachePath, { host, now, ttlMs: Number.POSITIVE_INFINITY });
    if (!cached)
        return false;
    setActiveCopilotModelCatalog(cached);
    return true;
}
//# sourceMappingURL=copilot-model-catalog.js.map