/**
 * AI Gateway (aigw) manager — handles model discovery, models.json generation,
 * and HTTP proxying for browser-side API access.
 *
 * When the user configures an aigw URL in preferences:
 * 1. Server fetches available models from the gateway's /v1/models endpoint
 * 2. Server writes/merges an "aigw" provider into ~/.bobbit/agent/models.json
 *    so agent subprocesses can use `set_model` with provider="aigw"
 * 3. Browser discovers models via server proxy (the aigw hostname may not
 *    resolve from the browser)
 *
 * When aigw is removed, the "aigw" provider is cleaned from models.json.
 */
import http from "node:http";
import https from "node:https";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { globalAgentDir } from "../bobbit-dir.js";
import { BOBBIT_AIGW_USER_AGENT, aigwUserAgentHeaders } from "./aigw-user-agent.js";
// modelRecencyRank() has moved to model-registry.ts
const DEFAULT_META = {
    contextWindow: 128_000,
    maxTokens: 16_384,
    reasoning: false,
    input: ["text"],
};
function zeroAigwCost() {
    return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
}
function normalizeCostValue(value) {
    if (!Number.isFinite(value) || value < 0)
        return 0;
    return Math.round(value * 1_000_000_000_000) / 1_000_000_000_000;
}
function normalizeAigwPricing(pricing) {
    if (!pricing || typeof pricing !== "object")
        return zeroAigwCost();
    const record = pricing;
    const prompt = record.prompt;
    const completion = record.completion;
    if (typeof prompt !== "number" ||
        typeof completion !== "number" ||
        !Number.isFinite(prompt) ||
        !Number.isFinite(completion) ||
        prompt < 0 ||
        completion < 0) {
        return zeroAigwCost();
    }
    return {
        input: normalizeCostValue(prompt * 1_000_000),
        output: normalizeCostValue(completion * 1_000_000),
        cacheRead: normalizeCostValue(prompt * 0.1 * 1_000_000),
        cacheWrite: normalizeCostValue(prompt * 1.25 * 1_000_000),
    };
}
/**
 * Infer model metadata from the model ID.
 * Patterns are matched greedily — first match wins.
 */
/**
 * Compat flags for the openai-completions provider in pi-ai.
 * These control which OpenAI API features are used in requests.
 * Gateway proxies often don't support the full OpenAI API surface,
 * so we disable features that cause errors.
 */
const GATEWAY_COMPAT = {
    supportsStore: false,
    supportsDeveloperRole: false,
    supportsUsageInStreaming: false,
    supportsReasoningEffort: false,
    supportsStrictMode: false,
    maxTokensField: "max_tokens",
};
const INFER_RULES = [
    // ── Anthropic Claude (most-specific size first) ─────────────────
    { test: /claude-opus/, meta: { contextWindow: 1_000_000, maxTokens: 32_768, reasoning: true, input: ["text", "image"] } },
    { test: /claude-sonnet/, meta: { contextWindow: 1_000_000, maxTokens: 16_384, reasoning: true, input: ["text", "image"] } },
    { test: /claude-haiku/, meta: { contextWindow: 200_000, maxTokens: 8_192, reasoning: false, input: ["text", "image"] } },
    { test: /claude/, meta: { contextWindow: 200_000, maxTokens: 16_384, reasoning: false, input: ["text", "image"] } },
    // ── OpenAI GPT-5.x (pro first so it doesn't match base variants) ─
    { test: /gpt-5\.5-pro/, meta: { contextWindow: 1_050_000, maxTokens: 128_000, reasoning: true, input: ["text", "image"] } },
    { test: /gpt-5\.5/, meta: { contextWindow: 272_000, maxTokens: 128_000, reasoning: true, input: ["text", "image"] } },
    { test: /gpt-5\.4-pro/, meta: { contextWindow: 1_050_000, maxTokens: 128_000, reasoning: true, input: ["text", "image"] } },
    // gpt-5.1-codex-max and gpt-5.2* / gpt-5.4* are reasoning models (and
    // xhigh-capable per src/shared/thinking-levels.ts). They must be classified
    // as reasoning so server-side clamping does not collapse xhigh to off for
    // aigw-routed users. Base gpt-5.4/5.5 currently advertise a 272k active
    // window in pi-ai; using the old speculative 1M here makes compaction look
    // far too early and can defer threshold compaction until provider overflow.
    { test: /gpt-5\.4/, meta: { contextWindow: 272_000, maxTokens: 128_000, reasoning: true, input: ["text", "image"] } },
    { test: /gpt-5\.2/, meta: { contextWindow: 400_000, maxTokens: 128_000, reasoning: true, input: ["text", "image"] } },
    { test: /gpt-5\.1-codex-max/, meta: { contextWindow: 400_000, maxTokens: 128_000, reasoning: true, input: ["text", "image"] } },
    { test: /gpt-5/, meta: { contextWindow: 400_000, maxTokens: 32_768, reasoning: false, input: ["text", "image"] } },
    // ── OpenAI o-series reasoning models (mini variants first) ──────
    { test: (id) => id.includes("o4-mini") || id.includes("o3-mini") || id.includes("o1-mini"), meta: { contextWindow: 200_000, maxTokens: 65_536, reasoning: true, input: ["text"] } },
    { test: (id) => id.includes("o4") || id.includes("o3") || id.includes("o1"), meta: { contextWindow: 200_000, maxTokens: 100_000, reasoning: true, input: ["text", "image"] } },
    // ── OpenAI GPT-4 (catch-all for 4o, 4.1, 4-turbo, …) ────────────
    { test: /gpt-4/, meta: { contextWindow: 128_000, maxTokens: 16_384, reasoning: false, input: ["text", "image"] } },
    // ── Alibaba Qwen ────────────────────────────────────────────────
    { test: /qwen/, meta: { contextWindow: 1_000_000, maxTokens: 32_768, reasoning: false, input: ["text"] } },
];
export function inferMeta(modelId) {
    const id = modelId.toLowerCase();
    for (const rule of INFER_RULES) {
        const matched = typeof rule.test === "function" ? rule.test(id) : rule.test.test(id);
        if (matched) {
            return { ...rule.meta, compat: GATEWAY_COMPAT };
        }
    }
    return { ...DEFAULT_META, compat: GATEWAY_COMPAT };
}
/**
 * Derive a short display name from a full gateway model ID.
 * e.g. "aws/us.anthropic.claude-sonnet-4-6" → "Claude Sonnet 4.6 (aws)"
 */
export function deriveName(modelId) {
    const parts = modelId.split("/");
    const prefix = parts.length > 1 ? parts[0] : undefined;
    const raw = parts[parts.length - 1];
    // Try to prettify common patterns
    let name = raw
        .replace(/^us\.anthropic\./, "")
        .replace(/^anthropic\./, "")
        .replace(/-v\d+:?\d*$/, "") // strip version suffixes like -v1:0
        .replace(/-(\d{8})$/, "") // strip date suffixes like -20250929
        .split("-")
        .map(s => s.charAt(0).toUpperCase() + s.slice(1))
        .join(" ");
    if (prefix && prefix !== name.toLowerCase()) {
        name += ` (${prefix})`;
    }
    return name;
}
// ── models.json management ─────────────────────────────────────────
function getModelsJsonPath() {
    return path.join(globalAgentDir(), "models.json");
}
function readModelsJson() {
    const p = getModelsJsonPath();
    try {
        if (fs.existsSync(p)) {
            return JSON.parse(fs.readFileSync(p, "utf-8"));
        }
    }
    catch (err) {
        console.error("[aigw-manager] Failed to read models.json:", err);
    }
    return { providers: {} };
}
function writeModelsJson(data) {
    const p = getModelsJsonPath();
    let tmp = "";
    try {
        const dir = path.dirname(p);
        if (!fs.existsSync(dir))
            fs.mkdirSync(dir, { recursive: true });
        tmp = `${p}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
        fs.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
        fs.renameSync(tmp, p);
        console.log(`[aigw-manager] Wrote models.json to ${p}`);
    }
    catch (err) {
        if (tmp) {
            try {
                fs.unlinkSync(tmp);
            }
            catch { /* best-effort */ }
        }
        console.error("[aigw-manager] Failed to write models.json:", err);
    }
}
/**
 * Parse model IDs from pi-ai's models.generated.js, grouped by provider.
 * Reads the file as text and extracts id+provider pairs via regex.
 */
function parseModelsGenerated() {
    const providerModels = new Map();
    try {
        const pkgUrl = import.meta.resolve("@earendil-works/pi-ai");
        const pkgDir = path.dirname(fileURLToPath(pkgUrl));
        const modelsPath = path.join(pkgDir, "models.generated.js");
        const text = fs.readFileSync(modelsPath, "utf-8");
        // The file has entries like:
        //   "some-model-id": {
        //       id: "some-model-id",
        //       ...
        //       provider: "amazon-bedrock",
        // We extract (id, provider) pairs.
        const entryRegex = /"([^"]+)":\s*\{[^}]*?provider:\s*"([^"]+)"/g;
        let match;
        while ((match = entryRegex.exec(text)) !== null) {
            const modelId = match[1];
            const provider = match[2];
            if (!providerModels.has(provider))
                providerModels.set(provider, []);
            providerModels.get(provider).push(modelId);
        }
    }
    catch (err) {
        console.error("[aigw-manager] Failed to parse models.generated.js:", err);
    }
    return providerModels;
}
/**
 * Write contextWindow overrides to models.json for all Claude models where
 * inferMeta() returns a larger context window than the built-in 200k.
 *
 * This fixes the 200k compaction bug: pi-ai hardcodes contextWindow: 200000
 * for all Claude models, but Sonnet/Opus actually support 1M tokens.
 * The modelOverrides in models.json tell pi-coding-agent to use the correct value.
 *
 * Preserves existing user modelOverrides — only sets contextWindow if the user
 * hasn't already overridden it for that model.
 */
export function writeContextWindowOverrides() {
    const providerModels = parseModelsGenerated();
    const targetProviders = ["amazon-bedrock", "anthropic"];
    const data = readModelsJson();
    if (!data.providers)
        data.providers = {};
    let overridesWritten = 0;
    for (const provider of targetProviders) {
        const modelIds = providerModels.get(provider) || [];
        const claudeIds = modelIds.filter(id => id.toLowerCase().includes("claude"));
        if (claudeIds.length === 0)
            continue;
        if (!data.providers[provider])
            data.providers[provider] = {};
        if (!data.providers[provider].modelOverrides)
            data.providers[provider].modelOverrides = {};
        const overrides = data.providers[provider].modelOverrides;
        for (const modelId of claudeIds) {
            const meta = inferMeta(modelId);
            if (meta.contextWindow > 200_000) {
                // Don't clobber existing user contextWindow override
                if (overrides[modelId]?.contextWindow !== undefined)
                    continue;
                if (!overrides[modelId])
                    overrides[modelId] = {};
                overrides[modelId].contextWindow = meta.contextWindow;
                overridesWritten++;
            }
        }
    }
    if (overridesWritten > 0) {
        writeModelsJson(data);
        console.log(`[aigw-manager] Wrote ${overridesWritten} contextWindow overrides to models.json`);
    }
    else {
        console.log("[aigw-manager] No contextWindow overrides needed");
    }
}
/**
 * Write aigw models into ~/.bobbit/agent/models.json, merging with existing
 * providers (preserving non-aigw entries).
 */
/**
 * Set env vars so agent subprocesses route Bedrock calls through the gateway.
 * Called both on fresh configuration and on startup when aigw is already configured.
 */
function setBedrockEnvVars(aigwUrl) {
    const bedrockBaseUrl = aigwUrl.replace(/\/+$/, "").replace(/\/v1$/, "") + "/aws";
    process.env.AWS_ENDPOINT_URL_BEDROCK_RUNTIME = bedrockBaseUrl;
    process.env.AWS_BEDROCK_FORCE_HTTP1 = "1";
    delete process.env.AWS_BEDROCK_SKIP_AUTH; // pi-ai would override creds with wrong dummy values
    process.env.AWS_ACCESS_KEY_ID = "anything";
    process.env.AWS_SECRET_ACCESS_KEY = "anything";
    if (!process.env.AWS_REGION)
        process.env.AWS_REGION = "us-east-1";
    console.log(`[aigw] Bedrock env configured: endpoint=${bedrockBaseUrl}`);
}
export function writeAigwModelsJson(aigwUrl, models) {
    const data = readModelsJson();
    if (!data.providers)
        data.providers = {};
    // AI gateways typically expose both OpenAI-compatible and Bedrock endpoints.
    // Route Claude models through the Bedrock Converse API (same path as Claude
    // Code) for full feature parity — native tool use, images, streaming.
    // Non-Claude models use OpenAI completions with conservative compat.
    const normalizedUrl = aigwUrl.replace(/\/+$/, "");
    // Bedrock Converse traffic goes to <gateway>/aws/model/<id>/converse-stream;
    // the provider's normalized baseUrl ends in /v1 for the OpenAI-compatible path
    // and is wrong for Bedrock. pi-ai uses `model.baseUrl` directly as the
    // `BedrockRuntimeClient` endpoint, so emit a per-model override on Claude
    // entries pointing at the /aws sub-tree. Mirrors the env var written by
    // setBedrockEnvVars() but survives across subprocess/env-strip boundaries.
    const bedrockBaseUrl = normalizedUrl.replace(/\/v1$/, "") + "/aws";
    const openaiCompat = {
        supportsDeveloperRole: false,
        supportsStore: false,
        supportsUsageInStreaming: false,
        supportsReasoningEffort: false,
        supportsStrictMode: false,
        maxTokensField: "max_tokens",
    };
    const isClaudeModel = (id) => id.toLowerCase().includes("claude");
    // Strip provider prefix for Bedrock (e.g. "aws/us.anthropic.claude-..." → "us.anthropic.claude-...")
    const bedrockModelId = (id) => {
        const slash = id.indexOf("/");
        return slash >= 0 ? id.slice(slash + 1) : id;
    };
    data.providers.aigw = {
        baseUrl: normalizedUrl,
        apiKey: "none",
        api: "openai-completions",
        // Provider-level header. pi-coding-agent's `resolveConfigValue` runs the
        // `!cmd` form via `child_process.exec` (shell-interpreted) and drops the
        // header entirely when stdout is empty — so when BOBBIT_SESSION_ID is
        // unset, no `x-opencode-session` header is sent (no fallback constant).
        // The literal here JSON-encodes to:
        //   "!node -e \"process.stdout.write(process.env.BOBBIT_SESSION_ID || '')\""
        headers: {
            "User-Agent": BOBBIT_AIGW_USER_AGENT,
            "x-opencode-session": `!node -e "process.stdout.write(process.env.BOBBIT_SESSION_ID || '')"`,
        },
        models: models.map(m => {
            const cost = m.cost ?? zeroAigwCost();
            if (isClaudeModel(m.id)) {
                return {
                    id: bedrockModelId(m.id),
                    name: m.name,
                    contextWindow: m.contextWindow,
                    maxTokens: m.maxTokens,
                    reasoning: m.reasoning,
                    input: m.input,
                    cost,
                    api: "bedrock-converse-stream",
                    // Per-model Bedrock endpoint override — provider baseUrl is the
                    // OpenAI-compatible /v1 root; Bedrock Converse lives under /aws.
                    baseUrl: bedrockBaseUrl,
                    ...(m.compat ? { compat: m.compat } : {}),
                };
            }
            return {
                id: m.id,
                name: m.name,
                contextWindow: m.contextWindow,
                maxTokens: m.maxTokens,
                reasoning: m.reasoning,
                input: m.input,
                cost,
                compat: { ...openaiCompat, ...(m.compat || {}) },
            };
        }),
    };
    setBedrockEnvVars(aigwUrl);
    writeModelsJson(data);
}
/**
 * Remove the "aigw" provider from models.json.
 */
export function removeAigwModelsJson() {
    const data = readModelsJson();
    if (data.providers?.aigw) {
        delete data.providers.aigw;
        writeModelsJson(data);
    }
}
// ── Startup internet check ─────────────────────────────────────────
/**
 * Apply `PI_OFFLINE=1` to the gateway process env when no internet was
 * detected at startup. Spawned pi-coding-agent subprocesses inherit
 * `process.env` (see `rpc-bridge.ts`) and pi 0.74.0+ honours this var by
 * skipping the GitHub fd/rg download path in `ensureTool()` — returning
 * `undefined` cleanly instead of timing out (~10s) on each first call.
 *
 * Rules:
 *   • If the user has already set `PI_OFFLINE` (any non-empty value), it is
 *     preserved verbatim — never overridden.
 *   • Otherwise, when `hasInternet === false`, set `PI_OFFLINE=1` and log a
 *     single explanatory line.
 *   • When `hasInternet === true`, do NOT set `PI_OFFLINE`. Leave existing
 *     state alone — don't introduce an unset that would change behaviour
 *     for online users who currently rely on pi's download fallback.
 *
 * Exported for unit testing. Idempotent.
 */
export function applyPiOfflineEnv(hasInternet) {
    const userValue = process.env.PI_OFFLINE;
    if (userValue !== undefined && userValue !== "") {
        // Respect any pre-existing user-supplied value.
        return;
    }
    if (hasInternet)
        return;
    process.env.PI_OFFLINE = "1";
    console.log("[pi-offline] Internet unavailable; setting PI_OFFLINE=1 — pi will skip GitHub fd/rg downloads. Use bundled binaries or pre-install fd/rg on PATH.");
}
/**
 * One-shot internet check at gateway startup. Tries HEAD requests to
 * well-known LLM API endpoints. Returns true if any responds.
 * Called once — not repeated after startup.
 */
export async function checkInternetAvailable() {
    const targets = [
        "https://api.anthropic.com",
        "https://api.openai.com",
    ];
    try {
        await Promise.any(targets.map((t) => httpHead(t, 4_000)));
        return true;
    }
    catch {
        return false;
    }
}
/**
 * Run once at gateway startup:
 * - If aigw is already configured, nothing to do.
 * - If not configured but internet is unavailable, try to auto-discover
 *   a gateway at a well-known local URL and configure it.
 *
 * Returns true if aigw is active after this call.
 */
export async function startupAigwCheck(prefs) {
    // Already configured — ensure env vars are set and models.json is up to date
    const existingUrl = getAigwUrl(prefs);
    if (existingUrl) {
        console.log("[aigw] AI Gateway already configured:", existingUrl);
        setBedrockEnvVars(existingUrl);
        // Users with a local aigw are typically offline; probe the public
        // internet once and wire PI_OFFLINE accordingly. The probe is short
        // (≤4s) and runs in parallel with no other startup work below.
        if (!process.env.BOBBIT_SKIP_AIGW_DISCOVERY) {
            try {
                const hasInternet = await checkInternetAvailable();
                applyPiOfflineEnv(hasInternet);
            }
            catch {
                applyPiOfflineEnv(false);
            }
        }
        if (process.env.BOBBIT_SKIP_AIGW_DISCOVERY) {
            console.log("[aigw] aigw configured, skipping startup re-discovery (BOBBIT_SKIP_AIGW_DISCOVERY)");
            return true;
        }
        try {
            const models = await discoverAigwModels(existingUrl);
            writeAigwModelsJson(existingUrl, models);
            console.log(`[aigw] re-discovered ${models.length} models on startup, refreshed models.json`);
        }
        catch (err) {
            const msg = err?.message || String(err);
            console.warn(`[aigw] gateway unreachable on startup (${msg}), keeping existing models.json`);
        }
        return true;
    }
    // Skip network probing + local-gateway auto-discovery when tests/CI opt out.
    // Tests that exercise the /api/aigw/* endpoints configure the gateway
    // explicitly and don't rely on the startup probe.
    if (process.env.BOBBIT_SKIP_AIGW_DISCOVERY)
        return false;
    // Check internet
    const hasInternet = await checkInternetAvailable();
    applyPiOfflineEnv(hasInternet);
    if (hasInternet) {
        console.log("[aigw] Internet available — using standard providers");
        return false;
    }
    console.log("[aigw] No internet detected — probing for local AI Gateway...");
    // Build candidate list from environment, then fall back to localhost
    const candidates = [];
    const anthropicBase = process.env.ANTHROPIC_BASE_URL;
    if (anthropicBase) {
        const base = anthropicBase.replace(/\/+$/, "");
        candidates.push(base.endsWith("/v1") ? base : `${base}/v1`);
    }
    const openaiBase = process.env.OPENAI_BASE_URL;
    if (openaiBase) {
        candidates.push(openaiBase.replace(/\/+$/, ""));
    }
    candidates.push("http://localhost:1111/v1", "http://127.0.0.1:1111/v1");
    for (const url of candidates) {
        try {
            const models = await discoverAigwModels(url);
            if (models.length > 0) {
                console.log(`[aigw] Found gateway at ${url} with ${models.length} models — auto-configuring`);
                await configureAigw(url, prefs);
                return true;
            }
        }
        catch {
            // try next
        }
    }
    console.log("[aigw] No gateway found at well-known URLs");
    return false;
}
// ── HTTP helpers ───────────────────────────────────────────────────
/**
 * Simple HTTP HEAD — resolves on any response, rejects on network error / timeout.
 */
function httpHead(url, timeoutMs = 4_000) {
    return new Promise((resolve, reject) => {
        const parsedUrl = new URL(url);
        const transport = parsedUrl.protocol === "https:" ? https : http;
        const req = transport.request(parsedUrl, { method: "HEAD", timeout: timeoutMs }, () => resolve());
        req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
        req.on("error", reject);
        req.end();
    });
}
/**
 * Simple HTTP GET that returns a parsed JSON body.
 * Works with both http:// and https:// URLs.
 */
function httpGet(url, timeoutMs = 10_000) {
    return new Promise((resolve, reject) => {
        const parsedUrl = new URL(url);
        const transport = parsedUrl.protocol === "https:" ? https : http;
        const req = transport.request(parsedUrl, { method: "GET", headers: aigwUserAgentHeaders(), timeout: timeoutMs }, (res) => {
            const chunks = [];
            res.on("data", (c) => chunks.push(c));
            res.on("end", () => {
                const body = Buffer.concat(chunks).toString("utf-8");
                if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
                    try {
                        resolve(JSON.parse(body));
                    }
                    catch {
                        reject(new Error(`Invalid JSON from ${url}`));
                    }
                }
                else {
                    reject(new Error(`HTTP ${res.statusCode} from ${url}: ${body.slice(0, 200)}`));
                }
            });
        });
        req.on("timeout", () => { req.destroy(); reject(new Error(`Timeout fetching ${url}`)); });
        req.on("error", reject);
        req.end();
    });
}
/**
 * Proxy an HTTP request: reads the incoming request body, forwards to the
 * target URL, and pipes the response back.
 */
export function proxyRequest(targetUrl, incomingReq, outgoingRes) {
    const parsed = new URL(targetUrl);
    const transport = parsed.protocol === "https:" ? https : http;
    const chunks = [];
    incomingReq.on("data", (c) => chunks.push(c));
    incomingReq.on("end", () => {
        const body = Buffer.concat(chunks);
        const headers = aigwUserAgentHeaders({
            "Content-Type": "application/json",
            ...(body.length > 0 ? { "Content-Length": String(body.length) } : {}),
        });
        const RESPONSE_TIMEOUT_MS = 120_000;
        let responseTimer;
        let completed = false;
        const cleanup = () => {
            if (responseTimer) {
                clearTimeout(responseTimer);
                responseTimer = undefined;
            }
            completed = true;
        };
        const proxyReq = transport.request(parsed, {
            method: incomingReq.method || "GET",
            headers,
            timeout: RESPONSE_TIMEOUT_MS,
        }, (proxyRes) => {
            outgoingRes.writeHead(proxyRes.statusCode || 502, proxyRes.headers);
            proxyRes.pipe(outgoingRes);
            proxyRes.on("end", cleanup);
            proxyRes.on("error", cleanup);
        });
        responseTimer = setTimeout(() => {
            if (!completed) {
                console.error(`[aigw-proxy] Response timeout after ${RESPONSE_TIMEOUT_MS}ms proxying to ${targetUrl}`);
                proxyReq.destroy();
                if (!outgoingRes.headersSent) {
                    outgoingRes.writeHead(504, { "Content-Type": "application/json" });
                }
                outgoingRes.end(JSON.stringify({ error: "Gateway timeout: response not completed within 120s" }));
                completed = true;
            }
        }, RESPONSE_TIMEOUT_MS);
        proxyReq.on("error", (err) => {
            cleanup();
            console.error(`[aigw-proxy] Error proxying to ${targetUrl}:`, err.message);
            if (!outgoingRes.headersSent) {
                outgoingRes.writeHead(502, { "Content-Type": "application/json" });
            }
            outgoingRes.end(JSON.stringify({ error: `Gateway proxy error: ${err.message}` }));
        });
        if (body.length > 0)
            proxyReq.write(body);
        proxyReq.end();
    });
}
// ── Public API ─────────────────────────────────────────────────────
/**
 * Fetch the model list from an aigw endpoint and return structured model info.
 * Hits GET {baseUrl}/v1/models (or {baseUrl}/models if baseUrl already ends with /v1).
 */
export async function discoverAigwModels(baseUrl) {
    const url = baseUrl.replace(/\/+$/, "");
    const modelsUrl = url.endsWith("/v1") ? `${url}/models` : `${url}/v1/models`;
    const data = await httpGet(modelsUrl);
    if (!data?.data || !Array.isArray(data.data)) {
        throw new Error("Unexpected response format from /v1/models — expected { data: [...] }");
    }
    return data.data.map((m) => {
        const meta = inferMeta(m.id);
        // Honour fields if the gateway provides them
        const ctxFromGw = m.context_length || m.context_window;
        const maxTokFromGw = m.max_tokens || m.max_completion_tokens;
        return {
            id: m.id,
            name: deriveName(m.id),
            api: "openai-completions",
            reasoning: meta.reasoning,
            input: meta.input,
            contextWindow: Math.max(ctxFromGw || 0, meta.contextWindow),
            maxTokens: Math.max(maxTokFromGw || 0, meta.maxTokens),
            cost: normalizeAigwPricing(m.pricing),
            ...(meta.compat ? { compat: meta.compat } : {}),
        };
    });
}
/**
 * Full configure flow: discover models, persist preference, write models.json.
 * Returns the discovered models.
 */
export async function configureAigw(baseUrl, prefs) {
    const rawModels = await discoverAigwModels(baseUrl);
    const normalizedUrl = baseUrl.replace(/\/+$/, "");
    // Normalize model IDs: Claude models get the provider prefix stripped
    // (e.g. "aws/us.anthropic.claude-..." → "us.anthropic.claude-...") because
    // they use the Bedrock API where the ID is just the Bedrock model ARN.
    const isClaudeModel = (id) => id.toLowerCase().includes("claude");
    const stripPrefix = (id) => { const i = id.indexOf("/"); return i >= 0 ? id.slice(i + 1) : id; };
    const models = rawModels.map(m => isClaudeModel(m.id)
        ? { ...m, id: stripPrefix(m.id), api: "bedrock-converse-stream" }
        : m);
    prefs.set("aigw.url", normalizedUrl);
    // Note: aigw.models no longer cached in preferences — model-registry discovers fresh each time
    writeAigwModelsJson(normalizedUrl, models);
    return models;
}
/**
 * Remove aigw configuration.
 */
export function removeAigw(prefs) {
    prefs.remove("aigw.url");
    prefs.remove("aigw.models");
    removeAigwModelsJson();
}
/**
 * Get the currently configured aigw URL (if any).
 */
export function getAigwUrl(prefs) {
    return prefs.get("aigw.url");
}
// getAigwModels() has been removed — model-registry discovers fresh each time
//# sourceMappingURL=aigw-manager.js.map