Skip to content

Commit 56c4f97

Browse files
committed
fix(models): restore provider catalog listing
1 parent eb3e4f2 commit 56c4f97

5 files changed

Lines changed: 216 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
3232

3333
### Fixes
3434

35+
- Models CLI: restore `openclaw models list --provider <id>` catalog and registry fallback rows for unconfigured providers, so provider-specific verification commands no longer report "No models found." Fixes #75517; supersedes #75615. Thanks @lotsoftick and @koshaji.
3536
- Control UI/chat: show inline feedback when local slash-command dispatch is unavailable or fails unexpectedly instead of clearing the composer silently. Fixes #52105. Thanks @MooreQiao.
3637
- Memory/markdown: replace CRLF managed blocks in place and collapse duplicate marker blocks without rewriting unmanaged markdown, so Dreaming and Memory Wiki files self-heal from repeated generated sections. Fixes #75491; supersedes #75495, #75810, and #76008. Thanks @asaenokkostya-coder, @ottodeng, @everettjf, and @lrg913427-dot.
3738
- Agents/tools: return critical tool-loop circuit-breaker stops as blocked tool results instead of thrown tool failures, so models see the guardrail and stop retrying the same call. Thanks @rayraiser.

src/commands/models/list.list-command.forward-compat.test.ts

Lines changed: 164 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -279,14 +279,175 @@ beforeEach(() => {
279279

280280
describe("modelsListCommand forward-compat", () => {
281281
describe("configured rows", () => {
282-
it("keeps configured provider filters on the registry-free row path", async () => {
282+
it("returns manifest catalog rows for provider filters without --all", async () => {
283+
mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] });
284+
mocks.loadStaticManifestCatalogRowsForList.mockReturnValueOnce([
285+
{
286+
provider: "moonshot",
287+
id: "kimi-k2.6",
288+
ref: "moonshot/kimi-k2.6",
289+
mergeKey: "moonshot::kimi-k2.6",
290+
name: "Kimi K2.6",
291+
source: "manifest",
292+
input: ["text", "image"],
293+
reasoning: false,
294+
status: "available",
295+
baseUrl: "https://api.moonshot.ai/v1",
296+
contextWindow: 262_144,
297+
},
298+
]);
299+
const runtime = createRuntime();
300+
301+
await modelsListCommand({ json: true, provider: "moonshot" }, runtime as never);
302+
303+
expect(mocks.loadModelRegistry).not.toHaveBeenCalled();
304+
expect(runtime.log).not.toHaveBeenCalledWith("No models found.");
305+
expect(lastPrintedRows<{ key: string }>()).toEqual([
306+
expect.objectContaining({ key: "moonshot/kimi-k2.6" }),
307+
]);
308+
});
309+
310+
it("keeps catalog metadata when provider-filtered configured entries overlap", async () => {
311+
mocks.resolveConfiguredEntries.mockReturnValueOnce({
312+
entries: [
313+
{
314+
key: "moonshot/kimi-k2.6",
315+
ref: { provider: "moonshot", model: "kimi-k2.6" },
316+
tags: new Set(["configured"]),
317+
aliases: [],
318+
},
319+
],
320+
});
321+
mocks.loadStaticManifestCatalogRowsForList.mockReturnValueOnce([
322+
{
323+
provider: "moonshot",
324+
id: "kimi-k2.6",
325+
ref: "moonshot/kimi-k2.6",
326+
mergeKey: "moonshot::kimi-k2.6",
327+
name: "Kimi K2.6",
328+
source: "manifest",
329+
input: ["text", "image"],
330+
reasoning: false,
331+
status: "available",
332+
baseUrl: "https://api.moonshot.ai/v1",
333+
contextWindow: 262_144,
334+
},
335+
]);
336+
const runtime = createRuntime();
337+
338+
await modelsListCommand({ json: true, provider: "moonshot" }, runtime as never);
339+
340+
expect(mocks.loadModelRegistry).not.toHaveBeenCalled();
341+
expect(lastPrintedRows<{ key: string; name: string; tags: string[] }>()).toEqual([
342+
expect.objectContaining({
343+
key: "moonshot/kimi-k2.6",
344+
name: "Kimi K2.6",
345+
tags: ["configured"],
346+
}),
347+
]);
348+
});
349+
350+
it("falls back to registry rows for unknown provider filters without --all", async () => {
351+
mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] });
352+
mocks.loadModelRegistry.mockResolvedValueOnce({
353+
models: [
354+
{
355+
provider: "google",
356+
id: "gemini-2.5-pro",
357+
name: "Gemini 2.5 Pro",
358+
api: "google-gemini",
359+
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
360+
input: ["text", "image"],
361+
contextWindow: 1_048_576,
362+
maxTokens: 65_536,
363+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
364+
},
365+
],
366+
availableKeys: undefined,
367+
registry: {
368+
getAll: () => [
369+
{
370+
provider: "google",
371+
id: "gemini-2.5-pro",
372+
name: "Gemini 2.5 Pro",
373+
api: "google-gemini",
374+
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
375+
input: ["text", "image"],
376+
contextWindow: 1_048_576,
377+
maxTokens: 65_536,
378+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
379+
},
380+
],
381+
},
382+
});
383+
const runtime = createRuntime();
384+
385+
await modelsListCommand({ json: true, provider: "google" }, runtime as never);
386+
387+
expect(mocks.loadModelRegistry).toHaveBeenCalled();
388+
expect(runtime.log).not.toHaveBeenCalledWith("No models found.");
389+
expect(lastPrintedRows<{ key: string }>()).toEqual([
390+
expect.objectContaining({ key: "google/gemini-2.5-pro" }),
391+
]);
392+
});
393+
394+
it("uses provider static catalog rows for provider filters without --all", async () => {
395+
mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] });
396+
mocks.hasProviderStaticCatalogForFilter.mockResolvedValueOnce(true);
397+
mocks.loadProviderCatalogModelsForList.mockResolvedValueOnce([
398+
{
399+
provider: "google",
400+
id: "gemini-2.5-pro",
401+
name: "gemini-2.5-pro",
402+
api: "google-gemini",
403+
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
404+
input: ["text", "image"],
405+
contextWindow: 1_048_576,
406+
maxTokens: 65_536,
407+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
408+
},
409+
]);
410+
const runtime = createRuntime();
411+
412+
await modelsListCommand({ json: true, provider: "google" }, runtime as never);
413+
414+
expect(mocks.loadModelRegistry).not.toHaveBeenCalled();
415+
expect(mocks.loadProviderCatalogModelsForList).toHaveBeenCalledWith(
416+
expect.objectContaining({
417+
providerFilter: "google",
418+
staticOnly: true,
419+
}),
420+
);
421+
expect(lastPrintedRows<{ key: string }>()).toEqual([
422+
expect.objectContaining({ key: "google/gemini-2.5-pro" }),
423+
]);
424+
});
425+
426+
it("uses provider-index catalog rows for provider filters without --all", async () => {
427+
mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] });
428+
mocks.loadProviderIndexCatalogRowsForList.mockReturnValueOnce([
429+
{
430+
provider: "moonshot",
431+
id: "kimi-k2.6",
432+
ref: "moonshot/kimi-k2.6",
433+
mergeKey: "moonshot::kimi-k2.6",
434+
name: "Kimi K2.6",
435+
source: "provider-index",
436+
input: ["text", "image"],
437+
reasoning: false,
438+
status: "available",
439+
baseUrl: "https://api.moonshot.ai/v1",
440+
contextWindow: 262_144,
441+
},
442+
]);
283443
const runtime = createRuntime();
284444

285445
await modelsListCommand({ json: true, provider: "moonshot" }, runtime as never);
286446

287447
expect(mocks.loadModelRegistry).not.toHaveBeenCalled();
288-
expect(mocks.printModelTable).not.toHaveBeenCalled();
289-
expect(runtime.log).toHaveBeenCalledWith("No models found.");
448+
expect(lastPrintedRows<{ key: string }>()).toEqual([
449+
expect.objectContaining({ key: "moonshot/kimi-k2.6" }),
450+
]);
290451
});
291452

292453
it("includes configured provider model rows for provider-filtered lists", async () => {

src/commands/models/list.list-command.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,12 @@ export async function modelsListCommand(
9797
let availabilityErrorMessage: string | undefined;
9898
const { entries } = resolveConfiguredEntries(cfg);
9999
const configuredByKey = new Map(entries.map((entry) => [entry.key, entry]));
100-
const sourcePlanModule = opts.all ? await loadSourcePlanModule() : undefined;
100+
const enableSourcePlanCascade = Boolean(opts.all) || Boolean(providerFilter);
101+
const sourcePlanModule = enableSourcePlanCascade ? await loadSourcePlanModule() : undefined;
101102
const sourcePlan = sourcePlanModule
102103
? await sourcePlanModule.planAllModelListSources({
103104
all: opts.all,
105+
enableCascade: enableSourcePlanCascade,
104106
providerFilter,
105107
cfg,
106108
})
@@ -156,14 +158,15 @@ export async function modelsListCommand(
156158
});
157159
const rows: ModelRow[] = [];
158160

159-
if (opts.all) {
161+
if (enableSourcePlanCascade) {
160162
const { appendAllModelRowSources } = await loadRowSourcesModule();
161163
if (!sourcePlan || !sourcePlanModule) {
162164
throw new Error("models list source plan was not initialized");
163165
}
164166
let rowContext = buildRowContext(sourcePlan.skipRuntimeModelSuppression);
165167
const initialAppend = await appendAllModelRowSources({
166168
rows,
169+
entries,
167170
context: rowContext,
168171
modelRegistry,
169172
registryModels,
@@ -189,6 +192,7 @@ export async function modelsListCommand(
189192
rowContext = buildRowContext(useScopedRegistryFallback);
190193
await appendAllModelRowSources({
191194
rows,
195+
entries,
192196
context: rowContext,
193197
modelRegistry,
194198
registryModels,

src/commands/models/list.row-sources.ts

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type { ConfiguredEntry, ModelRow } from "./list.types.js";
1414

1515
type AllModelRowSources = {
1616
rows: ModelRow[];
17+
entries?: ConfiguredEntry[];
1718
context: RowBuilderContext;
1819
modelRegistry?: ModelRegistry;
1920
registryModels?: ReturnType<ModelRegistry["getAll"]>;
@@ -28,12 +29,7 @@ export async function appendAllModelRowSources(
2829
params: AllModelRowSources,
2930
): Promise<AppendAllModelRowSourcesResult> {
3031
if (params.context.filter.provider && params.sourcePlan.kind !== "registry") {
31-
let seenKeys = new Set<string>();
32-
await appendConfiguredProviderRows({
33-
rows: params.rows,
34-
context: params.context,
35-
seenKeys,
36-
});
32+
const seenKeys = new Set<string>();
3733
let catalogRows = 0;
3834
if (params.sourcePlan.kind === "manifest") {
3935
catalogRows = await appendManifestCatalogRows({
@@ -63,7 +59,30 @@ export async function appendAllModelRowSources(
6359
staticOnly: params.sourcePlan.kind === "provider-runtime-static",
6460
});
6561
}
66-
if (catalogRows === 0 && params.sourcePlan.fallbackToRegistryWhenEmpty) {
62+
if (params.entries && params.entries.length > 0) {
63+
const missingEntries = params.entries.filter((entry) => !seenKeys.has(entry.key));
64+
if (missingEntries.length > 0) {
65+
await appendConfiguredRows({
66+
rows: params.rows,
67+
entries: missingEntries,
68+
modelRegistry: params.modelRegistry,
69+
context: params.context,
70+
});
71+
for (const row of params.rows) {
72+
seenKeys.add(row.key);
73+
}
74+
}
75+
}
76+
await appendConfiguredProviderRows({
77+
rows: params.rows,
78+
context: params.context,
79+
seenKeys,
80+
});
81+
if (
82+
catalogRows === 0 &&
83+
params.rows.length === 0 &&
84+
params.sourcePlan.fallbackToRegistryWhenEmpty
85+
) {
6786
if (!params.modelRegistry) {
6887
return { requiresRegistryFallback: true };
6988
}
@@ -88,6 +107,22 @@ export async function appendAllModelRowSources(
88107
skipSuppression: Boolean(params.modelRegistry),
89108
});
90109

110+
if (params.context.filter.provider && params.entries && params.entries.length > 0) {
111+
const missingEntries = params.entries.filter((entry) => !seenKeys.has(entry.key));
112+
if (missingEntries.length > 0) {
113+
const appendedRowsStart = params.rows.length;
114+
await appendConfiguredRows({
115+
rows: params.rows,
116+
entries: missingEntries,
117+
modelRegistry: params.modelRegistry,
118+
context: params.context,
119+
});
120+
for (const row of params.rows.slice(appendedRowsStart)) {
121+
seenKeys.add(row.key);
122+
}
123+
}
124+
}
125+
91126
await appendConfiguredProviderRows({
92127
rows: params.rows,
93128
context: params.context,

src/commands/models/list.source-plan.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ export function createRegistryModelListSourcePlan(): ModelListSourcePlan {
4444

4545
export async function planAllModelListSources(params: {
4646
all?: boolean;
47+
enableCascade?: boolean;
4748
providerFilter?: string;
4849
cfg: OpenClawConfig;
4950
}): Promise<ModelListSourcePlan> {
50-
if (!params.all) {
51+
const enableCascade = params.enableCascade ?? params.all;
52+
if (!enableCascade) {
5153
return createRegistryModelListSourcePlan();
5254
}
5355

0 commit comments

Comments
 (0)