fix(catalog): register plain Codex route for worker -wm SKUs

Codex backend discovery advertises worker-mode SKUs under a `-wm` suffix (gpt-5.6-luna-wm). Authoritative discovery replaced the bundled catalog and kept those slugs verbatim, so a configured `openai-codex/gpt-5.6-luna` vanished from the resolved catalog and the resolver's fuzzy fallback selected `-wm` instead — a route some ChatGPT accounts reject.

Model discovery now recognizes the `-wm` suffix: when the bundled Codex catalog ships the plain SKU, the `-wm` row is also registered under its plain id (re-derived so the 1M-window floor and daybreak pricing keyed on that slug still apply). Unknown `-wm` SKUs keep their authoritative verbatim slug and non-worker models are untouched, so distinct models and other providers are unaffected.
This commit is contained in:
Samuel Reed
2026-08-18 16:23:00 -04:00
parent 8500092296
commit 51fd17a8c3
2 changed files with 236 additions and 22 deletions
+92 -22
View File
@@ -1,5 +1,6 @@
import { type } from "@oh-my-pi/omptype";
import { parseKnownModel, semverEqual } from "../identity/classify";
import { getBundledModels } from "../models";
import { resolveOpenAIDaybreakStandardCost } from "../openai-pricing";
import type { FetchImpl, ModelSpec } from "../types";
import { discoveryFetch } from "../utils";
@@ -22,6 +23,21 @@ const GPT_5_6_CONTEXT_WINDOW = 372_000;
*/
const GPT_5_6_1M_CONTEXT_WINDOW = 1_000_000;
const CODEX_GPT_5_6_1M_SLUGS: ReadonlySet<string> = new Set(["gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"]);
/**
* Codex advertises worker-mode SKUs under a `-wm` suffix (`gpt-5.6-luna-wm`).
*
* Those rows route through the same Codex backend as their plain SKU, but an
* authoritative discovery list that only advertises the `-wm` slug prunes the
* bundled plain model, leaving a configured `openai-codex/gpt-5.6-luna`
* unresolvable except via fuzzy fallback onto the `-wm` row — which this user's
* ChatGPT account rejects. The compatibility rule, scoped to Codex discovery:
* a `-wm` slug whose plain counterpart exists in the bundled Codex catalog is
* ALSO registered under its plain id (re-derived, not cloned, so the 1M-window
* floor and daybreak pricing keyed on the plain slug still apply). Models
* without a bundled plain counterpart stay verbatim, preserving authoritative
* discovery for genuinely distinct `-wm` SKUs.
*/
const CODEX_WORKER_SUFFIX = "-wm";
const CODEX_REMOTE_COMPACTION = {
enabled: true,
api: "openai-codex-responses",
@@ -194,11 +210,26 @@ function normalizeCodexModels(payload: unknown, baseUrl: string): ModelSpec<"ope
}
const entries = parsedResponse.models ?? parsedResponse.data ?? [];
const normalized: NormalizedCodexModel[] = [];
const parsedEntries: ParsedCodexModelEntry[] = [];
for (const entry of entries) {
const model = normalizeCodexModelEntry(entry, baseUrl);
if (model) {
normalized.push(model);
const parsed = parseCodexModelEntry(entry);
if (parsed) {
parsedEntries.push(parsed);
}
}
// A worker `-wm` slug gets an extra plain-id route only when the bundled
// catalog ships the plain SKU (the "safe" precondition); the backend's own
// plain slug wins over any synthesized clone, and unknown `-wm` SKUs stay
// verbatim.
const advertisedSlugs = new Set(parsedEntries.map(parsed => parsed.slug));
const bundledCodexModelIds = getBundledCodexModelIds();
const normalized: NormalizedCodexModel[] = [];
for (const parsed of parsedEntries) {
normalized.push(buildNormalizedCodexModel(parsed, parsed.slug, baseUrl));
const plainSlug = plainCounterpartForWorkerSlug(parsed.slug, bundledCodexModelIds);
if (plainSlug && !advertisedSlugs.has(plainSlug)) {
normalized.push(buildNormalizedCodexModel(parsed, plainSlug, baseUrl));
}
}
@@ -212,7 +243,37 @@ function normalizeCodexModels(payload: unknown, baseUrl: string): ModelSpec<"ope
return normalized.map(item => item.model);
}
function normalizeCodexModelEntry(entry: unknown, baseUrl: string): NormalizedCodexModel | null {
/** Ids of the bundled Codex catalog, consulted once per discovery run. */
function getBundledCodexModelIds(): ReadonlySet<string> {
const ids = new Set(getBundledModels("openai-codex").map(model => model.id));
return ids;
}
/**
* Map a Codex worker `-wm` slug to its plain counterpart when the bundled
* catalog registers that plain SKU. Returns `null` for non-worker slugs and
* for `-wm` slugs without a safe plain counterpart.
*/
function plainCounterpartForWorkerSlug(slug: string, bundledCodexModelIds: ReadonlySet<string>): string | null {
if (!slug.endsWith(CODEX_WORKER_SUFFIX)) {
return null;
}
const plain = slug.slice(0, -CODEX_WORKER_SUFFIX.length);
return plain.length > 0 && bundledCodexModelIds.has(plain) ? plain : null;
}
interface ParsedCodexModelEntry {
slug: string;
name: string;
contextWindow: number | null;
reasoning: boolean;
input: ("text" | "image")[];
preferWebsockets: boolean;
useResponsesLite: boolean;
priority: number;
}
function parseCodexModelEntry(entry: unknown): ParsedCodexModelEntry | null {
const parsedEntry = codexModelEntrySchema(entry);
if (parsedEntry instanceof type.errors) {
return null;
@@ -229,44 +290,53 @@ function normalizeCodexModelEntry(entry: unknown, baseUrl: string): NormalizedCo
return null;
}
const name = toNonEmptyString(payload.display_name) ?? slug;
return {
slug,
name: toNonEmptyString(payload.display_name) ?? slug,
contextWindow: toPositiveInt(payload.context_window),
reasoning: supportsReasoning(payload.default_reasoning_level, payload.supported_reasoning_levels),
input: normalizeInputModalities(payload.input_modalities),
preferWebsockets: toBoolean(payload.prefer_websockets) === true,
useResponsesLite: toBoolean(payload.use_responses_lite) === true,
priority: toFiniteNumber(payload.priority) ?? Number.MAX_SAFE_INTEGER,
};
}
function buildNormalizedCodexModel(parsed: ParsedCodexModelEntry, slug: string, baseUrl: string): NormalizedCodexModel {
// Codex discovery historically omitted `context_window` for GPT-5.6-family
// SKUs (#5705); luna/sol/terra additionally floor the reported value because
// the registry still declares the pre-1M 272000 window.
const parsed = parseKnownModel(slug);
// the registry still declares the pre-1M 272000 window. Re-derived from the
// effective `slug` so a synthesized plain route gets the same treatment as
// one actually advertised under that id.
const parsedKnown = parseKnownModel(slug);
const fallbackContextWindow =
parsed.family === "openai" && semverEqual(parsed.version, "5.6")
parsedKnown.family === "openai" && semverEqual(parsedKnown.version, "5.6")
? GPT_5_6_CONTEXT_WINDOW
: DEFAULT_CONTEXT_WINDOW;
const reportedContextWindow = toPositiveInt(payload.context_window) ?? fallbackContextWindow;
const reportedContextWindow = parsed.contextWindow ?? fallbackContextWindow;
const contextWindow = CODEX_GPT_5_6_1M_SLUGS.has(slug)
? Math.max(reportedContextWindow, GPT_5_6_1M_CONTEXT_WINDOW)
: reportedContextWindow;
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 useResponsesLite = toBoolean(payload.use_responses_lite) === true;
const priority = toFiniteNumber(payload.priority) ?? Number.MAX_SAFE_INTEGER;
const daybreakCost = resolveOpenAIDaybreakStandardCost(slug);
return {
priority,
priority: parsed.priority,
model: {
id: slug,
name,
name: parsed.name,
api: "openai-codex-responses",
provider: "openai-codex",
baseUrl,
reasoning,
input,
reasoning: parsed.reasoning,
input: parsed.input,
cost: daybreakCost ? { ...daybreakCost } : { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
remoteCompaction: CODEX_REMOTE_COMPACTION,
contextWindow,
maxTokens,
...(preferWebsockets ? { preferWebsockets: true } : {}),
...(useResponsesLite ? { useResponsesLite: true } : {}),
...(priority !== Number.MAX_SAFE_INTEGER ? { priority } : {}),
...(parsed.preferWebsockets ? { preferWebsockets: true } : {}),
...(parsed.useResponsesLite ? { useResponsesLite: true } : {}),
...(parsed.priority !== Number.MAX_SAFE_INTEGER ? { priority: parsed.priority } : {}),
},
};
}
@@ -538,4 +538,148 @@ describe("Codex model discovery", () => {
await fs.rm(tempDir, { recursive: true, force: true });
}
});
it("registers a plain route when the backend advertises only the worker `-wm` slug", async () => {
const fetchFn: typeof fetch = Object.assign(
async () =>
new Response(
JSON.stringify({
models: [
{
slug: "gpt-5.6-luna-wm",
display_name: "GPT-5.6 Luna",
context_window: 272_000,
default_reasoning_level: "medium",
supported_reasoning_levels: ["low", "medium", "high"],
input_modalities: ["text", "image"],
supported_in_api: true,
},
],
}),
),
{ preconnect() {} },
);
const result = await fetchCodexModels({
accessToken: "test-token",
baseUrl: "https://codex.example/backend-api",
clientVersion: "0.99.0",
fetchFn,
});
// The authoritative `-wm` row stays surfaced verbatim…
expect(result?.models.some(model => model.id === "gpt-5.6-luna-wm")).toBe(true);
// …and the configured plain slug must also resolve to a real route.
const plainModel = result?.models.find(model => model.id === "gpt-5.6-luna");
expect(plainModel).toBeDefined();
// The plain route is re-derived for the plain slug, so the 1M floor applies.
expect(plainModel?.contextWindow).toBe(1_000_000);
expect(plainModel?.provider).toBe("openai-codex");
});
it("keeps the plain route through authoritative discovery that advertises only the `-wm` slug", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "pi-catalog-codex-luna-wm-"));
const bundledLuna: ModelSpec<"openai-codex-responses"> = {
id: "gpt-5.6-luna",
name: "GPT-5.6 Luna",
api: "openai-codex-responses",
provider: "openai-codex",
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1_000_000,
maxTokens: 128_000,
};
const fetchFn: typeof fetch = Object.assign(
async () =>
new Response(
JSON.stringify({
models: [
{
slug: "gpt-5.6-luna-wm",
display_name: "GPT-5.6 Luna",
default_reasoning_level: "medium",
supported_reasoning_levels: ["low", "medium", "high"],
input_modalities: ["text", "image"],
supported_in_api: true,
},
],
}),
),
{ preconnect() {} },
);
try {
const options = openaiCodexModelManagerOptions({
resolveAccounts: async () => [{ accessToken: "test-token" }],
fetch: fetchFn,
});
const result = await resolveProviderModels(
{ ...options, staticModels: [bundledLuna], cacheDbPath: path.join(tempDir, "models.db") },
"online",
);
const ids = result.models.map(model => model.id);
// The exact-id resolution the resolver performs for
// `openai-codex/gpt-5.6-luna` needs this row present.
expect(ids).toContain("gpt-5.6-luna");
expect(ids).toContain("gpt-5.6-luna-wm");
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
});
it("keeps a `-wm` slug verbatim when it has no bundled plain counterpart", async () => {
const fetchFn: typeof fetch = Object.assign(
async () =>
new Response(
JSON.stringify({
models: [
{
slug: "gpt-9.9-mystery-wm",
display_name: "GPT-9.9 Mystery (worker)",
input_modalities: ["text"],
supported_in_api: true,
},
],
}),
),
{ preconnect() {} },
);
const result = await fetchCodexModels({
accessToken: "test-token",
baseUrl: "https://codex.example/backend-api",
clientVersion: "0.99.0",
fetchFn,
});
// No bundled `gpt-9.9-mystery` entry, so no phantom plain route is made up.
expect(result?.models.map(model => model.id)).toEqual(["gpt-9.9-mystery-wm"]);
});
it("leaves a non-worker slug untouched by the worker-mapping rule", async () => {
const fetchFn: typeof fetch = Object.assign(
async () =>
new Response(
JSON.stringify({
models: [
{
slug: "gpt-5.6-luna",
display_name: "GPT-5.6 Luna",
default_reasoning_level: "medium",
supported_reasoning_levels: ["low", "medium", "high"],
input_modalities: ["text", "image"],
supported_in_api: true,
},
],
}),
),
{ preconnect() {} },
);
const result = await fetchCodexModels({
accessToken: "test-token",
baseUrl: "https://codex.example/backend-api",
clientVersion: "0.99.0",
fetchFn,
});
expect(result?.models.map(model => model.id)).toEqual(["gpt-5.6-luna"]);
});
});