import * as z from "zod/v4"; import { resolveCodexGpt56DiscoveryContext } from "../../context-cap-policy"; import { CODEX_BASE_URL, OPENAI_HEADER_VALUES, OPENAI_HEADERS } from "../../providers/openai-codex/constants"; import type { Model } from "../../types"; import { isRecord } from "../../utils"; const DEFAULT_MODEL_LIST_PATHS = ["/codex/models", "/models"] as const; const DEFAULT_MAX_TOKENS = 128_000; const DEFAULT_CODEX_CLIENT_VERSION = "0.99.0"; const NPM_CODEX_LATEST_URL = "https://registry.npmjs.org/@openai%2Fcodex/latest"; const codexReasoningPresetSchema = z .object({ effort: z.unknown().optional(), }) .loose(); const codexModelEntrySchema = z .object({ slug: z.unknown().optional(), id: z.unknown().optional(), display_name: z.unknown().optional(), context_window: z.unknown().optional(), default_reasoning_level: z.unknown().optional(), supported_reasoning_levels: z.unknown().optional(), input_modalities: z.unknown().optional(), supported_in_api: z.unknown().optional(), priority: z.unknown().optional(), prefer_websockets: z.unknown().optional(), }) .loose(); const codexModelsResponseSchema = z .object({ models: z.array(z.unknown()).optional(), data: z.array(z.unknown()).optional(), }) .loose(); type CodexModelEntry = z.infer; interface NormalizedCodexModel { model: Model<"openai-codex-responses">; priority: number; } /** * Fetch options for OpenAI code provider model discovery. */ export interface CodexModelDiscoveryOptions { /** OAuth access token used for `Authorization: Bearer ...`. */ accessToken: string; /** ChatGPT account id value used for `chatgpt-account-id` header. */ accountId?: string; /** Base URL for OpenAI code backend backend. Defaults to `https://chatgpt.com/backend-api`. */ baseUrl?: string; /** Optional client version attached as `client_version` query parameter. */ clientVersion?: string; /** Optional endpoint path candidates. Defaults to `/OpenAI code backend/models`, then `/models`. */ paths?: readonly string[]; /** Additional headers merged on top of required OpenAI code backend headers. */ headers?: Record; /** Abort signal for network request cancellation. */ signal?: AbortSignal; /** Optional fetch implementation override for tests. */ fetchFn?: typeof fetch; /** Optional registry fetch implementation override for client version lookup. */ registryFetchFn?: typeof fetch; } /** * Normalized OpenAI code backend discovery response. */ export interface CodexModelDiscoveryResult { models: Model<"openai-codex-responses">[]; etag?: string; } /** * Fetches model metadata from OpenAI code backend backend and normalizes it for pi model management. * * Returns `null` when no supported model-list route can be fetched/parsed. * Returns `{ models: [] }` when a route succeeds but yields no usable models. */ export async function fetchCodexModels(options: CodexModelDiscoveryOptions): Promise { const fetchFn = options.fetchFn ?? fetch; const baseUrl = normalizeBaseUrl(options.baseUrl); const paths = normalizePaths(options.paths); const headers = buildCodexHeaders(options); const clientVersion = await resolveCodexClientVersion( options.clientVersion, options.registryFetchFn ?? fetchFn, options.signal, ); let sawSuccessfulResponse = false; for (const path of paths) { const requestUrl = buildModelsUrl(baseUrl, path, clientVersion); let response: Response; try { response = await fetchFn(requestUrl, { method: "GET", headers, signal: options.signal, }); } catch { continue; } if (!response.ok) { continue; } let payload: unknown; try { payload = await response.json(); } catch { continue; } const models = normalizeCodexModels(payload, baseUrl); if (models === null) { continue; } sawSuccessfulResponse = true; const etag = getResponseEtag(response.headers); return etag ? { models, etag } : { models }; } return sawSuccessfulResponse ? { models: [] } : null; } function normalizeBaseUrl(baseUrl: string | undefined): string { const raw = (baseUrl ?? CODEX_BASE_URL).trim(); if (!raw) { return CODEX_BASE_URL; } return raw.replace(/\/+$/, ""); } function normalizePaths(paths: readonly string[] | undefined): string[] { if (!paths || paths.length === 0) { return [...DEFAULT_MODEL_LIST_PATHS]; } const normalized = paths .map(path => path.trim()) .filter(path => path.length > 0) .map(path => (path.startsWith("/") ? path : `/${path}`)); return normalized.length > 0 ? normalized : [...DEFAULT_MODEL_LIST_PATHS]; } function buildModelsUrl(baseUrl: string, path: string, clientVersion: string | undefined): string { const url = new URL(`${baseUrl}${path}`); if (clientVersion && clientVersion.trim().length > 0) { url.searchParams.set("client_version", clientVersion.trim()); } return url.toString(); } function buildCodexHeaders(options: CodexModelDiscoveryOptions): Headers { const headers = new Headers(options.headers); headers.set("Authorization", `Bearer ${options.accessToken}`); if (options.accountId && options.accountId.trim().length > 0) { headers.set(OPENAI_HEADERS.ACCOUNT_ID, options.accountId); } headers.set(OPENAI_HEADERS.BETA, OPENAI_HEADER_VALUES.BETA_RESPONSES); headers.set(OPENAI_HEADERS.ORIGINATOR, OPENAI_HEADER_VALUES.ORIGINATOR_CODEX); headers.set("accept", "application/json"); return headers; } async function resolveCodexClientVersion( clientVersion: string | undefined, fetchFn: typeof fetch, signal: AbortSignal | undefined, ): Promise { const normalizedClientVersion = normalizeClientVersion(clientVersion); if (normalizedClientVersion) { return normalizedClientVersion; } try { const response = await fetchFn(NPM_CODEX_LATEST_URL, { method: "GET", headers: { Accept: "application/json" }, signal, }); if (!response.ok) { return DEFAULT_CODEX_CLIENT_VERSION; } const payload: unknown = await response.json(); if (!isRecord(payload)) { return DEFAULT_CODEX_CLIENT_VERSION; } const npmVersion = normalizeClientVersion(payload.version); return npmVersion ?? DEFAULT_CODEX_CLIENT_VERSION; } catch (error) { if (isAbortError(error)) { throw error; } return DEFAULT_CODEX_CLIENT_VERSION; } } function normalizeClientVersion(value: unknown): string | undefined { if (typeof value !== "string") { return undefined; } const trimmed = value.trim(); if (!/^\d+\.\d+\.\d+$/.test(trimmed)) { return undefined; } return trimmed; } function isAbortError(error: unknown): error is Error { return error instanceof Error && error.name === "AbortError"; } function normalizeCodexModels(payload: unknown, baseUrl: string): Model<"openai-codex-responses">[] | null { const parsedResponse = codexModelsResponseSchema.safeParse(payload); if (!parsedResponse.success) { return null; } const entries = parsedResponse.data.models ?? parsedResponse.data.data ?? []; const normalized: NormalizedCodexModel[] = []; for (const entry of entries) { const model = normalizeCodexModelEntry(entry, baseUrl); if (model) { normalized.push(model); } } normalized.sort((left, right) => { if (left.priority !== right.priority) { return left.priority - right.priority; } return left.model.id.localeCompare(right.model.id); }); return normalized.map(item => item.model); } function normalizeCodexModelEntry(entry: unknown, baseUrl: string): NormalizedCodexModel | null { const parsedEntry = codexModelEntrySchema.safeParse(entry); if (!parsedEntry.success) { return null; } const payload: CodexModelEntry = parsedEntry.data; const slug = toNonEmptyString(payload.slug) ?? toNonEmptyString(payload.id); if (!slug) { return null; } const supportedInApi = toBoolean(payload.supported_in_api); if (supportedInApi === false) { return null; } const name = toNonEmptyString(payload.display_name) ?? slug; const modelIdentity = { id: slug, api: "openai-codex-responses", provider: "openai-codex" } as const; const contextWindow = resolveCodexGpt56DiscoveryContext(modelIdentity, payload.context_window); const maxTokens = Math.min(DEFAULT_MAX_TOKENS, contextWindow); const reasoning = supportsReasoning(payload.default_reasoning_level, payload.supported_reasoning_levels); const input = normalizeInputModalities(payload.input_modalities); const preferWebsockets = toBoolean(payload.prefer_websockets) === true; const priority = toFiniteNumber(payload.priority) ?? Number.MAX_SAFE_INTEGER; return { priority, model: { id: slug, name, api: "openai-codex-responses", provider: "openai-codex", baseUrl, reasoning, input, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow, maxTokens, ...(preferWebsockets ? { preferWebsockets: true } : {}), ...(priority !== Number.MAX_SAFE_INTEGER ? { priority } : {}), }, }; } function supportsReasoning(defaultReasoningLevel: unknown, supportedReasoningLevels: unknown): boolean { const defaultLevel = toNonEmptyString(defaultReasoningLevel)?.toLowerCase(); if (defaultLevel && defaultLevel !== "none") { return true; } if (!Array.isArray(supportedReasoningLevels)) { return false; } for (const level of supportedReasoningLevels) { const parsedLevel = codexReasoningPresetSchema.safeParse(level); if (!parsedLevel.success) { continue; } const effort = toNonEmptyString(parsedLevel.data.effort)?.toLowerCase(); if (effort && effort !== "none") { return true; } } return false; } function normalizeInputModalities(inputModalities: unknown): ("text" | "image")[] { if (!Array.isArray(inputModalities)) { return ["text", "image"]; } const set = new Set<"text" | "image">(); for (const modality of inputModalities) { const normalized = toNonEmptyString(modality)?.toLowerCase(); if (normalized === "text" || normalized === "image") { set.add(normalized); } } if (set.size === 0) { return ["text", "image"]; } const canonical: ("text" | "image")[] = ["text", "image"]; return canonical.filter(modality => set.has(modality)); } function getResponseEtag(headers: Headers): string | undefined { const etag = headers.get("etag"); if (!etag) { return undefined; } const trimmed = etag.trim(); return trimmed.length > 0 ? trimmed : undefined; } function toNonEmptyString(value: unknown): string | null { if (typeof value !== "string") { return null; } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } function toFiniteNumber(value: unknown): number | null { if (typeof value !== "number" || !Number.isFinite(value)) { return null; } return value; } function toBoolean(value: unknown): boolean | null { if (typeof value !== "boolean") { return null; } return value; }