fix: restore models list registry fallback

This commit is contained in:
Peter Steinberger
2026-04-24 06:40:28 +01:00
parent 73288c20bd
commit bff212822c
4 changed files with 121 additions and 22 deletions

View File

@@ -468,6 +468,50 @@ describe("modelsListCommand forward-compat", () => {
]);
});
it("falls back to registry-backed rows when the fast-path catalog is empty", async () => {
mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] });
mocks.hasProviderStaticCatalogForFilter.mockResolvedValueOnce(true);
mocks.loadProviderCatalogModelsForList.mockResolvedValueOnce([]).mockResolvedValueOnce([]);
mocks.loadModelRegistry.mockResolvedValueOnce({
models: [{ ...OPENAI_CODEX_MODEL }],
availableKeys: new Set(["openai-codex/gpt-5.4"]),
registry: {
getAll: () => [{ ...OPENAI_CODEX_MODEL }],
},
});
const runtime = createRuntime();
await modelsListCommand(
{ all: true, provider: "openai-codex", json: true },
runtime as never,
);
expect(mocks.loadModelRegistry).toHaveBeenCalledWith(
mocks.resolvedConfig,
expect.objectContaining({
providerFilter: "openai-codex",
}),
);
expect(mocks.loadProviderCatalogModelsForList).toHaveBeenNthCalledWith(1, {
cfg: mocks.resolvedConfig,
agentDir: "/tmp/openclaw-agent",
providerFilter: "openai-codex",
staticOnly: true,
});
expect(mocks.loadProviderCatalogModelsForList).toHaveBeenNthCalledWith(2, {
cfg: mocks.resolvedConfig,
agentDir: "/tmp/openclaw-agent",
providerFilter: "openai-codex",
staticOnly: undefined,
});
expect(lastPrintedRows<{ key: string; available: boolean }>()).toEqual([
expect.objectContaining({
key: "openai-codex/gpt-5.4",
available: true,
}),
]);
});
it("keeps the registry path for provider filters without static catalog coverage", async () => {
mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] });
mocks.hasProviderStaticCatalogForFilter.mockResolvedValueOnce(false);

View File

@@ -70,13 +70,16 @@ export async function modelsListCommand(
providerFilter,
useProviderCatalogFastPath,
});
const loadRegistryState = async () => {
const loaded = await loadListModelRegistry(cfg, { providerFilter });
modelRegistry = loaded.registry;
discoveredKeys = loaded.discoveredKeys;
availableKeys = loaded.availableKeys;
availabilityErrorMessage = loaded.availabilityErrorMessage;
};
try {
if (shouldLoadRegistry) {
const loaded = await loadListModelRegistry(cfg, { providerFilter });
modelRegistry = loaded.registry;
discoveredKeys = loaded.discoveredKeys;
availableKeys = loaded.availableKeys;
availabilityErrorMessage = loaded.availabilityErrorMessage;
await loadRegistryState();
} else if (!opts.all) {
const loaded = loadConfiguredListModelRegistry(cfg, entries, { providerFilter });
modelRegistry = loaded.registry;
@@ -88,14 +91,7 @@ export async function modelsListCommand(
process.exitCode = 1;
return;
}
if (availabilityErrorMessage !== undefined) {
runtime.error(
`Model availability lookup failed; falling back to auth heuristics for discovered models: ${availabilityErrorMessage}`,
);
}
const rows: ModelRow[] = [];
const rowContext = {
const buildRowContext = (skipRuntimeModelSuppression: boolean) => ({
cfg,
agentDir,
authStore,
@@ -106,16 +102,35 @@ export async function modelsListCommand(
provider: providerFilter,
local: opts.local,
},
skipRuntimeModelSuppression: useProviderCatalogFastPath,
};
skipRuntimeModelSuppression,
});
const rows: ModelRow[] = [];
if (opts.all) {
await appendAllModelRowSources({
let rowContext = buildRowContext(useProviderCatalogFastPath);
const initialAppend = await appendAllModelRowSources({
rows,
context: rowContext,
modelRegistry,
useProviderCatalogFastPath,
});
if (initialAppend.requiresRegistryFallback) {
try {
await loadRegistryState();
} catch (err) {
runtime.error(`Model registry unavailable:\n${formatErrorWithStack(err)}`);
process.exitCode = 1;
return;
}
rows.length = 0;
rowContext = buildRowContext(false);
await appendAllModelRowSources({
rows,
context: rowContext,
modelRegistry,
useProviderCatalogFastPath: false,
});
}
} else {
const registry = modelRegistry;
if (!registry) {
@@ -127,10 +142,16 @@ export async function modelsListCommand(
rows,
entries,
modelRegistry: registry,
context: rowContext,
context: buildRowContext(false),
});
}
if (availabilityErrorMessage !== undefined) {
runtime.error(
`Model availability lookup failed; falling back to auth heuristics for discovered models: ${availabilityErrorMessage}`,
);
}
if (rows.length === 0) {
runtime.log("No models found.");
return;

View File

@@ -167,6 +167,30 @@ describe("loadProviderCatalogModelsForList", () => {
);
});
it("returns an empty catalog when a static provider catalog throws", async () => {
providerDiscoveryMocks.resolvePluginDiscoveryProviders.mockResolvedValueOnce([
{
id: "moonshot",
pluginId: "moonshot",
label: "Moonshot",
auth: [],
staticCatalog: {
run: async () => {
throw new Error("catalog offline");
},
},
},
]);
await expect(
loadProviderCatalogModelsForList({
...baseParams,
providerFilter: "moonshot",
staticOnly: true,
}),
).resolves.toEqual([]);
});
it("only skips registry for providers with actual static catalogs", async () => {
providerDiscoveryMocks.resolvePluginDiscoveryProviders.mockResolvedValue([catalogOnlyProvider]);

View File

@@ -16,6 +16,10 @@ type AllModelRowSources = {
useProviderCatalogFastPath: boolean;
};
type AppendAllModelRowSourcesResult = {
requiresRegistryFallback: boolean;
};
export function modelRowSourcesRequireRegistry(params: {
all?: boolean;
providerFilter?: string;
@@ -30,7 +34,9 @@ export function modelRowSourcesRequireRegistry(params: {
return true;
}
export async function appendAllModelRowSources(params: AllModelRowSources): Promise<void> {
export async function appendAllModelRowSources(
params: AllModelRowSources,
): Promise<AppendAllModelRowSourcesResult> {
if (params.context.filter.provider && params.useProviderCatalogFastPath) {
let seenKeys = new Set<string>();
appendConfiguredProviderRows({
@@ -45,13 +51,16 @@ export async function appendAllModelRowSources(params: AllModelRowSources): Prom
staticOnly: true,
});
if (catalogRows === 0) {
seenKeys = appendDiscoveredRows({
if (!params.modelRegistry) {
return { requiresRegistryFallback: true };
}
appendDiscoveredRows({
rows: params.rows,
models: params.modelRegistry?.getAll() ?? [],
models: params.modelRegistry.getAll(),
context: params.context,
});
}
return;
return { requiresRegistryFallback: false };
}
const seenKeys = appendDiscoveredRows({
@@ -73,7 +82,7 @@ export async function appendAllModelRowSources(params: AllModelRowSources): Prom
context: params.context,
seenKeys,
});
return;
return { requiresRegistryFallback: false };
}
await appendProviderCatalogRows({
@@ -81,6 +90,7 @@ export async function appendAllModelRowSources(params: AllModelRowSources): Prom
context: params.context,
seenKeys,
});
return { requiresRegistryFallback: false };
}
export function appendConfiguredModelRowSources(params: {