mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:10:45 +00:00
fix(models): restore provider catalog listing
This commit is contained in:
@@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -279,14 +279,175 @@ beforeEach(() => {
|
||||
|
||||
describe("modelsListCommand forward-compat", () => {
|
||||
describe("configured rows", () => {
|
||||
it("keeps configured provider filters on the registry-free row path", async () => {
|
||||
it("returns manifest catalog rows for provider filters without --all", async () => {
|
||||
mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] });
|
||||
mocks.loadStaticManifestCatalogRowsForList.mockReturnValueOnce([
|
||||
{
|
||||
provider: "moonshot",
|
||||
id: "kimi-k2.6",
|
||||
ref: "moonshot/kimi-k2.6",
|
||||
mergeKey: "moonshot::kimi-k2.6",
|
||||
name: "Kimi K2.6",
|
||||
source: "manifest",
|
||||
input: ["text", "image"],
|
||||
reasoning: false,
|
||||
status: "available",
|
||||
baseUrl: "https://api.moonshot.ai/v1",
|
||||
contextWindow: 262_144,
|
||||
},
|
||||
]);
|
||||
const runtime = createRuntime();
|
||||
|
||||
await modelsListCommand({ json: true, provider: "moonshot" }, runtime as never);
|
||||
|
||||
expect(mocks.loadModelRegistry).not.toHaveBeenCalled();
|
||||
expect(mocks.printModelTable).not.toHaveBeenCalled();
|
||||
expect(runtime.log).toHaveBeenCalledWith("No models found.");
|
||||
expect(runtime.log).not.toHaveBeenCalledWith("No models found.");
|
||||
expect(lastPrintedRows<{ key: string }>()).toEqual([
|
||||
expect.objectContaining({ key: "moonshot/kimi-k2.6" }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps catalog metadata when provider-filtered configured entries overlap", async () => {
|
||||
mocks.resolveConfiguredEntries.mockReturnValueOnce({
|
||||
entries: [
|
||||
{
|
||||
key: "moonshot/kimi-k2.6",
|
||||
ref: { provider: "moonshot", model: "kimi-k2.6" },
|
||||
tags: new Set(["configured"]),
|
||||
aliases: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
mocks.loadStaticManifestCatalogRowsForList.mockReturnValueOnce([
|
||||
{
|
||||
provider: "moonshot",
|
||||
id: "kimi-k2.6",
|
||||
ref: "moonshot/kimi-k2.6",
|
||||
mergeKey: "moonshot::kimi-k2.6",
|
||||
name: "Kimi K2.6",
|
||||
source: "manifest",
|
||||
input: ["text", "image"],
|
||||
reasoning: false,
|
||||
status: "available",
|
||||
baseUrl: "https://api.moonshot.ai/v1",
|
||||
contextWindow: 262_144,
|
||||
},
|
||||
]);
|
||||
const runtime = createRuntime();
|
||||
|
||||
await modelsListCommand({ json: true, provider: "moonshot" }, runtime as never);
|
||||
|
||||
expect(mocks.loadModelRegistry).not.toHaveBeenCalled();
|
||||
expect(lastPrintedRows<{ key: string; name: string; tags: string[] }>()).toEqual([
|
||||
expect.objectContaining({
|
||||
key: "moonshot/kimi-k2.6",
|
||||
name: "Kimi K2.6",
|
||||
tags: ["configured"],
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("falls back to registry rows for unknown provider filters without --all", async () => {
|
||||
mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] });
|
||||
mocks.loadModelRegistry.mockResolvedValueOnce({
|
||||
models: [
|
||||
{
|
||||
provider: "google",
|
||||
id: "gemini-2.5-pro",
|
||||
name: "Gemini 2.5 Pro",
|
||||
api: "google-gemini",
|
||||
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
||||
input: ["text", "image"],
|
||||
contextWindow: 1_048_576,
|
||||
maxTokens: 65_536,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
},
|
||||
],
|
||||
availableKeys: undefined,
|
||||
registry: {
|
||||
getAll: () => [
|
||||
{
|
||||
provider: "google",
|
||||
id: "gemini-2.5-pro",
|
||||
name: "Gemini 2.5 Pro",
|
||||
api: "google-gemini",
|
||||
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
||||
input: ["text", "image"],
|
||||
contextWindow: 1_048_576,
|
||||
maxTokens: 65_536,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const runtime = createRuntime();
|
||||
|
||||
await modelsListCommand({ json: true, provider: "google" }, runtime as never);
|
||||
|
||||
expect(mocks.loadModelRegistry).toHaveBeenCalled();
|
||||
expect(runtime.log).not.toHaveBeenCalledWith("No models found.");
|
||||
expect(lastPrintedRows<{ key: string }>()).toEqual([
|
||||
expect.objectContaining({ key: "google/gemini-2.5-pro" }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses provider static catalog rows for provider filters without --all", async () => {
|
||||
mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] });
|
||||
mocks.hasProviderStaticCatalogForFilter.mockResolvedValueOnce(true);
|
||||
mocks.loadProviderCatalogModelsForList.mockResolvedValueOnce([
|
||||
{
|
||||
provider: "google",
|
||||
id: "gemini-2.5-pro",
|
||||
name: "gemini-2.5-pro",
|
||||
api: "google-gemini",
|
||||
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
||||
input: ["text", "image"],
|
||||
contextWindow: 1_048_576,
|
||||
maxTokens: 65_536,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
},
|
||||
]);
|
||||
const runtime = createRuntime();
|
||||
|
||||
await modelsListCommand({ json: true, provider: "google" }, runtime as never);
|
||||
|
||||
expect(mocks.loadModelRegistry).not.toHaveBeenCalled();
|
||||
expect(mocks.loadProviderCatalogModelsForList).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
providerFilter: "google",
|
||||
staticOnly: true,
|
||||
}),
|
||||
);
|
||||
expect(lastPrintedRows<{ key: string }>()).toEqual([
|
||||
expect.objectContaining({ key: "google/gemini-2.5-pro" }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses provider-index catalog rows for provider filters without --all", async () => {
|
||||
mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] });
|
||||
mocks.loadProviderIndexCatalogRowsForList.mockReturnValueOnce([
|
||||
{
|
||||
provider: "moonshot",
|
||||
id: "kimi-k2.6",
|
||||
ref: "moonshot/kimi-k2.6",
|
||||
mergeKey: "moonshot::kimi-k2.6",
|
||||
name: "Kimi K2.6",
|
||||
source: "provider-index",
|
||||
input: ["text", "image"],
|
||||
reasoning: false,
|
||||
status: "available",
|
||||
baseUrl: "https://api.moonshot.ai/v1",
|
||||
contextWindow: 262_144,
|
||||
},
|
||||
]);
|
||||
const runtime = createRuntime();
|
||||
|
||||
await modelsListCommand({ json: true, provider: "moonshot" }, runtime as never);
|
||||
|
||||
expect(mocks.loadModelRegistry).not.toHaveBeenCalled();
|
||||
expect(lastPrintedRows<{ key: string }>()).toEqual([
|
||||
expect.objectContaining({ key: "moonshot/kimi-k2.6" }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("includes configured provider model rows for provider-filtered lists", async () => {
|
||||
|
||||
@@ -97,10 +97,12 @@ export async function modelsListCommand(
|
||||
let availabilityErrorMessage: string | undefined;
|
||||
const { entries } = resolveConfiguredEntries(cfg);
|
||||
const configuredByKey = new Map(entries.map((entry) => [entry.key, entry]));
|
||||
const sourcePlanModule = opts.all ? await loadSourcePlanModule() : undefined;
|
||||
const enableSourcePlanCascade = Boolean(opts.all) || Boolean(providerFilter);
|
||||
const sourcePlanModule = enableSourcePlanCascade ? await loadSourcePlanModule() : undefined;
|
||||
const sourcePlan = sourcePlanModule
|
||||
? await sourcePlanModule.planAllModelListSources({
|
||||
all: opts.all,
|
||||
enableCascade: enableSourcePlanCascade,
|
||||
providerFilter,
|
||||
cfg,
|
||||
})
|
||||
@@ -156,7 +158,7 @@ export async function modelsListCommand(
|
||||
});
|
||||
const rows: ModelRow[] = [];
|
||||
|
||||
if (opts.all) {
|
||||
if (enableSourcePlanCascade) {
|
||||
const { appendAllModelRowSources } = await loadRowSourcesModule();
|
||||
if (!sourcePlan || !sourcePlanModule) {
|
||||
throw new Error("models list source plan was not initialized");
|
||||
@@ -164,6 +166,7 @@ export async function modelsListCommand(
|
||||
let rowContext = buildRowContext(sourcePlan.skipRuntimeModelSuppression);
|
||||
const initialAppend = await appendAllModelRowSources({
|
||||
rows,
|
||||
entries,
|
||||
context: rowContext,
|
||||
modelRegistry,
|
||||
registryModels,
|
||||
@@ -189,6 +192,7 @@ export async function modelsListCommand(
|
||||
rowContext = buildRowContext(useScopedRegistryFallback);
|
||||
await appendAllModelRowSources({
|
||||
rows,
|
||||
entries,
|
||||
context: rowContext,
|
||||
modelRegistry,
|
||||
registryModels,
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { ConfiguredEntry, ModelRow } from "./list.types.js";
|
||||
|
||||
type AllModelRowSources = {
|
||||
rows: ModelRow[];
|
||||
entries?: ConfiguredEntry[];
|
||||
context: RowBuilderContext;
|
||||
modelRegistry?: ModelRegistry;
|
||||
registryModels?: ReturnType<ModelRegistry["getAll"]>;
|
||||
@@ -28,12 +29,7 @@ export async function appendAllModelRowSources(
|
||||
params: AllModelRowSources,
|
||||
): Promise<AppendAllModelRowSourcesResult> {
|
||||
if (params.context.filter.provider && params.sourcePlan.kind !== "registry") {
|
||||
let seenKeys = new Set<string>();
|
||||
await appendConfiguredProviderRows({
|
||||
rows: params.rows,
|
||||
context: params.context,
|
||||
seenKeys,
|
||||
});
|
||||
const seenKeys = new Set<string>();
|
||||
let catalogRows = 0;
|
||||
if (params.sourcePlan.kind === "manifest") {
|
||||
catalogRows = await appendManifestCatalogRows({
|
||||
@@ -63,7 +59,30 @@ export async function appendAllModelRowSources(
|
||||
staticOnly: params.sourcePlan.kind === "provider-runtime-static",
|
||||
});
|
||||
}
|
||||
if (catalogRows === 0 && params.sourcePlan.fallbackToRegistryWhenEmpty) {
|
||||
if (params.entries && params.entries.length > 0) {
|
||||
const missingEntries = params.entries.filter((entry) => !seenKeys.has(entry.key));
|
||||
if (missingEntries.length > 0) {
|
||||
await appendConfiguredRows({
|
||||
rows: params.rows,
|
||||
entries: missingEntries,
|
||||
modelRegistry: params.modelRegistry,
|
||||
context: params.context,
|
||||
});
|
||||
for (const row of params.rows) {
|
||||
seenKeys.add(row.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
await appendConfiguredProviderRows({
|
||||
rows: params.rows,
|
||||
context: params.context,
|
||||
seenKeys,
|
||||
});
|
||||
if (
|
||||
catalogRows === 0 &&
|
||||
params.rows.length === 0 &&
|
||||
params.sourcePlan.fallbackToRegistryWhenEmpty
|
||||
) {
|
||||
if (!params.modelRegistry) {
|
||||
return { requiresRegistryFallback: true };
|
||||
}
|
||||
@@ -88,6 +107,22 @@ export async function appendAllModelRowSources(
|
||||
skipSuppression: Boolean(params.modelRegistry),
|
||||
});
|
||||
|
||||
if (params.context.filter.provider && params.entries && params.entries.length > 0) {
|
||||
const missingEntries = params.entries.filter((entry) => !seenKeys.has(entry.key));
|
||||
if (missingEntries.length > 0) {
|
||||
const appendedRowsStart = params.rows.length;
|
||||
await appendConfiguredRows({
|
||||
rows: params.rows,
|
||||
entries: missingEntries,
|
||||
modelRegistry: params.modelRegistry,
|
||||
context: params.context,
|
||||
});
|
||||
for (const row of params.rows.slice(appendedRowsStart)) {
|
||||
seenKeys.add(row.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await appendConfiguredProviderRows({
|
||||
rows: params.rows,
|
||||
context: params.context,
|
||||
|
||||
@@ -44,10 +44,12 @@ export function createRegistryModelListSourcePlan(): ModelListSourcePlan {
|
||||
|
||||
export async function planAllModelListSources(params: {
|
||||
all?: boolean;
|
||||
enableCascade?: boolean;
|
||||
providerFilter?: string;
|
||||
cfg: OpenClawConfig;
|
||||
}): Promise<ModelListSourcePlan> {
|
||||
if (!params.all) {
|
||||
const enableCascade = params.enableCascade ?? params.all;
|
||||
if (!enableCascade) {
|
||||
return createRegistryModelListSourcePlan();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user