Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7d49439
test: cover account-aware native model discovery
ChefGroep Oct 3, 2026
89e8478
test: require curated provider expansion
ChefGroep Oct 3, 2026
1bce8f1
feat: discover native models with routed Codex account
ChefGroep Oct 3, 2026
909cce8
feat: admit authoritative live native model slugs
ChefGroep Oct 3, 2026
d471a56
feat: merge live native catalog without static whitelist loss
ChefGroep Oct 3, 2026
90dc2f0
feat: promote five OpenAI-compatible providers
ChefGroep Oct 3, 2026
5d4d088
fix: align promoted provider directory endpoints
ChefGroep Oct 3, 2026
f0410bd
fix: bound native catalog discovery requests
ChefGroep Oct 3, 2026
177b52f
test: include Cohere in free-tier parity
ChefGroep Oct 3, 2026
f49a348
test: cover bounded account fallback discovery
ChefGroep Oct 3, 2026
f4f653f
fix: bound native discovery across account fallbacks
ChefGroep Oct 3, 2026
400a8cf
test: respect standalone account-pool opt-out
ChefGroep Oct 3, 2026
a29e520
fix: honor standalone pool disable in discovery
ChefGroep Oct 3, 2026
486fbb2
test: cover native catalog body read failure
ChefGroep Oct 3, 2026
72820b9
fix: degrade on native catalog body failures
ChefGroep Oct 3, 2026
223c215
refactor: keep native discovery result typed
ChefGroep Oct 3, 2026
5b78bbd
test: preserve live native metadata in catalog builder
ChefGroep Oct 3, 2026
154f8d2
feat: preserve live native metadata in catalog builder
ChefGroep Oct 3, 2026
af26296
fix: serve live native models to Codex clients
ChefGroep Oct 3, 2026
f76aff8
test: verify live native Codex catalog route
ChefGroep Oct 3, 2026
cb1ef62
test: type-check native merge arguments as a tuple
ChefGroep Oct 3, 2026
cede3ae
refactor: validate native model ids without control regex
ChefGroep Oct 3, 2026
d72d477
chore(stack): merge #296 prompt-cache observability into #297
ChefGroep Oct 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/codex/catalog/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,18 @@ export function nativeModelRows(config: Pick<OcxConfig, "disabledModels">): Arra
});
}

export function applyNativeVisibility(entries: RawEntry[], disabledNative: Set<string>): RawEntry[] {
export function applyNativeVisibility(
entries: RawEntry[],
disabledNative: Set<string>,
authoritativeNativeSlugs: ReadonlySet<string> = 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;
Expand Down
251 changes: 251 additions & 0 deletions src/codex/catalog/native-discovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import type { OcxConfig } from "../../types";
import {
MODEL_DISCOVERY_MAX_MODEL_ID_LENGTH,
MODEL_DISCOVERY_MAX_MODELS,
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";
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";

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;
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<NativePoolToken>;
resolveClientVersion?: () => string | null;
}

type CredentialCandidate =
| { kind: "main" }
| { kind: "pool"; id: string };

function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}

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.codexAccountPools === false
? []
: (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<Pick<
NativeOpenAiCatalogDiscoveryDeps,
"getMainAccountToken" | "getValidCodexToken"
>>,
): Promise<NativeCredential | null> {
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<string>();
for (const raw of envelope.rows) {
if (!isRecord(raw)) return null;
const slug = raw.slug;
if (
typeof slug !== "string"
|| !slug
|| slug !== slug.trim()
|| slug.length > MODEL_DISCOVERY_MAX_MODEL_ID_LENGTH
|| hasNativeModelIdControlChars(slug)
) {
return null;
}
if (seen.has(slug)) continue;
seen.add(slug);
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;
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<NativeOpenAiCatalogDiscovery> {
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),
);
// 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;

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,
},
signal: requestSignal,
});
} catch {
continue;
}

if (!response.ok) {
try {
void response.body?.cancel().catch(() => undefined);
} catch {
// Best-effort body cleanup only.
}
continue;
}

let parsed: BoundedDiscoveryJsonResult;
try {
parsed = await readBoundedDiscoveryJson(
response,
MODEL_DISCOVERY_MAX_RESPONSE_BYTES,
);
} catch {
continue;
}
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<string, RawEntry>();
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 => {
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()];
}
Loading
Loading