/** * Codex model discovery — probes backend endpoints for available models. */ import { getConfig } from "../config.js"; import { getTransport } from "../tls/transport.js"; import type { BackendModelEntry } from "../models/model-store.js"; let _firstModelFetchLogged = false; export async function fetchModels( headers: Record, proxyUrl?: string | null, ): Promise { const config = getConfig(); const transport = getTransport(); const baseUrl = config.api.base_url; const clientVersion = config.client.app_version; const endpoints = [ `${baseUrl}/codex/models?client_version=${clientVersion}`, `${baseUrl}/models`, `${baseUrl}/sentinel/chat-requirements`, ]; headers["Accept"] = "application/json"; if (!transport.isImpersonate()) { headers["Accept-Encoding"] = "gzip, deflate"; } for (const url of endpoints) { try { const result = await transport.get(url, headers, 15, proxyUrl); const parsed = JSON.parse(result.body) as Record; const sentinel = parsed.chat_models as Record | undefined; const models = sentinel?.models ?? parsed.models ?? parsed.data ?? parsed.categories; if (Array.isArray(models) && models.length > 0) { console.log(`[CodexApi] getModels() found ${models.length} entries from ${url}`); if (!_firstModelFetchLogged) { console.log(`[CodexApi] Raw response keys: ${Object.keys(parsed).join(", ")}`); console.log(`[CodexApi] Raw model sample: ${JSON.stringify(models[0]).slice(0, 500)}`); if (models.length > 1) { console.log(`[CodexApi] Raw model sample[1]: ${JSON.stringify(models[1]).slice(0, 500)}`); } _firstModelFetchLogged = true; } // Flatten nested categories into a single list const flattened: BackendModelEntry[] = []; for (const item of models) { if (item && typeof item === "object") { const entry = item as Record; if (Array.isArray(entry.models)) { for (const sub of entry.models as BackendModelEntry[]) { flattened.push(sub); } } else { flattened.push(item as BackendModelEntry); } } } if (flattened.length > 0) { console.log(`[CodexApi] getModels() total after flatten: ${flattened.length} models`); return flattened; } } } catch (err) { const msg = err instanceof Error ? err.message : String(err); console.log(`[CodexApi] Probe ${url} failed: ${msg}`); continue; } } return null; } export async function probeEndpoint( path: string, headers: Record, proxyUrl?: string | null, ): Promise | null> { const config = getConfig(); const transport = getTransport(); const url = `${config.api.base_url}${path}`; headers["Accept"] = "application/json"; if (!transport.isImpersonate()) { headers["Accept-Encoding"] = "gzip, deflate"; } try { const result = await transport.get(url, headers, 15, proxyUrl); return JSON.parse(result.body) as Record; } catch { return null; } }