feat: use manifest catalog rows for provider list fast path

This commit is contained in:
Shakker
2026-04-25 04:14:19 +01:00
parent 82a529aaaf
commit 35171f4e47
7 changed files with 153 additions and 10 deletions

View File

@@ -21,6 +21,7 @@ const loadModelCatalog = vi.fn(async () => []);
const loadProviderCatalogModelsForList = vi.fn<() => Promise<Array<Record<string, unknown>>>>(
async () => [],
);
const loadStaticManifestCatalogRowsForList = vi.fn(() => []);
const hasProviderStaticCatalogForFilter = vi.fn().mockResolvedValue(false);
const shouldSuppressBuiltInModel = vi.fn().mockReturnValue(false);
const modelRegistryState = {
@@ -113,6 +114,10 @@ vi.mock("./models/list.provider-catalog.js", async (importOriginal) => {
};
});
vi.mock("./models/list.manifest-catalog.js", () => ({
loadStaticManifestCatalogRowsForList,
}));
vi.mock("../agents/model-suppression.js", () => ({
shouldSuppressBuiltInModel,
}));
@@ -162,6 +167,8 @@ beforeEach(() => {
loadModelCatalog.mockResolvedValue([]);
loadProviderCatalogModelsForList.mockReset();
loadProviderCatalogModelsForList.mockResolvedValue([]);
loadStaticManifestCatalogRowsForList.mockReset();
loadStaticManifestCatalogRowsForList.mockReturnValue([]);
hasProviderStaticCatalogForFilter.mockReset();
hasProviderStaticCatalogForFilter.mockResolvedValue(false);
shouldSuppressBuiltInModel.mockReset();

View File

@@ -62,6 +62,7 @@ const mocks = vi.hoisted(() => {
loadModelRegistry: vi.fn(),
loadModelCatalog: vi.fn(),
loadProviderCatalogModelsForList: vi.fn(),
loadStaticManifestCatalogRowsForList: vi.fn(),
hasProviderStaticCatalogForFilter: vi.fn(),
resolveConfiguredEntries: vi.fn(),
printModelTable: vi.fn(),
@@ -89,6 +90,7 @@ function resetMocks() {
});
mocks.loadModelCatalog.mockResolvedValue([]);
mocks.loadProviderCatalogModelsForList.mockResolvedValue([]);
mocks.loadStaticManifestCatalogRowsForList.mockReturnValue([]);
mocks.hasProviderStaticCatalogForFilter.mockResolvedValue(false);
mocks.resolveConfiguredEntries.mockReturnValue({
entries: [
@@ -147,6 +149,10 @@ function installModelsListCommandForwardCompatMocks() {
hasProviderStaticCatalogForFilter: mocks.hasProviderStaticCatalogForFilter,
}));
vi.doMock("./list.manifest-catalog.js", () => ({
loadStaticManifestCatalogRowsForList: mocks.loadStaticManifestCatalogRowsForList,
}));
vi.doMock("./list.registry-load.js", () => ({
loadListModelRegistry: async (
cfg: unknown,
@@ -469,6 +475,38 @@ describe("modelsListCommand forward-compat", () => {
]);
});
it("uses manifest catalog rows before provider runtime catalog rows", 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({ all: true, provider: "moonshot", json: true }, runtime as never);
expect(mocks.loadModelRegistry).not.toHaveBeenCalled();
expect(mocks.hasProviderStaticCatalogForFilter).not.toHaveBeenCalled();
expect(mocks.loadProviderCatalogModelsForList).not.toHaveBeenCalled();
expect(lastPrintedRows<{ key: string; available: boolean }>()).toEqual([
expect.objectContaining({
key: "moonshot/kimi-k2.6",
available: false,
}),
]);
});
it("falls back to registry-backed rows when the fast-path catalog is empty", async () => {
mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] });
mocks.hasProviderStaticCatalogForFilter.mockResolvedValueOnce(true);

View File

@@ -1,5 +1,6 @@
import type { ModelRegistry } from "@mariozechner/pi-coding-agent";
import { parseModelRef } from "../../agents/model-selection.js";
import type { NormalizedModelCatalogRow } from "../../model-catalog/index.js";
import type { RuntimeEnv } from "../../runtime.js";
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
import { resolveConfiguredEntries } from "./list.configured.js";
@@ -61,13 +62,20 @@ export async function modelsListCommand(
let availabilityErrorMessage: string | undefined;
const { entries } = resolveConfiguredEntries(cfg);
const configuredByKey = new Map(entries.map((entry) => [entry.key, entry]));
let manifestCatalogRows: readonly NormalizedModelCatalogRow[] = [];
if (opts.all && providerFilter) {
const { loadStaticManifestCatalogRowsForList } = await import("./list.manifest-catalog.js");
manifestCatalogRows = loadStaticManifestCatalogRowsForList({ cfg, providerFilter });
}
const useManifestCatalogFastPath = manifestCatalogRows.length > 0;
const useProviderCatalogFastPath =
opts.all && providerFilter
!useManifestCatalogFastPath && opts.all && providerFilter
? await hasProviderStaticCatalogForFilter({ cfg, providerFilter })
: false;
const shouldLoadRegistry = modelRowSourcesRequireRegistry({
all: opts.all,
providerFilter,
useManifestCatalogFastPath,
useProviderCatalogFastPath,
});
const loadRegistryState = async () => {
@@ -112,6 +120,8 @@ export async function modelsListCommand(
rows,
context: rowContext,
modelRegistry,
manifestCatalogRows,
useManifestCatalogFastPath,
useProviderCatalogFastPath,
});
if (initialAppend.requiresRegistryFallback) {
@@ -128,6 +138,8 @@ export async function modelsListCommand(
rows,
context: rowContext,
modelRegistry,
manifestCatalogRows: [],
useManifestCatalogFastPath: false,
useProviderCatalogFastPath: false,
});
}

View File

@@ -0,0 +1,27 @@
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { planManifestModelCatalogRows } from "../../model-catalog/index.js";
import type { NormalizedModelCatalogRow } from "../../model-catalog/index.js";
import { loadPluginManifestRegistry } from "../../plugins/manifest-registry.js";
export function loadStaticManifestCatalogRowsForList(params: {
cfg: OpenClawConfig;
providerFilter: string;
env?: NodeJS.ProcessEnv;
}): readonly NormalizedModelCatalogRow[] {
const registry = loadPluginManifestRegistry({
config: params.cfg,
env: params.env,
cache: true,
});
const plan = planManifestModelCatalogRows({
registry,
providerFilter: params.providerFilter,
});
const staticProviders = new Set(
plan.entries.filter((entry) => entry.discovery === "static").map((entry) => entry.provider),
);
if (staticProviders.size === 0) {
return [];
}
return plan.rows.filter((row) => staticProviders.has(row.provider));
}

View File

@@ -8,7 +8,7 @@ export type ListRowModel = {
id: string;
name: string;
provider: string;
input: Array<"text" | "image">;
input: Array<"text" | "image" | "document">;
baseUrl?: string;
contextWindow?: number | null;
contextTokens?: number | null;

View File

@@ -1,9 +1,11 @@
import type { ModelRegistry } from "@mariozechner/pi-coding-agent";
import type { NormalizedModelCatalogRow } from "../../model-catalog/index.js";
import {
appendCatalogSupplementRows,
appendConfiguredProviderRows,
appendConfiguredRows,
appendDiscoveredRows,
appendManifestCatalogRows,
appendProviderCatalogRows,
type RowBuilderContext,
} from "./list.rows.js";
@@ -13,6 +15,8 @@ type AllModelRowSources = {
rows: ModelRow[];
context: RowBuilderContext;
modelRegistry?: ModelRegistry;
manifestCatalogRows?: readonly NormalizedModelCatalogRow[];
useManifestCatalogFastPath: boolean;
useProviderCatalogFastPath: boolean;
};
@@ -23,12 +27,16 @@ type AppendAllModelRowSourcesResult = {
export function modelRowSourcesRequireRegistry(params: {
all?: boolean;
providerFilter?: string;
useManifestCatalogFastPath: boolean;
useProviderCatalogFastPath: boolean;
}): boolean {
if (!params.all) {
return false;
}
if (params.providerFilter && params.useProviderCatalogFastPath) {
if (
params.providerFilter &&
(params.useManifestCatalogFastPath || params.useProviderCatalogFastPath)
) {
return false;
}
return true;
@@ -37,19 +45,33 @@ export function modelRowSourcesRequireRegistry(params: {
export async function appendAllModelRowSources(
params: AllModelRowSources,
): Promise<AppendAllModelRowSourcesResult> {
if (params.context.filter.provider && params.useProviderCatalogFastPath) {
if (
params.context.filter.provider &&
(params.useManifestCatalogFastPath || params.useProviderCatalogFastPath)
) {
let seenKeys = new Set<string>();
appendConfiguredProviderRows({
rows: params.rows,
context: params.context,
seenKeys,
});
const catalogRows = await appendProviderCatalogRows({
rows: params.rows,
context: params.context,
seenKeys,
staticOnly: true,
});
let catalogRows = 0;
if (params.useManifestCatalogFastPath) {
catalogRows = appendManifestCatalogRows({
rows: params.rows,
context: params.context,
seenKeys,
manifestRows: params.manifestCatalogRows ?? [],
});
}
if (catalogRows === 0 && params.useProviderCatalogFastPath) {
catalogRows = await appendProviderCatalogRows({
rows: params.rows,
context: params.context,
seenKeys,
staticOnly: true,
});
}
if (catalogRows === 0) {
if (!params.modelRegistry) {
return { requiresRegistryFallback: true };

View File

@@ -6,6 +6,7 @@ import { shouldSuppressBuiltInModel } from "../../agents/model-suppression.js";
import { normalizeProviderId } from "../../agents/provider-id.js";
import type { ModelDefinitionConfig, ModelProviderConfig } from "../../config/types.models.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { NormalizedModelCatalogRow } from "../../model-catalog/index.js";
import type { ListRowModel } from "./list.model-row.js";
import { toModelRow } from "./list.registry.js";
import {
@@ -134,6 +135,17 @@ function toConfiguredProviderListModel(params: {
};
}
function toManifestCatalogListModel(row: NormalizedModelCatalogRow): ListRowModel {
return {
provider: row.provider,
id: row.id,
name: row.name,
baseUrl: row.baseUrl,
input: [...row.input],
contextWindow: row.contextWindow ?? DEFAULT_CONTEXT_TOKENS,
};
}
function shouldListConfiguredProviderModel(params: {
providerConfig: Partial<ModelProviderConfig>;
model: Partial<ModelDefinitionConfig>;
@@ -213,6 +225,31 @@ export function appendConfiguredProviderRows(params: {
}
}
export function appendManifestCatalogRows(params: {
rows: ModelRow[];
context: RowBuilderContext;
seenKeys: Set<string>;
manifestRows: readonly NormalizedModelCatalogRow[];
}): number {
let appended = 0;
for (const manifestRow of params.manifestRows) {
const key = modelKey(manifestRow.provider, manifestRow.id);
if (
appendVisibleRow({
rows: params.rows,
model: toManifestCatalogListModel(manifestRow),
key,
context: params.context,
seenKeys: params.seenKeys,
allowProviderAvailabilityFallback: true,
})
) {
appended += 1;
}
}
return appended;
}
export async function appendCatalogSupplementRows(params: {
rows: ModelRow[];
modelRegistry: ModelRegistry;