mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
feat: use manifest catalog rows for provider list fast path
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
27
src/commands/models/list.manifest-catalog.ts
Normal file
27
src/commands/models/list.manifest-catalog.ts
Normal 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));
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user