Merge PR #8929: fix(catalog): register plain Codex route for worker -wm SKUs (@STRML)

This commit is contained in:
can1357
2026-08-19 01:39:18 +02:00
2 changed files with 264 additions and 24 deletions
+118 -24
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,29 @@ 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. Both listings derive their base-model
* metadata (1M-window floor, daybreak pricing, context fallback) from the
* canonical plain slug — the suffix is a routing variant, not a different
* model, so the `-wm` row no longer keeps stale backend-parsed capability
* values while its plain listing is enriched.
*
* Deliberate boundary: the "safe" gate is the bundled Codex catalog. A `-wm`
* slug whose plain counterpart is only a user-local models.yml entry (not
* bundled) stays verbatim — authoritative discovery for genuinely distinct
* `-wm` SKUs is preserved, and a hidden plain backend entry can be re-surfaced
* through its advertised `-wm` row because the configured plain slug must
* resolve.
*/
const CODEX_WORKER_SUFFIX = "-wm";
const CODEX_REMOTE_COMPACTION = {
enabled: true,
api: "openai-codex-responses",
@@ -194,11 +218,30 @@ 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. Both listings of a safe `-wm` model carry the same base-model
// metadata (context-window floor, daybreak pricing) derived from the
// canonical plain slug — the suffix is a routing variant, not a different
// model.
const advertisedSlugs = new Set(parsedEntries.map(parsed => parsed.slug));
const bundledCodexModelIds = getBundledCodexModelIds();
const normalized: NormalizedCodexModel[] = [];
for (const parsed of parsedEntries) {
const canonicalSlug = plainCounterpartForWorkerSlug(parsed.slug, bundledCodexModelIds) ?? parsed.slug;
normalized.push(buildNormalizedCodexModel(parsed, parsed.slug, canonicalSlug, baseUrl));
const plainSlug = canonicalSlug !== parsed.slug ? canonicalSlug : null;
if (plainSlug && !advertisedSlugs.has(plainSlug)) {
normalized.push(buildNormalizedCodexModel(parsed, plainSlug, canonicalSlug, baseUrl));
}
}
@@ -212,7 +255,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 +302,65 @@ 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,
};
}
/**
* Build a normalized Codex model spec. `slug` is the registered id (either the
* advertised slug or a synthesized plain counterpart); `canonicalSlug` names
* the model's bundled SKU (`slug` itself for plain/unknown rows, the plain
* counterpart for a safe `-wm` row) and owns the base-model metadata derivation
* so both listings of a model report the same context window and pricing.
*/
function buildNormalizedCodexModel(
parsed: ParsedCodexModelEntry,
slug: string,
canonicalSlug: 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. Keyed on the
// canonical slug so a safe `gpt-5.6-luna-wm` row gets the same floor as its
// plain listing.
const parsedKnown = parseKnownModel(canonicalSlug);
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 contextWindow = CODEX_GPT_5_6_1M_SLUGS.has(slug)
const reportedContextWindow = parsed.contextWindow ?? fallbackContextWindow;
const contextWindow = CODEX_GPT_5_6_1M_SLUGS.has(canonicalSlug)
? 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);
const daybreakCost = resolveOpenAIDaybreakStandardCost(canonicalSlug);
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 } : {}),
},
};
}
@@ -11,6 +11,7 @@ import { resolveProviderModels } from "@oh-my-pi/pi-catalog/model-manager";
import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
import { openaiCodexModelManagerOptions } from "@oh-my-pi/pi-catalog/provider-models/special";
import type { ModelSpec } from "@oh-my-pi/pi-catalog/types";
import { resolveProviderModelReference } from "@oh-my-pi/pi-coding-agent/config/model-resolver";
describe("Codex model discovery", () => {
it("marks discovered models for provider-native V2 compaction", async () => {
@@ -538,4 +539,149 @@ 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…
const workerModel = result?.models.find(model => model.id === "gpt-5.6-luna-wm");
expect(workerModel).toBeDefined();
// …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();
expect(plainModel?.provider).toBe("openai-codex");
// Both rows are the same model: the worker variant shares the plain
// SKU's base metadata, so the 1M window floor applies to both.
expect(workerModel?.contextWindow).toBe(1_000_000);
expect(plainModel?.contextWindow).toBe(1_000_000);
});
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 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,
});
// No artificial static input: the bundled Codex catalog is the real
// gate that licenses the plain-route synthesis.
const result = await resolveProviderModels(
{ ...options, cacheDbPath: path.join(tempDir, "models.db") },
"online",
);
const ids = result.models.map(model => model.id);
expect(ids).toContain("gpt-5.6-luna");
expect(ids).toContain("gpt-5.6-luna-wm");
// Same engine the runtime uses: resolving the configured
// `openai-codex/gpt-5.6-luna` must bind to the plain route by exact
// id, not fall through to the `-wm` fuzzy match.
const resolved = resolveProviderModelReference("openai-codex", "gpt-5.6-luna", result.models);
expect(resolved?.id).toBe("gpt-5.6-luna");
expect(resolved?.provider).toBe("openai-codex");
// An explicitly configured worker slug still resolves verbatim.
const resolvedWm = resolveProviderModelReference("openai-codex", "gpt-5.6-luna-wm", result.models);
expect(resolvedWm?.id).toBe("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"]);
});
});