From 7d494390aaf8a7a61b952bd2bbc198cfe3549593 Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sat, 3 Oct 2026 23:53:42 +0200 Subject: [PATCH 01/22] test: cover account-aware native model discovery --- tests/codex-native-model-discovery.test.ts | 118 +++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 tests/codex-native-model-discovery.test.ts diff --git a/tests/codex-native-model-discovery.test.ts b/tests/codex-native-model-discovery.test.ts new file mode 100644 index 00000000..5c0321b8 --- /dev/null +++ b/tests/codex-native-model-discovery.test.ts @@ -0,0 +1,118 @@ +import { describe, expect, test } from "bun:test"; +import { discoverNativeOpenAiCatalog } from "../src/codex/catalog/native-discovery"; +import { mergeCatalogEntriesForSync } from "../src/codex/catalog/sync"; + +const liveNative = { + slug: "gpt-6.1-sol", + display_name: "GPT-6.1 Sol", + description: "Live native model", + supported_reasoning_levels: [ + { effort: "high", description: "High reasoning" }, + ], + visibility: "list", + priority: 1, + supported_in_api: true, + context_window: 400_000, + input_modalities: ["text", "image"], +}; + +describe("live native OpenAI catalog discovery", () => { + test("uses the explicitly selected pool account before the physical main account", async () => { + let mainReads = 0; + const requests: Array<{ url: string; headers: Headers }> = []; + + const result = await discoverNativeOpenAiCatalog({ + providers: {}, + codexAccounts: [{ id: "pool-a", email: "a@example.test", isMain: false }], + activeCodexAccountId: "pool-a", + } as any, { + getEffectiveActiveCodexAccountId: () => "pool-a", + getMainAccountToken: () => { + mainReads += 1; + return { accessToken: "stale-main", chatgptAccountId: "stale-main-account" }; + }, + getValidCodexToken: async (id: string) => { + expect(id).toBe("pool-a"); + return { + accessToken: "pool-access", + chatgptAccountId: "pool-chatgpt-account", + generation: 1, + }; + }, + resolveClientVersion: () => "0.160.0", + fetch: async (input: RequestInfo | URL, init?: RequestInit) => { + requests.push({ url: String(input), headers: new Headers(init?.headers) }); + return new Response(JSON.stringify({ models: [liveNative] }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + }, + }); + + expect(mainReads).toBe(0); + expect(requests).toHaveLength(1); + expect(requests[0]!.url).toBe( + "https://chatgpt.com/backend-api/codex/models?client_version=0.160.0", + ); + expect(requests[0]!.headers.get("authorization")).toBe("Bearer pool-access"); + expect(requests[0]!.headers.get("chatgpt-account-id")).toBe("pool-chatgpt-account"); + expect(requests[0]!.headers.get("originator")).toBe("codex_cli_rs"); + expect(requests[0]!.headers.get("version")).toBe("0.160.0"); + expect(result.models.map(model => model.slug)).toEqual(["gpt-6.1-sol"]); + }); + + test("falls back from a dead physical main account to a usable pool credential", async () => { + const seenAuth: string[] = []; + const result = await discoverNativeOpenAiCatalog({ + providers: {}, + codexAccounts: [{ id: "pool-b", email: "b@example.test", isMain: false }], + } as any, { + getEffectiveActiveCodexAccountId: () => undefined, + getMainAccountToken: () => ({ + accessToken: "dead-main", + chatgptAccountId: "dead-main-account", + }), + getValidCodexToken: async () => ({ + accessToken: "pool-good", + chatgptAccountId: "pool-good-account", + generation: 1, + }), + resolveClientVersion: () => "0.160.0", + fetch: async (_input: RequestInfo | URL, init?: RequestInit) => { + const auth = new Headers(init?.headers).get("authorization") ?? ""; + seenAuth.push(auth); + if (auth === "Bearer dead-main") return new Response("", { status: 401 }); + return new Response(JSON.stringify({ models: [liveNative] }), { status: 200 }); + }, + }); + + expect(seenAuth).toEqual(["Bearer dead-main", "Bearer pool-good"]); + expect(result.models.map(model => model.slug)).toEqual(["gpt-6.1-sol"]); + }); + + test("authoritative live native rows survive the static native whitelist unchanged", () => { + const result = mergeCatalogEntriesForSync( + [liveNative], + [], + new Map(), + [], + false, + new Set(), + null, + new Set(), + new Set(), + "default", + new Set(), + false, + true, + new Set(["gpt-6.1-sol"]), + ); + + const row = result.find(entry => entry.slug === "gpt-6.1-sol"); + expect(row).toBeDefined(); + expect(row?.display_name).toBe("GPT-6.1 Sol"); + expect(row?.supported_reasoning_levels).toEqual([ + { effort: "high", description: "High reasoning" }, + ]); + }); +}); From 89e8478650063b85a5ee77aa8a569befc8f4c133 Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sat, 3 Oct 2026 23:54:20 +0200 Subject: [PATCH 02/22] test: require curated provider expansion --- tests/provider-registry-parity.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/provider-registry-parity.test.ts b/tests/provider-registry-parity.test.ts index 50829c0a..b0509ced 100644 --- a/tests/provider-registry-parity.test.ts +++ b/tests/provider-registry-parity.test.ts @@ -51,6 +51,11 @@ const EXPECTED_KEY_PROVIDER_IDS = [ "azure-openai", "deepseek", "cerebras", + "cohere", + "friendliai", + "sambanova", + "nebius", + "novita", "together", "fireworks", "firepass", From 1bce8f12edcd09d24061cbdcabb4b1b462a17c8e Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sat, 3 Oct 2026 23:59:24 +0200 Subject: [PATCH 03/22] feat: discover native models with routed Codex account --- src/codex/catalog/native-discovery.ts | 222 ++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 src/codex/catalog/native-discovery.ts diff --git a/src/codex/catalog/native-discovery.ts b/src/codex/catalog/native-discovery.ts new file mode 100644 index 00000000..086bfc13 --- /dev/null +++ b/src/codex/catalog/native-discovery.ts @@ -0,0 +1,222 @@ +import type { OcxConfig } from "../../types"; +import { + MODEL_DISCOVERY_MAX_MODEL_ID_LENGTH, + MODEL_DISCOVERY_MAX_MODELS, + MODEL_DISCOVERY_MAX_RESPONSE_BYTES, + extractModelEnvelopeRows, + readBoundedDiscoveryJson, +} from "../../providers/model-discovery"; +import { isSelectableCodexPoolAccount, MAIN_CODEX_ACCOUNT_ID } from "../account-id"; +import { getValidCodexToken } from "../account-store"; +import { getMainAccountToken } from "../main-account"; +import { getEffectiveActiveCodexAccountId } from "../routing"; +import { resolveCodexRuntime } from "../runtime"; +import type { RawEntry } from "./parsing"; + +const NATIVE_MODELS_ENDPOINT = "https://chatgpt.com/backend-api/codex/models"; +const NATIVE_MODEL_ID_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/; + +type NativeCredential = { + accessToken: string; + chatgptAccountId: string; +}; + +type NativePoolToken = NativeCredential & { + generation: number; +}; + +export interface NativeOpenAiCatalogDiscovery { + models: RawEntry[]; + clientVersion: string | null; +} + +export interface NativeOpenAiCatalogDiscoveryDeps { + fetch?: typeof fetch; + getEffectiveActiveCodexAccountId?: (config: OcxConfig) => string | undefined; + getMainAccountToken?: () => NativeCredential | null; + getValidCodexToken?: (id: string) => Promise; + resolveClientVersion?: () => string | null; +} + +type CredentialCandidate = + | { kind: "main" } + | { kind: "pool"; id: string }; + +function defaultClientVersion(): string | null { + return resolveCodexRuntime({ discoverAlternatives: false }).runtime.version; +} + +function credentialCandidates( + config: OcxConfig, + selectedId: string | undefined, +): CredentialCandidate[] { + const paused = new Set(config.pausedCodexAccountIds ?? []); + const poolIds = (config.codexAccounts ?? []) + .filter(isSelectableCodexPoolAccount) + .map(account => account.id) + .filter(id => !paused.has(id)); + + const selectedPool = selectedId + && selectedId !== MAIN_CODEX_ACCOUNT_ID + && poolIds.includes(selectedId) + ? selectedId + : undefined; + + const out: CredentialCandidate[] = []; + if (selectedPool) out.push({ kind: "pool", id: selectedPool }); + if (!paused.has(MAIN_CODEX_ACCOUNT_ID)) out.push({ kind: "main" }); + for (const id of poolIds) { + if (id !== selectedPool) out.push({ kind: "pool", id }); + } + return out; +} + +async function resolveCredential( + candidate: CredentialCandidate, + deps: Required>, +): Promise { + if (candidate.kind === "main") return deps.getMainAccountToken(); + try { + const token = await deps.getValidCodexToken(candidate.id); + return { + accessToken: token.accessToken, + chatgptAccountId: token.chatgptAccountId, + }; + } catch { + return null; + } +} + +function validatedNativeModels(value: unknown): RawEntry[] | null { + const envelope = extractModelEnvelopeRows(value, MODEL_DISCOVERY_MAX_MODELS, ["models"]); + if (!envelope.ok) return null; + + const models: RawEntry[] = []; + const seen = new Set(); + for (const raw of envelope.rows) { + if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null; + const entry = raw as RawEntry; + const slug = entry.slug; + if ( + typeof slug !== "string" + || !slug + || slug !== slug.trim() + || slug.length > MODEL_DISCOVERY_MAX_MODEL_ID_LENGTH + || NATIVE_MODEL_ID_CONTROL_CHARS.test(slug) + ) { + return null; + } + if (seen.has(slug)) continue; + seen.add(slug); + const clone = { ...entry }; + // The request is already version-filtered for the installed Codex runtime. Keeping this + // field would make a proxy serving another compatible Codex build hide an otherwise usable row. + delete clone.minimal_client_version; + models.push(clone); + } + return models; +} + +/** + * Fetch the native Codex model catalog with the credential OCX is actually routing through. + * + * This intentionally never rewrites ~/.codex/auth.json. A selected pool account is tried first; + * otherwise the physical Desktop login is tried first. Authentication failures and invalid + * responses fall through to the remaining pool credentials, and total failure leaves the + * snapshot-backed catalog path untouched. + */ +export async function discoverNativeOpenAiCatalog( + config: OcxConfig, + injected: NativeOpenAiCatalogDiscoveryDeps = {}, +): Promise { + const deps = { + fetch: injected.fetch ?? fetch, + getEffectiveActiveCodexAccountId: + injected.getEffectiveActiveCodexAccountId ?? getEffectiveActiveCodexAccountId, + getMainAccountToken: injected.getMainAccountToken ?? getMainAccountToken, + getValidCodexToken: injected.getValidCodexToken ?? getValidCodexToken, + resolveClientVersion: injected.resolveClientVersion ?? defaultClientVersion, + }; + + const clientVersion = deps.resolveClientVersion(); + if (!clientVersion) return { models: [], clientVersion: null }; + + const url = new URL(NATIVE_MODELS_ENDPOINT); + url.searchParams.set("client_version", clientVersion); + + const candidates = credentialCandidates( + config, + deps.getEffectiveActiveCodexAccountId(config), + ); + for (const candidate of candidates) { + const credential = await resolveCredential(candidate, deps); + if (!credential?.accessToken || !credential.chatgptAccountId) continue; + + let response: Response; + try { + response = await deps.fetch(url, { + method: "GET", + headers: { + authorization: `Bearer ${credential.accessToken}`, + "chatgpt-account-id": credential.chatgptAccountId, + originator: "codex_cli_rs", + version: clientVersion, + }, + }); + } catch { + continue; + } + + if (!response.ok) { + try { + void response.body?.cancel(); + } catch { + // Best-effort body cleanup only. + } + continue; + } + + const parsed = await readBoundedDiscoveryJson( + response, + MODEL_DISCOVERY_MAX_RESPONSE_BYTES, + ); + if (!parsed.ok) continue; + const models = validatedNativeModels(parsed.value); + if (!models) continue; + return { models, clientVersion }; + } + + return { models: [], clientVersion }; +} + +/** Replace same-slug native rows with live authoritative rows and append newly rolled-out ones. */ +export function mergeDiscoveredNativeCatalogRows( + catalogModels: RawEntry[], + discoveredModels: RawEntry[], +): RawEntry[] { + if (discoveredModels.length === 0) return catalogModels; + + const bySlug = new Map( + discoveredModels.flatMap(model => + typeof model.slug === "string" && !model.slug.includes("/") + ? [[model.slug, model] as const] + : [] + ), + ); + if (bySlug.size === 0) return catalogModels; + + const merged = catalogModels.map(model => { + const slug = typeof model.slug === "string" && !model.slug.includes("/") + ? model.slug + : undefined; + if (!slug) return model; + const replacement = bySlug.get(slug); + if (!replacement) return model; + bySlug.delete(slug); + return replacement; + }); + return [...merged, ...bySlug.values()]; +} From 909cce8aa82e0e826d8ff142536b4e65e4009fb7 Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sat, 3 Oct 2026 23:59:32 +0200 Subject: [PATCH 04/22] feat: admit authoritative live native model slugs --- src/codex/catalog/metadata.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/codex/catalog/metadata.ts b/src/codex/catalog/metadata.ts index 32fed5e7..2be0aa93 100644 --- a/src/codex/catalog/metadata.ts +++ b/src/codex/catalog/metadata.ts @@ -137,10 +137,18 @@ export function nativeModelRows(config: Pick): Arra }); } -export function applyNativeVisibility(entries: RawEntry[], disabledNative: Set): RawEntry[] { +export function applyNativeVisibility( + entries: RawEntry[], + disabledNative: Set, + authoritativeNativeSlugs: ReadonlySet = new Set(), +): RawEntry[] { for (const entry of entries) { const slug = typeof entry.slug === "string" ? entry.slug : ""; - if (!slug || slug.includes("/") || !SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug)) continue; + if ( + !slug + || slug.includes("/") + || (!SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug) && !authoritativeNativeSlugs.has(slug)) + ) continue; entry.visibility = disabledNative.has(slug) ? "hide" : "list"; } return entries; From d471a56ac3b8046c6762da1a986b4f809f200d40 Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sat, 3 Oct 2026 23:59:54 +0200 Subject: [PATCH 05/22] feat: merge live native catalog without static whitelist loss --- src/codex/catalog/sync.ts | 49 +++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/src/codex/catalog/sync.ts b/src/codex/catalog/sync.ts index 3931f2bd..11c95716 100644 --- a/src/codex/catalog/sync.ts +++ b/src/codex/catalog/sync.ts @@ -33,6 +33,7 @@ import upstreamModelsSnapshot from "../data/upstream-models.json"; import { activeCodexModelsCachePath, applyJawcodeCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride, catalogModelSlug, ensureCatalogBackup, ensureStrictCatalogFields, findNativeTemplate, isRoutedModelCompatibilityExcluded, normalizeRoutedCatalogEntry, normalizeServiceTiers, readCatalog, readCatalogBackup, readCodexCatalogPath, readNativeBaseline } from "./parsing"; import type { CatalogModel, MultiAgentMode, RawEntry } from "./parsing"; +import { discoverNativeOpenAiCatalog, mergeDiscoveredNativeCatalogRows } from "./native-discovery"; import { applyNativeVisibility, disabledNativeSlugs, isUnsupportedOpenAiNativeSlug, nativeOpenAiSlugs, shouldUpgradeToUpstreamEntry, upstreamNativeEntry } from "./metadata"; import { loadCatalogForSync, resetBundledCatalogCacheForTests } from "./bundled"; import { applyCatalogModelMetadata, applyReasoningLevels, catalogEntryEfforts, clampCatalogModelsToCodexSupport, ensureGpt56ReasoningLevels, ensureUltraReasoningLevel, isGpt56NativeSlug } from "./effort"; @@ -332,6 +333,7 @@ export function mergeCatalogEntriesForSync( exactComboSlugs: ReadonlySet = new Set(), hasPhysicalComboProvider = false, includeNativeOpenAi = true, + authoritativeNativeSlugs: ReadonlySet = new Set(), ): RawEntry[] { const rank = new Map(featured.map((slug, i) => [slug, i] as const)); const native = includeNativeOpenAi @@ -340,7 +342,10 @@ export function mergeCatalogEntriesForSync( && !(m.slug as string).includes("/") && m.owned_by !== COMBO_NAMESPACE && !goIds.has(m.slug as string) - && !isUnsupportedOpenAiNativeSlug(m.slug as string)) + && ( + !isUnsupportedOpenAiNativeSlug(m.slug as string) + || authoritativeNativeSlugs.has(m.slug as string) + )) .map(m => { const slug = m.slug as string; // Featured models rank first (rank order); non-featured natives are pushed below the featured @@ -369,7 +374,9 @@ export function mergeCatalogEntriesForSync( const preserved = normalizeServiceTiers({ ...m, priority }); // Older natives kept from disk still need the mock top tiers (max + ultra always // for subagent max spawns; wire-clamped to the model's real top rung). - if (!isGpt56NativeSlug(slug)) ensureUltraReasoningLevel(preserved); + if (!authoritativeNativeSlugs.has(slug) && !isGpt56NativeSlug(slug)) { + ensureUltraReasoningLevel(preserved); + } return preserved; }) : []; @@ -432,6 +439,8 @@ export function mergeCatalogEntriesForSync( const normalized = normalizeServiceTiers(m); applyNativeOpenAiContextOverride(normalized); const exactCombo = typeof m.slug === "string" && exactComboSlugs.has(m.slug); + const authoritativeNative = typeof m.slug === "string" + && authoritativeNativeSlugs.has(m.slug); const e = ensureStrictCatalogFields(normalized, { preserveExactInputModalities: exactCombo, isRouted: finalRoutedEntries.includes(m), @@ -439,7 +448,7 @@ export function mergeCatalogEntriesForSync( // Mock-max universality (260709): preserved routed entries from disk may predate // the max rung — ensure it here so subagent max spawns validate on every // reasoning-capable entry. max only: 5.6 exact ladders (luna: no ultra) stay intact. - if (!exactCombo) { + if (!exactCombo && !authoritativeNative) { const levels = Array.isArray(e.supported_reasoning_levels) ? e.supported_reasoning_levels as Array<{ effort?: string }> : []; @@ -459,7 +468,10 @@ export function mergeCatalogEntriesForSync( }); // Native enable/disable (single choke point: bare slugs in `disabledModels`). Runs as the // LAST pass so the upstream-upgrade branch above can never clobber a hide flag back to list. - return applyMultiAgentMode(applyNativeVisibility(mergedEntries, disabledNative), multiAgentMode); + return applyMultiAgentMode( + applyNativeVisibility(mergedEntries, disabledNative, authoritativeNativeSlugs), + multiAgentMode, + ); } export async function syncCatalogModels(config: OcxConfig): Promise<{ @@ -516,7 +528,34 @@ export async function syncCatalogModels(config: OcxConfig): Promise<{ // bare gpt-* rows that hard-404 via NoEnabledOpenAiProviderError. Keep natives when no // providers are configured yet (fresh install / catalog bootstrap tests). const includeNativeOpenAi = enabledProviders.length === 0 || hasCanonicalOpenai; - catalog.models = mergeCatalogEntriesForSync(catalog.models ?? [], goEntries, baseline, featured, wsEnabled, goIds, template, disabledNativeSlugs(config), gatheredProviderNames, multiAgentMode, exactComboSlugs, hasPhysicalComboProvider, includeNativeOpenAi); + const liveNative = includeNativeOpenAi + ? await discoverNativeOpenAiCatalog(config) + : { models: [], clientVersion: null }; + const authoritativeNativeSlugs = new Set( + liveNative.models.flatMap(model => + typeof model.slug === "string" && !model.slug.includes("/") ? [model.slug] : [] + ), + ); + const catalogModels = mergeDiscoveredNativeCatalogRows( + catalog.models ?? [], + liveNative.models, + ); + catalog.models = mergeCatalogEntriesForSync( + catalogModels, + goEntries, + baseline, + featured, + wsEnabled, + goIds, + template, + disabledNativeSlugs(config), + gatheredProviderNames, + multiAgentMode, + exactComboSlugs, + hasPhysicalComboProvider, + includeNativeOpenAi, + authoritativeNativeSlugs, + ); clampCatalogModelsToCodexSupport(catalog.models); atomicWriteFile(catalogPath, JSON.stringify(catalog, null, 2) + "\n"); From 90dc2f0179414e6c415d7cca148412fda52c629f Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 4 Oct 2026 00:00:23 +0200 Subject: [PATCH 06/22] feat: promote five OpenAI-compatible providers --- src/providers/registry.ts | 64 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/providers/registry.ts b/src/providers/registry.ts index 7080a288..260c43cc 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -1439,6 +1439,70 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ dashboardUrl: "https://cloud.cerebras.ai/platform/apikeys", defaultModel: "gpt-oss-120b", }, + // Cohere's OpenAI Compatibility API supports the OpenAI SDK at this base URL. + // Trial/evaluation API keys are free but rate-limited. + // Evidence: https://docs.cohere.com/docs/compatibility-api + // https://docs.cohere.com/v2/docs/rate-limits + { + id: "cohere", + label: "Cohere", + baseUrl: "https://api.cohere.ai/compatibility/v1", + adapter: "openai-chat", + authKind: "key", + dashboardUrl: "https://dashboard.cohere.com/api-keys", + freeTier: true, + liveModels: true, + preserveCustomDestination: true, + note: "OpenAI Compatibility API. Free evaluation keys are rate-limited and intended for evaluation/prototyping.", + }, + // Friendli Model API is OpenAI-compatible and publishes GET /models on the same serverless base. + // Evidence: https://learn.friendli.ai/articles/4213111023-q10-1-how-do-i-connect-friendliai-with-litellm-or-other-openai-compatible-client + { + id: "friendliai", + label: "FriendliAI", + baseUrl: "https://api.friendli.ai/serverless/v1", + adapter: "openai-chat", + authKind: "key", + dashboardUrl: "https://suite.friendli.ai", + liveModels: true, + preserveCustomDestination: true, + }, + // SambaCloud documents an OpenAI-compatible API at api.sambanova.ai/v1. + // Evidence: https://cloud.sambanova.ai/dashboard + { + id: "sambanova", + label: "SambaNova", + baseUrl: "https://api.sambanova.ai/v1", + adapter: "openai-chat", + authKind: "key", + dashboardUrl: "https://cloud.sambanova.ai/apis", + liveModels: true, + preserveCustomDestination: true, + }, + // Nebius AI Studio's current OpenAI-compatible OAS publishes chat, responses and GET /v1/models. + // Evidence: https://api.studio.nebius.com/docs + { + id: "nebius", + label: "Nebius AI Studio", + baseUrl: "https://api.studio.nebius.com/v1", + adapter: "openai-chat", + authKind: "key", + dashboardUrl: "https://studio.nebius.com", + liveModels: true, + preserveCustomDestination: true, + }, + // Novita's current serverless docs use the OpenAI-compatible /openai/v1 path. + // Evidence: https://blogs.novita.ai/glm-5-1-api-novita-ai/ + { + id: "novita", + label: "Novita AI", + baseUrl: "https://api.novita.ai/openai/v1", + adapter: "openai-chat", + authKind: "key", + dashboardUrl: "https://novita.ai/settings/key-management", + liveModels: true, + preserveCustomDestination: true, + }, // FREEZE 2026-07-10: exact serverless ids remain auth-gated/unverified. Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md. { id: "together", From 5d4d088bbdf9690b027988d981f4315372a48763 Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 4 Oct 2026 00:00:36 +0200 Subject: [PATCH 07/22] fix: align promoted provider directory endpoints --- src/providers/free-directory.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/providers/free-directory.ts b/src/providers/free-directory.ts index ee3651bd..1db10b4b 100644 --- a/src/providers/free-directory.ts +++ b/src/providers/free-directory.ts @@ -202,13 +202,13 @@ const CONNECTABLE: Record = { }, ), cohere: openAi( - "https://api.cohere.com/compatibility/v1", + "https://api.cohere.ai/compatibility/v1", "https://dashboard.cohere.com/api-keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.cohere.com/reference/list-models", - modelsUrl: "https://api.cohere.com/compatibility/v1/models", + modelsUrl: "https://api.cohere.ai/compatibility/v1/models", }, ), friendliai: openAi( @@ -677,7 +677,7 @@ const CONNECTABLE: Record = { verification: "official", }), nebius: openAi( - "https://api.tokenfactory.nebius.com/v1", + "https://api.studio.nebius.com/v1", "https://studio.nebius.com", { verification: "official" }, ), From f0410bd4651158882bd607f7e5a6a89d9b65ea1e Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 4 Oct 2026 00:01:24 +0200 Subject: [PATCH 08/22] fix: bound native catalog discovery requests --- src/codex/catalog/native-discovery.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/codex/catalog/native-discovery.ts b/src/codex/catalog/native-discovery.ts index 086bfc13..91bcc2da 100644 --- a/src/codex/catalog/native-discovery.ts +++ b/src/codex/catalog/native-discovery.ts @@ -165,6 +165,7 @@ export async function discoverNativeOpenAiCatalog( originator: "codex_cli_rs", version: clientVersion, }, + signal: AbortSignal.timeout(8_000), }); } catch { continue; @@ -172,7 +173,7 @@ export async function discoverNativeOpenAiCatalog( if (!response.ok) { try { - void response.body?.cancel(); + void response.body?.cancel().catch(() => undefined); } catch { // Best-effort body cleanup only. } From 177b52f8994929846a9db1bc41b220f19c92e02a Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 4 Oct 2026 00:06:20 +0200 Subject: [PATCH 09/22] test: include Cohere in free-tier parity --- tests/provider-registry-parity.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/provider-registry-parity.test.ts b/tests/provider-registry-parity.test.ts index b0509ced..5c177fb2 100644 --- a/tests/provider-registry-parity.test.ts +++ b/tests/provider-registry-parity.test.ts @@ -748,6 +748,7 @@ describe("provider registry parity", () => { expect(nvidia?.keyOptional).toBeUndefined(); expect(freeTierProviders).toEqual([ "tokenharbor", + "cohere", "nvidia", "cloudflare-workers-ai", "omniroute", From f49a348d0d9ac8e344049473ae610a4270eeade3 Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 4 Oct 2026 00:08:54 +0200 Subject: [PATCH 10/22] test: cover bounded account fallback discovery --- tests/codex-native-model-discovery.test.ts | 67 ++++++++++++++++++---- 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/tests/codex-native-model-discovery.test.ts b/tests/codex-native-model-discovery.test.ts index 5c0321b8..fda9a905 100644 --- a/tests/codex-native-model-discovery.test.ts +++ b/tests/codex-native-model-discovery.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test"; import { discoverNativeOpenAiCatalog } from "../src/codex/catalog/native-discovery"; import { mergeCatalogEntriesForSync } from "../src/codex/catalog/sync"; +import type { OcxConfig } from "../src/types"; const liveNative = { slug: "gpt-6.1-sol", @@ -16,16 +17,26 @@ const liveNative = { input_modalities: ["text", "image"], }; +function config( + overrides: Pick = {}, +): OcxConfig { + return { + port: 10100, + providers: {}, + defaultProvider: "openai", + ...overrides, + }; +} + describe("live native OpenAI catalog discovery", () => { test("uses the explicitly selected pool account before the physical main account", async () => { let mainReads = 0; const requests: Array<{ url: string; headers: Headers }> = []; - const result = await discoverNativeOpenAiCatalog({ - providers: {}, + const result = await discoverNativeOpenAiCatalog(config({ codexAccounts: [{ id: "pool-a", email: "a@example.test", isMain: false }], activeCodexAccountId: "pool-a", - } as any, { + }), { getEffectiveActiveCodexAccountId: () => "pool-a", getMainAccountToken: () => { mainReads += 1; @@ -51,22 +62,23 @@ describe("live native OpenAI catalog discovery", () => { expect(mainReads).toBe(0); expect(requests).toHaveLength(1); - expect(requests[0]!.url).toBe( + const request = requests[0]; + if (!request) throw new Error("expected one native catalog request"); + expect(request.url).toBe( "https://chatgpt.com/backend-api/codex/models?client_version=0.160.0", ); - expect(requests[0]!.headers.get("authorization")).toBe("Bearer pool-access"); - expect(requests[0]!.headers.get("chatgpt-account-id")).toBe("pool-chatgpt-account"); - expect(requests[0]!.headers.get("originator")).toBe("codex_cli_rs"); - expect(requests[0]!.headers.get("version")).toBe("0.160.0"); + expect(request.headers.get("authorization")).toBe("Bearer pool-access"); + expect(request.headers.get("chatgpt-account-id")).toBe("pool-chatgpt-account"); + expect(request.headers.get("originator")).toBe("codex_cli_rs"); + expect(request.headers.get("version")).toBe("0.160.0"); expect(result.models.map(model => model.slug)).toEqual(["gpt-6.1-sol"]); }); test("falls back from a dead physical main account to a usable pool credential", async () => { const seenAuth: string[] = []; - const result = await discoverNativeOpenAiCatalog({ - providers: {}, + const result = await discoverNativeOpenAiCatalog(config({ codexAccounts: [{ id: "pool-b", email: "b@example.test", isMain: false }], - } as any, { + }), { getEffectiveActiveCodexAccountId: () => undefined, getMainAccountToken: () => ({ accessToken: "dead-main", @@ -90,6 +102,39 @@ describe("live native OpenAI catalog discovery", () => { expect(result.models.map(model => model.slug)).toEqual(["gpt-6.1-sol"]); }); + test("shares one total request deadline across account fallbacks", async () => { + const signals: AbortSignal[] = []; + let calls = 0; + const result = await discoverNativeOpenAiCatalog(config({ + codexAccounts: [{ id: "pool-c", email: "c@example.test", isMain: false }], + }), { + getEffectiveActiveCodexAccountId: () => undefined, + getMainAccountToken: () => ({ + accessToken: "dead-main", + chatgptAccountId: "dead-main-account", + }), + getValidCodexToken: async () => ({ + accessToken: "pool-good", + chatgptAccountId: "pool-good-account", + generation: 1, + }), + resolveClientVersion: () => "0.160.0", + fetch: async (_input: RequestInfo | URL, init?: RequestInit) => { + if (!(init?.signal instanceof AbortSignal)) { + throw new Error("expected native catalog request deadline"); + } + signals.push(init.signal); + calls += 1; + if (calls === 1) return new Response("", { status: 401 }); + return new Response(JSON.stringify({ models: [liveNative] }), { status: 200 }); + }, + }); + + expect(signals).toHaveLength(2); + expect(signals[1]).toBe(signals[0]); + expect(result.models.map(model => model.slug)).toEqual(["gpt-6.1-sol"]); + }); + test("authoritative live native rows survive the static native whitelist unchanged", () => { const result = mergeCatalogEntriesForSync( [liveNative], From f4f653fa77035f819e724e00be44a83e70d5cb3a Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 4 Oct 2026 00:09:13 +0200 Subject: [PATCH 11/22] fix: bound native discovery across account fallbacks --- src/codex/catalog/native-discovery.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/codex/catalog/native-discovery.ts b/src/codex/catalog/native-discovery.ts index 91bcc2da..33ddbc54 100644 --- a/src/codex/catalog/native-discovery.ts +++ b/src/codex/catalog/native-discovery.ts @@ -42,6 +42,10 @@ type CredentialCandidate = | { kind: "main" } | { kind: "pool"; id: string }; +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + function defaultClientVersion(): string | null { return resolveCodexRuntime({ discoverAlternatives: false }).runtime.version; } @@ -97,9 +101,8 @@ function validatedNativeModels(value: unknown): RawEntry[] | null { const models: RawEntry[] = []; const seen = new Set(); for (const raw of envelope.rows) { - if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null; - const entry = raw as RawEntry; - const slug = entry.slug; + if (!isRecord(raw)) return null; + const slug = raw.slug; if ( typeof slug !== "string" || !slug @@ -111,7 +114,7 @@ function validatedNativeModels(value: unknown): RawEntry[] | null { } if (seen.has(slug)) continue; seen.add(slug); - const clone = { ...entry }; + const clone: RawEntry = { ...raw }; // The request is already version-filtered for the installed Codex runtime. Keeping this // field would make a proxy serving another compatible Codex build hide an otherwise usable row. delete clone.minimal_client_version; @@ -151,6 +154,9 @@ export async function discoverNativeOpenAiCatalog( config, deps.getEffectiveActiveCodexAccountId(config), ); + // One wall-clock budget for the entire account fallback sequence. A dead upstream must not + // multiply the discovery delay by the number of configured pool accounts. + const requestSignal = AbortSignal.timeout(8_000); for (const candidate of candidates) { const credential = await resolveCredential(candidate, deps); if (!credential?.accessToken || !credential.chatgptAccountId) continue; @@ -165,7 +171,7 @@ export async function discoverNativeOpenAiCatalog( originator: "codex_cli_rs", version: clientVersion, }, - signal: AbortSignal.timeout(8_000), + signal: requestSignal, }); } catch { continue; From 400a8cf0c00b2ce5151453dd5a5ef9ed83cdb340 Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 4 Oct 2026 00:11:51 +0200 Subject: [PATCH 12/22] test: respect standalone account-pool opt-out --- tests/codex-native-model-discovery.test.ts | 33 +++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/codex-native-model-discovery.test.ts b/tests/codex-native-model-discovery.test.ts index fda9a905..81ae41c7 100644 --- a/tests/codex-native-model-discovery.test.ts +++ b/tests/codex-native-model-discovery.test.ts @@ -18,7 +18,10 @@ const liveNative = { }; function config( - overrides: Pick = {}, + overrides: Pick< + OcxConfig, + "codexAccounts" | "activeCodexAccountId" | "codexAccountPools" + > = {}, ): OcxConfig { return { port: 10100, @@ -102,6 +105,34 @@ describe("live native OpenAI catalog discovery", () => { expect(result.models.map(model => model.slug)).toEqual(["gpt-6.1-sol"]); }); + test("does not consult pool credentials when account pools are disabled", async () => { + let poolReads = 0; + const result = await discoverNativeOpenAiCatalog(config({ + codexAccountPools: false, + codexAccounts: [{ id: "pool-disabled", email: "off@example.test", isMain: false }], + activeCodexAccountId: "pool-disabled", + }), { + getEffectiveActiveCodexAccountId: () => "pool-disabled", + getMainAccountToken: () => ({ + accessToken: "dead-main", + chatgptAccountId: "dead-main-account", + }), + getValidCodexToken: async () => { + poolReads += 1; + return { + accessToken: "must-not-be-read", + chatgptAccountId: "must-not-be-read", + generation: 1, + }; + }, + resolveClientVersion: () => "0.160.0", + fetch: async () => new Response("", { status: 401 }), + }); + + expect(poolReads).toBe(0); + expect(result.models).toEqual([]); + }); + test("shares one total request deadline across account fallbacks", async () => { const signals: AbortSignal[] = []; let calls = 0; From a29e520362926acd8d7075bdf716e918c38dee7c Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 4 Oct 2026 00:12:01 +0200 Subject: [PATCH 13/22] fix: honor standalone pool disable in discovery --- src/codex/catalog/native-discovery.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/codex/catalog/native-discovery.ts b/src/codex/catalog/native-discovery.ts index 33ddbc54..24f4d86c 100644 --- a/src/codex/catalog/native-discovery.ts +++ b/src/codex/catalog/native-discovery.ts @@ -55,10 +55,12 @@ function credentialCandidates( selectedId: string | undefined, ): CredentialCandidate[] { const paused = new Set(config.pausedCodexAccountIds ?? []); - const poolIds = (config.codexAccounts ?? []) - .filter(isSelectableCodexPoolAccount) - .map(account => account.id) - .filter(id => !paused.has(id)); + const poolIds = config.codexAccountPools === false + ? [] + : (config.codexAccounts ?? []) + .filter(isSelectableCodexPoolAccount) + .map(account => account.id) + .filter(id => !paused.has(id)); const selectedPool = selectedId && selectedId !== MAIN_CODEX_ACCOUNT_ID @@ -206,13 +208,12 @@ export function mergeDiscoveredNativeCatalogRows( ): RawEntry[] { if (discoveredModels.length === 0) return catalogModels; - const bySlug = new Map( - discoveredModels.flatMap(model => - typeof model.slug === "string" && !model.slug.includes("/") - ? [[model.slug, model] as const] - : [] - ), - ); + const bySlug = new Map(); + for (const model of discoveredModels) { + if (typeof model.slug === "string" && !model.slug.includes("/")) { + bySlug.set(model.slug, model); + } + } if (bySlug.size === 0) return catalogModels; const merged = catalogModels.map(model => { From 486fbb2073aedcdb80d0b9544b8b5cb22e3e7dbd Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 4 Oct 2026 00:12:42 +0200 Subject: [PATCH 14/22] test: cover native catalog body read failure --- tests/codex-native-model-discovery.test.ts | 33 ++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/codex-native-model-discovery.test.ts b/tests/codex-native-model-discovery.test.ts index 81ae41c7..ecb0bece 100644 --- a/tests/codex-native-model-discovery.test.ts +++ b/tests/codex-native-model-discovery.test.ts @@ -105,6 +105,39 @@ describe("live native OpenAI catalog discovery", () => { expect(result.models.map(model => model.slug)).toEqual(["gpt-6.1-sol"]); }); + test("falls back when a successful response body fails while reading", async () => { + let calls = 0; + const result = await discoverNativeOpenAiCatalog(config({ + codexAccounts: [{ id: "pool-stream", email: "stream@example.test", isMain: false }], + }), { + getEffectiveActiveCodexAccountId: () => undefined, + getMainAccountToken: () => ({ + accessToken: "main-stream-fails", + chatgptAccountId: "main-stream-account", + }), + getValidCodexToken: async () => ({ + accessToken: "pool-stream-good", + chatgptAccountId: "pool-stream-account", + generation: 1, + }), + resolveClientVersion: () => "0.160.0", + fetch: async () => { + calls += 1; + if (calls === 1) { + return new Response(new ReadableStream({ + start(controller) { + controller.error(new Error("upstream body failed")); + }, + }), { status: 200 }); + } + return new Response(JSON.stringify({ models: [liveNative] }), { status: 200 }); + }, + }); + + expect(calls).toBe(2); + expect(result.models.map(model => model.slug)).toEqual(["gpt-6.1-sol"]); + }); + test("does not consult pool credentials when account pools are disabled", async () => { let poolReads = 0; const result = await discoverNativeOpenAiCatalog(config({ From 72820b9cea16ab762e345de3ea4774dc5b8a1c68 Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 4 Oct 2026 00:12:49 +0200 Subject: [PATCH 15/22] fix: degrade on native catalog body failures --- src/codex/catalog/native-discovery.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/codex/catalog/native-discovery.ts b/src/codex/catalog/native-discovery.ts index 24f4d86c..1f14db64 100644 --- a/src/codex/catalog/native-discovery.ts +++ b/src/codex/catalog/native-discovery.ts @@ -188,10 +188,15 @@ export async function discoverNativeOpenAiCatalog( continue; } - const parsed = await readBoundedDiscoveryJson( - response, - MODEL_DISCOVERY_MAX_RESPONSE_BYTES, - ); + let parsed; + try { + parsed = await readBoundedDiscoveryJson( + response, + MODEL_DISCOVERY_MAX_RESPONSE_BYTES, + ); + } catch { + continue; + } if (!parsed.ok) continue; const models = validatedNativeModels(parsed.value); if (!models) continue; From 223c21569d442bde4821d2d8c81b5a600b5ab100 Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 4 Oct 2026 00:13:01 +0200 Subject: [PATCH 16/22] refactor: keep native discovery result typed --- src/codex/catalog/native-discovery.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/codex/catalog/native-discovery.ts b/src/codex/catalog/native-discovery.ts index 1f14db64..67a5d081 100644 --- a/src/codex/catalog/native-discovery.ts +++ b/src/codex/catalog/native-discovery.ts @@ -5,6 +5,7 @@ import { MODEL_DISCOVERY_MAX_RESPONSE_BYTES, extractModelEnvelopeRows, readBoundedDiscoveryJson, + type BoundedDiscoveryJsonResult, } from "../../providers/model-discovery"; import { isSelectableCodexPoolAccount, MAIN_CODEX_ACCOUNT_ID } from "../account-id"; import { getValidCodexToken } from "../account-store"; @@ -188,7 +189,7 @@ export async function discoverNativeOpenAiCatalog( continue; } - let parsed; + let parsed: BoundedDiscoveryJsonResult; try { parsed = await readBoundedDiscoveryJson( response, From 5b78bbdeb399a88b79c676d9c62e7cf004e8b926 Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 4 Oct 2026 00:15:39 +0200 Subject: [PATCH 17/22] test: preserve live native metadata in catalog builder --- tests/codex-native-model-discovery.test.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/codex-native-model-discovery.test.ts b/tests/codex-native-model-discovery.test.ts index ecb0bece..432e2a52 100644 --- a/tests/codex-native-model-discovery.test.ts +++ b/tests/codex-native-model-discovery.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { discoverNativeOpenAiCatalog } from "../src/codex/catalog/native-discovery"; -import { mergeCatalogEntriesForSync } from "../src/codex/catalog/sync"; +import { buildCatalogEntries, mergeCatalogEntriesForSync } from "../src/codex/catalog/sync"; import type { OcxConfig } from "../src/types"; const liveNative = { @@ -199,6 +199,26 @@ describe("live native OpenAI catalog discovery", () => { expect(result.models.map(model => model.slug)).toEqual(["gpt-6.1-sol"]); }); + test("catalog builder preserves authoritative metadata for a newly rolled-out native slug", () => { + const entries = buildCatalogEntries( + null, + ["gpt-6.1-sol"], + [], + undefined, + false, + "default", + new Set(), + new Map([["gpt-6.1-sol", liveNative]]), + ); + + const row = entries.find(entry => entry.slug === "gpt-6.1-sol"); + expect(row?.display_name).toBe("GPT-6.1 Sol"); + expect(row?.context_window).toBe(400_000); + expect(row?.supported_reasoning_levels).toEqual([ + { effort: "high", description: "High reasoning" }, + ]); + }); + test("authoritative live native rows survive the static native whitelist unchanged", () => { const result = mergeCatalogEntriesForSync( [liveNative], From 154f8d248607e1ce930a63d17ec13d6755114b02 Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 4 Oct 2026 00:16:02 +0200 Subject: [PATCH 18/22] feat: preserve live native metadata in catalog builder --- src/codex/catalog/sync.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/codex/catalog/sync.ts b/src/codex/catalog/sync.ts index 11c95716..fa669e1d 100644 --- a/src/codex/catalog/sync.ts +++ b/src/codex/catalog/sync.ts @@ -133,6 +133,17 @@ export function finishUpstreamNativeEntry(clone: RawEntry, priority: number): Ra return ensureStrictCatalogFields(normalizeServiceTiers(clone)); } +/** + * Normalize a live native Codex row without synthesizing reasoning tiers. + * The upstream response is already filtered for the requesting Codex version. + */ +export function finishAuthoritativeNativeEntry(entry: RawEntry, priority: number): RawEntry { + const clone = structuredClone(entry); + if (priority !== 9) clone.priority = priority; + applyNativeOpenAiContextOverride(clone); + return ensureStrictCatalogFields(normalizeServiceTiers(clone)); +} + export function isExactComboCatalogModel( model: CatalogModel | undefined, exactComboSlugs: ReadonlySet, @@ -236,6 +247,7 @@ export function buildCatalogEntries( wsEnabled = false, multiAgentMode: MultiAgentMode = "default", exactComboSlugs: ReadonlySet = new Set(), + authoritativeNativeEntries: ReadonlyMap = new Map(), ): RawEntry[] { // Codex's models-manager sorts by `priority` ASC and advertises the first 5 picker-visible // models to spawn_agent (sort_by_key(priority) + MAX_MODEL_OVERRIDES_IN_SPAWN_AGENT=5). Catalog @@ -248,7 +260,10 @@ export function buildCatalogEntries( .filter(model => model.provider === COMBO_NAMESPACE) .map(catalogModelSlug)); for (const slug of gptSlugs) { - const e = deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9); + const authoritative = authoritativeNativeEntries.get(slug); + const e = authoritative + ? finishAuthoritativeNativeEntry(authoritative, 9) + : deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9); if (rank.has(slug)) e.priority = rank.get(slug)!; out.push(e); } From af26296dfb41ce88efa499342aa829e48848c9cf Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 4 Oct 2026 00:16:33 +0200 Subject: [PATCH 19/22] fix: serve live native models to Codex clients --- src/server/index.ts | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/server/index.ts b/src/server/index.ts index 4a766268..a35625d4 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -835,8 +835,27 @@ export function startServer(port?: number) { return jsonResponse({ data }, 200, req, config); } if (url.searchParams.has("client_version")) { - // Codex client → Codex catalog shape: native gpt + namespaced routed models, - // cloned from a native template so required fields (base_instructions, etc.) are present. + // Codex client → Codex catalog shape. Ask the same upstream catalog the client would + // use, but authenticate with OCX's effective account so a stale Desktop main login + // cannot hide models available to the selected pool account. + const { discoverNativeOpenAiCatalog } = + await import("../codex/catalog/native-discovery"); + const requestedClientVersion = url.searchParams.get("client_version")?.trim() || null; + const liveNative = await discoverNativeOpenAiCatalog(config, { + resolveClientVersion: () => requestedClientVersion, + }); + const authoritativeNativeEntries = + new Map(); + for (const model of liveNative.models) { + if (typeof model.slug === "string" && !model.slug.includes("/")) { + authoritativeNativeEntries.set(model.slug, model); + } + } + const authoritativeNativeSlugs = new Set(authoritativeNativeEntries.keys()); + const codexNativeSlugs = [ + ...new Set([...nativeSlugs, ...authoritativeNativeSlugs]), + ]; + // Pass the subagent picks so featured models lead by priority (matches the on-disk file). // Disabled natives stay in the catalog shape with visibility "hide" (mirrors the // on-disk sync; codex-rs keeps them out of the picker itself). @@ -846,18 +865,20 @@ export function startServer(port?: number) { : "default"; const entries = buildCatalogEntries( loadCatalogTemplate(), - nativeSlugs, + codexNativeSlugs, goOrdered, config.subagentModels, websocketsEnabled(config), maMode as "v1" | "default" | "v2", exactComboCatalogSlugs(config), + authoritativeNativeEntries, ); return jsonResponse( { models: applyNativeVisibility( entries, disabledNativeSlugs(config), + authoritativeNativeSlugs, ), }, 200, From f76aff8dfaeb119074a05ea57132a9a5279b4d66 Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 4 Oct 2026 00:17:24 +0200 Subject: [PATCH 20/22] test: verify live native Codex catalog route --- tests/claude-models-discovery.test.ts | 91 ++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/tests/claude-models-discovery.test.ts b/tests/claude-models-discovery.test.ts index cf7958a4..6fc5501e 100644 --- a/tests/claude-models-discovery.test.ts +++ b/tests/claude-models-discovery.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, expect, setDefaultTimeout, test } from "bun:test"; -import { mkdtempSync, rmSync } from "node:fs"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { saveConfig } from "../src/config"; @@ -49,6 +49,23 @@ function configWithStaticModels(claudeCode?: OcxConfig["claudeCode"]): OcxConfig } as OcxConfig; } +function configWithNativeOpenAi(): OcxConfig { + return { + port: 0, + defaultProvider: "openai", + openaiProviderTierVersion: 2, + codexAccountPools: false, + providers: { + openai: { + adapter: "openai-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + authMode: "forward", + codexAccountMode: "direct", + }, + }, + }; +} + test("anthropic-version header flips /v1/models to the discovery contract", async () => { saveConfig(configWithStaticModels()); const server = startServer(0); @@ -158,3 +175,75 @@ test("OpenAI list shape and Codex catalog shape stay unchanged", async () => { server.stop(true); } }); + +test("Codex client catalog includes live native metadata for its client version", async () => { + if (!isolatedCodexHome) throw new Error("isolated Codex home not installed"); + writeFileSync( + join(isolatedCodexHome.path, "auth.json"), + JSON.stringify({ + tokens: { + access_token: "native-test-access", + account_id: "native-test-account", + }, + }), + "utf8", + ); + saveConfig(configWithNativeOpenAi()); + + const server = startServer(0); + const originalFetch = globalThis.fetch; + let upstreamRequest: { url: string; headers: Headers } | null = null; + const mockFetch: typeof fetch = async (input, init) => { + const target = String(input); + if (target.startsWith("https://chatgpt.com/backend-api/codex/models")) { + upstreamRequest = { url: target, headers: new Headers(init?.headers) }; + return new Response(JSON.stringify({ + models: [{ + slug: "gpt-6.1-sol", + display_name: "GPT-6.1 Sol", + description: "Live native model", + base_instructions: "Live native instructions.", + shell_type: "shell_command", + visibility: "list", + priority: 1, + supported_in_api: true, + default_reasoning_level: "high", + supported_reasoning_levels: [ + { effort: "high", description: "High reasoning" }, + ], + context_window: 400_000, + input_modalities: ["text", "image"], + }], + }), { status: 200, headers: { "content-type": "application/json" } }); + } + return originalFetch(input, init); + }; + globalThis.fetch = mockFetch; + + try { + const response = await originalFetch( + new URL("/v1/models?client_version=9.9.9", server.url), + ); + expect(response.status).toBe(200); + const json: { models?: Array> } = await response.json(); + const live = json.models?.find(model => model.slug === "gpt-6.1-sol"); + expect(live?.display_name).toBe("GPT-6.1 Sol"); + expect(live?.context_window).toBe(400_000); + expect(live?.supported_reasoning_levels).toEqual([ + { effort: "high", description: "High reasoning" }, + ]); + + if (!upstreamRequest) throw new Error("expected native upstream catalog request"); + const upstreamUrl = new URL(upstreamRequest.url); + expect(upstreamUrl.searchParams.get("client_version")).toBe("9.9.9"); + expect(upstreamRequest.headers.get("authorization")).toBe( + "Bearer native-test-access", + ); + expect(upstreamRequest.headers.get("chatgpt-account-id")).toBe( + "native-test-account", + ); + } finally { + globalThis.fetch = originalFetch; + server.stop(true); + } +}); From cb1ef62613f558f0e12f95099b5fe4a4cc4e1dad Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 4 Oct 2026 00:21:44 +0200 Subject: [PATCH 21/22] test: type-check native merge arguments as a tuple --- tests/codex-native-model-discovery.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/codex-native-model-discovery.test.ts b/tests/codex-native-model-discovery.test.ts index 432e2a52..aaccf5fd 100644 --- a/tests/codex-native-model-discovery.test.ts +++ b/tests/codex-native-model-discovery.test.ts @@ -220,7 +220,7 @@ describe("live native OpenAI catalog discovery", () => { }); test("authoritative live native rows survive the static native whitelist unchanged", () => { - const result = mergeCatalogEntriesForSync( + const args: Parameters = [ [liveNative], [], new Map(), @@ -235,7 +235,8 @@ describe("live native OpenAI catalog discovery", () => { false, true, new Set(["gpt-6.1-sol"]), - ); + ]; + const result = mergeCatalogEntriesForSync(...args); const row = result.find(entry => entry.slug === "gpt-6.1-sol"); expect(row).toBeDefined(); From cede3aeb3888b226176b6e79e495bb1456412dad Mon Sep 17 00:00:00 2001 From: OnlineChef Date: Sun, 4 Oct 2026 00:23:26 +0200 Subject: [PATCH 22/22] refactor: validate native model ids without control regex --- src/codex/catalog/native-discovery.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/codex/catalog/native-discovery.ts b/src/codex/catalog/native-discovery.ts index 67a5d081..ef598e9d 100644 --- a/src/codex/catalog/native-discovery.ts +++ b/src/codex/catalog/native-discovery.ts @@ -15,7 +15,22 @@ import { resolveCodexRuntime } from "../runtime"; import type { RawEntry } from "./parsing"; const NATIVE_MODELS_ENDPOINT = "https://chatgpt.com/backend-api/codex/models"; -const NATIVE_MODEL_ID_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/; + +function hasNativeModelIdControlChars(value: string): boolean { + for (const char of value) { + const code = char.codePointAt(0); + if (code === undefined) continue; + if ( + code <= 0x1f + || (code >= 0x7f && code <= 0x9f) + || code === 0x2028 + || code === 0x2029 + ) { + return true; + } + } + return false; +} type NativeCredential = { accessToken: string; @@ -111,7 +126,7 @@ function validatedNativeModels(value: unknown): RawEntry[] | null { || !slug || slug !== slug.trim() || slug.length > MODEL_DISCOVERY_MAX_MODEL_ID_LENGTH - || NATIVE_MODEL_ID_CONTROL_CHARS.test(slug) + || hasNativeModelIdControlChars(slug) ) { return null; }