Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 3 additions & 4 deletions packages/first-class-providers/src/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,9 @@ export const OPENAI_API_MAX_COMPLETION_TOKENS_MODELS: readonly string[] = [
* Tier A order: dual-path OpenAI, OAuth xAI, Go/Zen, Z.AI, big three, Custom.
*
* Canonical static registry for provider identity: base URLs, model lists,
* and defaults live here. The Codex/xAI OAuth fallbacks stay separate — they
* back live catalog calls, not static identity — but must agree with this
* registry; src/provider/identity-divergence.test.ts pins that. Do not add
* another edge list: reference this registry or the fallbacks.
* and defaults live here. The Codex/xAI OAuth model lists stay separate, since
* those sign-ins serve their own model sets and defaults. Do not add another
* edge list: reference this registry or those lists.
*/
export const FIRST_CLASS_PROVIDERS: readonly FirstClassProviderDef[] = [
{
Expand Down
27 changes: 27 additions & 0 deletions src/auth/codex/constants.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, expect, test } from "bun:test";

import { CODEX_DEFAULT_MODELS } from "./constants.js";

describe("CODEX_DEFAULT_MODELS", () => {
// Recorded from GET /codex/models for a ChatGPT Plus account (client_version
// 0.165.0, 2026-11-01). Nothing else pins the order or the default: the other
// tests read the constant.
test("follows the order the Codex backend listed the models in", () => {
expect([...CODEX_DEFAULT_MODELS]).toEqual([
"gpt-6.1-sol",
"gpt-6-astra",
"gpt-6.1-terra",
"gpt-6-sol",
"gpt-6-luna",
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",
]);
});

test("leaves out the models OpenAI retired for ChatGPT sign-in", () => {
for (const retired of ["gpt-5.4", "gpt-5.4-mini", "gpt-5.5"]) {
expect(CODEX_DEFAULT_MODELS as readonly string[]).not.toContain(retired);
}
});
});
21 changes: 11 additions & 10 deletions src/auth/codex/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,20 @@ export const CODEX_CLIENT_VERSION = "0.50.0";
// the public Codex CLI originator.
export const CODEX_ORIGINATOR = "codex_cli_rs";

// Fallback model list, used only when the live catalog (GET /codex/models) is
// unavailable — rate-limited it returns an empty list. The backend rotates its
// serving set, so the live fetch is authoritative and these are just a
// current-generation default so the picker is never empty. The first entry
// doubles as the ChatGPT-OAuth default: it must stay the model shared with the
// OpenAI API-key path's default in FIRST_CLASS_PROVIDERS (gpt-5.4) so both
// auth paths serving OpenAI agree. identity-divergence.test.ts pins this.
// The models a ChatGPT sign-in offers, in the order GET /codex/models listed
// them for a ChatGPT Plus account (client_version 0.165.0, 2026-11-01); the
// first entry is the ChatGPT default. Availability varies by plan and rollout,
// so other accounts may be served a different set. gpt-5.4, gpt-5.4-mini and
// gpt-5.5 are left out because OpenAI retired them from Codex with ChatGPT
// sign-in. API-key sessions are not affected, so the API-key path keeps its
// own list and default.
export const CODEX_DEFAULT_MODELS = [
"gpt-5.4",
"gpt-5.5",
"gpt-6.1-sol",
"gpt-6-astra",
"gpt-6.1-terra",
"gpt-6-sol",
"gpt-6-luna",
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",
"gpt-5.4-mini",
] as const;
4 changes: 2 additions & 2 deletions src/config/codex-providers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ describe("codex provider naming", () => {
});
});

// The default-model agreement is pinned without literals in
// provider/identity-divergence.test.ts — no second literal re-pin here.
// The projected models and default are pinned against CODEX_DEFAULT_MODELS in
// provider/identity-divergence.test.ts; no literal re-pin here.
describe("codexProvidersAsSettings", () => {
test("projects profiles into provider settings seeded with the access token", () => {
const profiles: CodexProfile[] = [
Expand Down
14 changes: 0 additions & 14 deletions src/provider/identity-divergence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,20 +56,6 @@ describe("provider identity divergence", () => {
resetGoModelDiscoveryForTests();
});

test("OpenAI API-key default and ChatGPT-OAuth fallback default agree", () => {
const apiPath = firstClassProviderById("openai")?.paths?.find(
(p) => p.id === "api",
);
if (apiPath?.defaultModel === undefined)
throw new Error("Expected the OpenAI API path to declare a defaultModel");
// Both auth paths serve OpenAI models, so the shared model must lead both
// lists — the fallback default is models[0] by construction.
const codexFallbacks: readonly string[] = CODEX_DEFAULT_MODELS;
const codexDefault: string | undefined = codexFallbacks[0];
expect(codexDefault).toBe(apiPath.defaultModel);
expect(codexFallbacks).toContain(apiPath.defaultModel);
});

test("OpenAI API-key entry is self-consistent", () => {
const apiPath = firstClassProviderById("openai")?.paths?.find(
(p) => p.id === "api",
Expand Down
56 changes: 39 additions & 17 deletions src/provider/reasoning-effort.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { afterEach, describe, test, expect } from "bun:test";
import { CODEX_DEFAULT_MODELS } from "../auth/codex/constants.js";
import {
isReasoningEffort,
normalizeProviderEfforts,
Expand Down Expand Up @@ -54,32 +55,53 @@ describe("supportedEfforts", () => {
]);
});

test("gpt-6-astra takes low through max on both API and Codex paths", () => {
test("gpt-6-astra takes low through max on the API path", () => {
expect(supportedEfforts("gpt-6-astra")).toEqual([
"low",
"medium",
"high",
"xhigh",
"max",
]);
expect(supportedEfforts("gpt-6-astra", undefined, true)).toEqual([
"low",
"medium",
"high",
"xhigh",
"max",
]);
});

test("gpt-5.6 family additionally takes max and ultra on the codex backend", () => {
expect(supportedEfforts("gpt-5.6-sol", undefined, true)).toEqual([
"low",
"medium",
"high",
"xhigh",
"max",
"ultra",
]);
test("ChatGPT models the backend serves up to ultra offer max and ultra", () => {
for (const model of [
"gpt-6.1-sol",
"gpt-6-astra",
"gpt-6.1-terra",
"gpt-6-sol",
"gpt-5.6-sol",
"gpt-5.6-terra",
]) {
expect(supportedEfforts(model, undefined, true)).toEqual([
"low",
"medium",
"high",
"xhigh",
"max",
"ultra",
]);
}
});

test("ChatGPT Luna models stop at max", () => {
for (const model of ["gpt-6-luna", "gpt-5.6-luna"]) {
const efforts = supportedEfforts(model, undefined, true);
expect(efforts).toEqual(["low", "medium", "high", "xhigh", "max"]);
expect(efforts).not.toContain("ultra");
}
});

test("every built-in ChatGPT model has an assigned effort ladder", () => {
// A model added to the fallback list without a ladder would silently
// stop at xhigh. A model the backend really serves only up to xhigh
// belongs here, so adding one is a decision rather than an accident.
const servedOnlyUpToXhigh: readonly string[] = [];
for (const model of CODEX_DEFAULT_MODELS) {
if (servedOnlyUpToXhigh.includes(model)) continue;
expect(supportedEfforts(model, undefined, true)).toContain("max");
}
});

test("the model name alone does not imply codex levels", () => {
Expand Down
34 changes: 24 additions & 10 deletions src/provider/reasoning-effort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,30 @@ const FULL_EFFORT_MODELS: readonly string[] = [
"gpt-5.1-codex-max",
];

// Codex backend models take low/medium/high/xhigh — no `minimal`, no `none`.
// ChatGPT (Codex backend) models not listed below: low/medium/high/xhigh,
// no `minimal`, no `none`.
const CODEX_EFFORTS: readonly ReasoningEffort[] = [
"low",
"medium",
"high",
"xhigh",
];

// gpt-5.6 family adds `max` and `ultra`; older Codex models do not support
// them.
const MAX_EFFORT_CODEX_MODELS: readonly string[] = [
// Per-model levels the Codex backend lists (`supported_reasoning_levels` in
// GET /codex/models, client_version 0.165.0, 2026-10-28). Exact ids rather
// than a name rule: an id not seen yet gets the conservative fallback above
// instead of a guessed tier, because the adapter does not clamp and a level
// the backend does not serve breaks the turn.
const ULTRA_EFFORT_CODEX_MODELS: readonly string[] = [
"gpt-6.1-sol",
"gpt-6-astra",
"gpt-6.1-terra",
"gpt-6-sol",
"gpt-5.6-sol",
"gpt-5.6-terra",
];
const MAX_EFFORT_CODEX_MODELS: readonly string[] = [
"gpt-6-luna",
"gpt-5.6-luna",
];

Expand Down Expand Up @@ -96,8 +107,8 @@ function isMuseSparkModel(model: string): boolean {
const GROK_46_AND_47_EFFORTS: readonly ReasoningEffort[] = CODEX_EFFORTS;
const GROK_46_AND_47_MODELS: readonly string[] = ["grok-4.6", "grok-4.7"];

// GPT-6 Astra accepts low–max on both surfaces; not ultra (gpt-5.6
// Codex-only), minimal, or none.
// GPT-6 Astra on the API path accepts low–max; not ultra, minimal, or none.
// On ChatGPT sign-in it follows the served-levels lists above.
const GPT6_ASTRA_EFFORTS: readonly ReasoningEffort[] = [
"low",
"medium",
Expand Down Expand Up @@ -152,14 +163,17 @@ export function supportedEfforts(
if (providerEfforts.length > 0) {
return [...providerEfforts];
}
if (model === "gpt-6-astra") {
return [...GPT6_ASTRA_EFFORTS];
}
if (isCodex) {
if (ULTRA_EFFORT_CODEX_MODELS.includes(model)) {
return [...CODEX_EFFORTS, "max", "ultra"];
}
return MAX_EFFORT_CODEX_MODELS.includes(model)
? [...CODEX_EFFORTS, "max", "ultra"]
? [...CODEX_EFFORTS, "max"]
: [...CODEX_EFFORTS];
}
if (model === "gpt-6-astra") {
return [...GPT6_ASTRA_EFFORTS];
}
if (FULL_EFFORT_MODELS.includes(model)) {
return ["none", ...DEFAULT_EFFORTS, "xhigh"];
}
Expand Down
Loading