fix: restrict gap check to exact synthetic fallback IDs and guard allowAny

- Replace broad provider-based check with exact SYNTHETIC_FALLBACK_KEYS
  set matching the 4 IDs from applySyntheticCatalogFallbacks(). Prevents
  false positives for real catalog models from openai/openai-codex.
- Add early return when buildAllowedModelSet returns allowAny: true.
- Add missing mockLoadModelCatalog assertion in empty-allowlist test.
- Add test for allowAny: true path.

Addresses review feedback from chatgpt-codex-connector[bot] and
greptile-apps[bot].

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio 2026-03-15 16:47:17 -05:00
parent 0a9a085cfb
commit 3b393e074f
2 changed files with 35 additions and 13 deletions

View File

@ -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<string, unknown>;
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");
});

View File

@ -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<v
}
const catalog = await loadModelCatalog({ config: cfg });
const { allowedKeys } = buildAllowedModelSet({
const { allowAny, allowedKeys } = buildAllowedModelSet({
cfg,
catalog,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
if (allowAny) {
return; // all models are allowed; nothing to warn about
}
const gaps: Array<{ provider: string; id: string }> = [];
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);
}