Skip to content

Commit f19a73c

Browse files
authored
fix(catalog): omit bare OpenAI models without openai provider (lidge-jun#643)
Fixes lidge-jun#636. Skip bare gpt-*/native OpenAI catalog rows when no enabled canonical openai provider exists; make NoEnabledOpenAiProviderError actionable.
1 parent e6169b0 commit f19a73c

3 files changed

Lines changed: 24 additions & 6 deletions

File tree

src/codex/catalog/sync.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -329,9 +329,11 @@ export function mergeCatalogEntriesForSync(
329329
multiAgentMode: MultiAgentMode = "default",
330330
exactComboSlugs: ReadonlySet<string> = new Set(),
331331
hasPhysicalComboProvider = false,
332+
includeNativeOpenAi = true,
332333
): RawEntry[] {
333334
const rank = new Map(featured.map((slug, i) => [slug, i] as const));
334-
const native = catalogModels
335+
const native = includeNativeOpenAi
336+
? catalogModels
335337
.filter(m => typeof m.slug === "string"
336338
&& !(m.slug as string).includes("/")
337339
&& m.owned_by !== COMBO_NAMESPACE
@@ -367,11 +369,14 @@ export function mergeCatalogEntriesForSync(
367369
// for subagent max spawns; wire-clamped to the model's real top rung).
368370
if (!isGpt56NativeSlug(slug)) ensureUltraReasoningLevel(preserved);
369371
return preserved;
370-
});
372+
})
373+
: [];
371374

372375
// Backfill any native OpenAI slug that the on-disk catalog is missing (e.g. gpt-5.5), so a
373376
// routed provider exposing the same id can never delete the native OpenAI/Codex base row.
377+
// Skip when no enabled canonical openai provider exists (#636) — bare gpt-* would 404.
374378
const nativeSlugs = new Set(native.flatMap(m => typeof m.slug === "string" ? [m.slug] : []));
379+
if (includeNativeOpenAi) {
375380
for (const slug of nativeOpenAiSlugs()) {
376381
if (nativeSlugs.has(slug)) continue;
377382
nativeSlugs.add(slug);
@@ -382,6 +387,7 @@ export function mergeCatalogEntriesForSync(
382387
: 9;
383388
native.push(deriveEntry(template ? JSON.parse(JSON.stringify(template)) : null, slug, "OpenAI native model (Codex OAuth passthrough).", priority));
384389
}
390+
}
385391

386392
const freshSlugs = new Set(
387393
routedEntries.flatMap(entry => typeof entry.slug === "string" ? [entry.slug] : []),
@@ -497,7 +503,16 @@ export async function syncCatalogModels(config: OcxConfig): Promise<{
497503
// native AND routed so the advertised flag matches the implemented endpoint (phase 120.4) and a
498504
// native template can never leak supports_websockets while the flag is off.
499505
const wsEnabled = websocketsEnabled(config);
500-
catalog.models = mergeCatalogEntriesForSync(catalog.models ?? [], goEntries, baseline, featured, wsEnabled, goIds, template, disabledNativeSlugs(config), gatheredProviderNames, multiAgentMode, exactComboSlugs, hasPhysicalComboProvider);
506+
const enabledProviders = Object.entries(config.providers ?? {})
507+
.filter(([, prov]) => prov.disabled !== true);
508+
const hasCanonicalOpenai = enabledProviders.some(([name, prov]) =>
509+
name === "openai" && isCanonicalOpenAiForwardProvider(prov),
510+
);
511+
// #636: when the user only configured non-OpenAI providers (e.g. kimi), do not advertise
512+
// bare gpt-* rows that hard-404 via NoEnabledOpenAiProviderError. Keep natives when no
513+
// providers are configured yet (fresh install / catalog bootstrap tests).
514+
const includeNativeOpenAi = enabledProviders.length === 0 || hasCanonicalOpenai;
515+
catalog.models = mergeCatalogEntriesForSync(catalog.models ?? [], goEntries, baseline, featured, wsEnabled, goIds, template, disabledNativeSlugs(config), gatheredProviderNames, multiAgentMode, exactComboSlugs, hasPhysicalComboProvider, includeNativeOpenAi);
501516
clampCatalogModelsToCodexSupport(catalog.models);
502517

503518
atomicWriteFile(catalogPath, JSON.stringify(catalog, null, 2) + "\n");

src/router.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,10 @@ function activeProviderEntries(config: OcxConfig): [string, OcxProviderConfig][]
293293

294294
export class NoEnabledOpenAiProviderError extends Error {
295295
constructor(modelId: string) {
296-
super(`No enabled OpenAI provider for model: ${modelId}. Run 'ocx init' to configure a provider, or check that your config has an enabled 'openai' provider.`);
296+
super(
297+
`Model ${modelId} requires the canonical openai provider. `
298+
+ `Run: ocx provider add openai && ocx sync && ocx restart`,
299+
);
297300
this.name = "NoEnabledOpenAiProviderError";
298301
}
299302
}

tests/router.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,9 @@ describe("routeModel registry effort defaults", () => {
142142
expect(routeModel({ ...base, providers: { ...base.providers, openai: { ...forward, codexAccountMode: "direct" } } }, "gpt-5.5"))
143143
.toMatchObject({ providerName: "openai", codexAccountMode: "direct" });
144144
expect(() => routeModel({ ...base, providers: { ...base.providers, openai: { ...forward, disabled: true } } }, "gpt-5.5"))
145-
.toThrow(NoEnabledOpenAiProviderError);
145+
.toThrow(/requires the canonical openai provider/);
146146
const unavailable = { ...base, providers: { "openai-proxy": base.providers["openai-proxy"] } };
147-
expect(() => routeModel(unavailable, "gpt-5.5")).toThrow(NoEnabledOpenAiProviderError);
147+
expect(() => routeModel(unavailable, "gpt-5.5")).toThrow(/ocx provider add openai/);
148148
});
149149

150150
test("rejects legacy chatgpt namespaces even when configured", () => {

0 commit comments

Comments
 (0)