fix(catalog): preserved codex discovery token fallback

Keep the bearer resolved by the discovery preflight when it is not among the
stored OAuth account resolutions. This preserves Codex discovery for env,
runtime/config override, and stored non-OAuth credential sources while still
unioning all configured OAuth account catalogs.

Add a ModelRegistry regression test that drives runtime-key discovery and
asserts the resolved bearer reaches the Codex models endpoint.

Fixes #6265
This commit is contained in:
roboomp
2026-07-22 07:08:22 +00:00
parent b9dd5bce8a
commit 2eff59e060
2 changed files with 42 additions and 3 deletions
@@ -434,13 +434,22 @@ function getOAuthCredentialsForProvider(authStorage: AuthStorage, provider: stri
* must fetch per account and union the results; resolving a single access token
* (as before) hid models available only through a sibling account (#6265).
*/
async function resolveCodexDiscoveryAccounts(authStorage: AuthStorage): Promise<OpenAICodexAccount[]> {
async function resolveCodexDiscoveryAccounts(
authStorage: AuthStorage,
resolvedAccessToken: string,
): Promise<OpenAICodexAccount[]> {
const accesses = await authStorage.getOAuthAccesses("openai-codex");
const accounts: OpenAICodexAccount[] = [];
for (const access of accesses) {
if (!access.ok) continue;
accounts.push({ accessToken: access.accessToken, accountId: access.accountId });
}
if (!accounts.some(account => account.accessToken === resolvedAccessToken)) {
const matchingCredential = getOAuthCredentialsForProvider(authStorage, "openai-codex").find(
credential => credential.access === resolvedAccessToken,
);
accounts.push({ accessToken: resolvedAccessToken, accountId: matchingCredential?.accountId });
}
return accounts;
}
@@ -1724,9 +1733,9 @@ export class ModelRegistry {
providerId: "openai-codex",
authoritative: true,
resolveKey: value => value,
createOptions: () =>
createOptions: accessToken =>
openaiCodexModelManagerOptions({
resolveAccounts: () => resolveCodexDiscoveryAccounts(this.authStorage),
resolveAccounts: () => resolveCodexDiscoveryAccounts(this.authStorage, accessToken),
fetch: this.#fetch,
}),
},
@@ -327,6 +327,36 @@ describe("ModelRegistry runtime discovery", () => {
expect(registry.find("openai-codex", "gpt-5.4-nano")).toBeUndefined();
});
test("Codex discovery falls back to a resolved non-OAuth token when no OAuth accounts exist", async () => {
authStorage.setRuntimeApiKey("openai-codex", "runtime-openai-codex");
let modelListCalls = 0;
const fetchMock: FetchImpl = async (input, init) => {
const url = String(input);
if (url.startsWith("https://chatgpt.com/backend-api") && url.includes("/models")) {
modelListCalls++;
expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer runtime-openai-codex");
return Response.json({
models: [
{
slug: "runtime-codex-model",
display_name: "Runtime Codex Model",
context_window: 128_000,
supported_in_api: true,
input_modalities: ["text"],
},
],
});
}
throw new Error(`Unexpected URL: ${url}`);
};
const registry = new ModelRegistry(authStorage, modelsJsonPath, { fetch: fetchMock });
await registry.refreshProvider("openai-codex", "online");
expect(modelListCalls).toBe(1);
expect(registry.find("openai-codex", "runtime-codex-model")).toBeDefined();
});
test("configured discovery suppresses built-in special OAuth discovery", async () => {
await authStorage.set("google-gemini-cli", {
type: "oauth",