diff --git a/src/commands/doctor-synthetic-allowlist.test.ts b/src/commands/doctor-synthetic-allowlist.test.ts index 3b0b452e146..a5a66717729 100644 --- a/src/commands/doctor-synthetic-allowlist.test.ts +++ b/src/commands/doctor-synthetic-allowlist.test.ts @@ -4,14 +4,14 @@ vi.mock("../agents/model-catalog.js", () => ({ loadModelCatalog: vi.fn(), })); vi.mock("../agents/model-selection.js", async (importOriginal) => { - const actual = (await importOriginal()) as Record; + const actual = await importOriginal(); return { ...actual, buildAllowedModelSet: vi.fn() }; }); vi.mock("../terminal/note.js", () => ({ note: vi.fn() })); -import type { OpenClawConfig } from "../config/config.js"; import { loadModelCatalog } from "../agents/model-catalog.js"; import { buildAllowedModelSet } from "../agents/model-selection.js"; +import type { OpenClawConfig } from "../config/config.js"; import { note } from "../terminal/note.js"; import { noteSyntheticAllowlistGaps } from "./doctor-synthetic-allowlist.js"; @@ -38,6 +38,20 @@ describe("noteSyntheticAllowlistGaps", () => { it("skips when allowlist is empty", async () => { await noteSyntheticAllowlistGaps(makeCfg({})); + expect(mockLoadModelCatalog).not.toHaveBeenCalled(); + expect(mockNote).not.toHaveBeenCalled(); + }); + + it("skips when allowAny is true", async () => { + const catalog = [{ id: "gpt-5.4", name: "gpt-5.4", provider: "openai" }]; + mockLoadModelCatalog.mockResolvedValue(catalog); + mockBuildAllowedModelSet.mockReturnValue({ + allowAny: true, + allowedCatalog: catalog, + allowedKeys: new Set(["openai/gpt-5.4"]), + }); + + await noteSyntheticAllowlistGaps(makeCfg({ "anthropic/claude-opus-4-6": {} })); expect(mockNote).not.toHaveBeenCalled(); }); @@ -69,7 +83,7 @@ describe("noteSyntheticAllowlistGaps", () => { mockLoadModelCatalog.mockResolvedValue(catalog); mockBuildAllowedModelSet.mockReturnValue({ allowAny: false, - allowedCatalog: [catalog[0]!, catalog[3]!], + allowedCatalog: [catalog[0], catalog[3]], allowedKeys: new Set(["openai-codex/gpt-5.4", "anthropic/claude-opus-4-6"]), }); @@ -78,7 +92,7 @@ describe("noteSyntheticAllowlistGaps", () => { ); expect(mockNote).toHaveBeenCalledTimes(1); - const noteText = mockNote.mock.calls[0]![0] as string; + const noteText = mockNote.mock.calls[0][0]; expect(noteText).toContain("2 synthetic model"); expect(noteText).toContain("openai/gpt-5.4"); expect(noteText).toContain("openai/gpt-5.4-pro"); @@ -92,7 +106,7 @@ describe("noteSyntheticAllowlistGaps", () => { mockLoadModelCatalog.mockResolvedValue(catalog); mockBuildAllowedModelSet.mockReturnValue({ allowAny: false, - allowedCatalog: [catalog[1]!], + allowedCatalog: [catalog[1]], allowedKeys: new Set(["anthropic/claude-opus-4-6"]), }); @@ -110,7 +124,7 @@ describe("noteSyntheticAllowlistGaps", () => { }); await noteSyntheticAllowlistGaps(makeCfg({ "anthropic/claude-opus-4-6": {} })); - const noteText = mockNote.mock.calls[0]![0] as string; + const noteText = mockNote.mock.calls[0][0]; expect(noteText).toContain("1 synthetic model available"); expect(noteText).not.toContain("models available"); }); diff --git a/src/commands/doctor-synthetic-allowlist.ts b/src/commands/doctor-synthetic-allowlist.ts index df472bb77fd..f41eb6f17db 100644 --- a/src/commands/doctor-synthetic-allowlist.ts +++ b/src/commands/doctor-synthetic-allowlist.ts @@ -6,11 +6,16 @@ import type { OpenClawConfig } from "../config/config.js"; import { note } from "../terminal/note.js"; /** - * Known providers whose models may appear in the runtime catalog via - * `applySyntheticCatalogFallbacks()`. Only these are checked to avoid - * false positives from Pi SDK registry models. + * The exact provider/id pairs created by `applySyntheticCatalogFallbacks()`. + * Only these are checked to avoid false positives from real catalog models + * that happen to share the same provider. */ -const SYNTHETIC_FALLBACK_PROVIDERS = new Set(["openai", "openai-codex"]); +const SYNTHETIC_FALLBACK_KEYS = new Set([ + "openai/gpt-5.4", + "openai/gpt-5.4-pro", + "openai-codex/gpt-5.4", + "openai-codex/gpt-5.3-codex-spark", +]); /** * Warn when synthetic catalog fallback models are present in the runtime @@ -28,19 +33,22 @@ export async function noteSyntheticAllowlistGaps(cfg: OpenClawConfig): Promise = []; for (const entry of catalog) { - if (!SYNTHETIC_FALLBACK_PROVIDERS.has(entry.provider.toLowerCase())) { + const key = modelKey(entry.provider, entry.id); + if (!SYNTHETIC_FALLBACK_KEYS.has(key)) { continue; } - const key = modelKey(entry.provider, entry.id); if (!allowedKeys.has(key)) { gaps.push(entry); }