mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
refactor: support refreshable manifest list rows
This commit is contained in:
@@ -781,9 +781,11 @@ Suppression fields:
|
||||
| `when.baseUrlHosts` | `string[]` | Optional list of effective provider base URL hosts required before the suppression applies. |
|
||||
| `when.providerConfigApiIn` | `string[]` | Optional list of exact provider-config `api` values required before the suppression applies. |
|
||||
|
||||
Do not put runtime-only data in `modelCatalog`. If a provider needs account
|
||||
state, an API request, or local process discovery to know the complete model
|
||||
set, declare that provider as `refreshable` or `runtime` in `discovery`.
|
||||
Do not put runtime-only data in `modelCatalog`. Use `static` only when manifest
|
||||
rows are complete enough for provider-filtered list and picker surfaces to skip
|
||||
registry/runtime discovery. Use `refreshable` when manifest rows are useful
|
||||
seeds or supplements but a registry/cache refresh may add more rows. Use
|
||||
`runtime` when OpenClaw must load provider runtime to know the list.
|
||||
|
||||
## modelIdNormalization reference
|
||||
|
||||
|
||||
@@ -70,4 +70,20 @@ describe("loadStaticManifestCatalogRowsForList", () => {
|
||||
env: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("can load refreshable manifest rows for broad registry-backed lists", async () => {
|
||||
const { loadManifestCatalogRowsForList } = await import("./list.manifest-catalog.js");
|
||||
mocks.loadPluginRegistrySnapshot.mockReturnValueOnce({ plugins: [], diagnostics: [] });
|
||||
mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValueOnce({
|
||||
plugins: [openrouterPlugin, moonshotPlugin],
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
expect(
|
||||
loadManifestCatalogRowsForList({
|
||||
cfg: {},
|
||||
staticOnly: false,
|
||||
}).map((row) => row.ref),
|
||||
).toEqual(["moonshot/kimi-k2.6", "openrouter/auto"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,12 +13,13 @@ import {
|
||||
type PluginRegistrySnapshot,
|
||||
} from "../../plugins/plugin-registry.js";
|
||||
|
||||
function loadStaticManifestCatalogRowsForPluginIds(params: {
|
||||
function loadManifestCatalogRowsForPluginIds(params: {
|
||||
cfg: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
index: PluginRegistrySnapshot;
|
||||
pluginIds?: readonly string[];
|
||||
providerFilter?: string;
|
||||
staticOnly?: boolean;
|
||||
}): readonly NormalizedModelCatalogRow[] {
|
||||
if (params.pluginIds && params.pluginIds.length === 0) {
|
||||
return [];
|
||||
@@ -33,6 +34,9 @@ function loadStaticManifestCatalogRowsForPluginIds(params: {
|
||||
registry,
|
||||
...(params.providerFilter ? { providerFilter: params.providerFilter } : {}),
|
||||
});
|
||||
if (params.staticOnly === false) {
|
||||
return plan.rows;
|
||||
}
|
||||
const staticProviders = new Set(
|
||||
plan.entries.filter((entry) => entry.discovery === "static").map((entry) => entry.provider),
|
||||
);
|
||||
@@ -77,10 +81,11 @@ function resolveDeclaredModelCatalogPluginIds(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export function loadStaticManifestCatalogRowsForList(params: {
|
||||
export function loadManifestCatalogRowsForList(params: {
|
||||
cfg: OpenClawConfig;
|
||||
providerFilter?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
staticOnly?: boolean;
|
||||
}): readonly NormalizedModelCatalogRow[] {
|
||||
const providerFilter = params.providerFilter
|
||||
? normalizeModelCatalogProviderId(params.providerFilter)
|
||||
@@ -90,13 +95,14 @@ export function loadStaticManifestCatalogRowsForList(params: {
|
||||
env: params.env,
|
||||
});
|
||||
if (!providerFilter) {
|
||||
return loadStaticManifestCatalogRowsForPluginIds({
|
||||
return loadManifestCatalogRowsForPluginIds({
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
index,
|
||||
staticOnly: params.staticOnly,
|
||||
});
|
||||
}
|
||||
const conventionRows = loadStaticManifestCatalogRowsForPluginIds({
|
||||
const conventionRows = loadManifestCatalogRowsForPluginIds({
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
index,
|
||||
@@ -106,11 +112,12 @@ export function loadStaticManifestCatalogRowsForList(params: {
|
||||
providerFilter,
|
||||
}),
|
||||
providerFilter,
|
||||
staticOnly: params.staticOnly,
|
||||
});
|
||||
if (conventionRows.length > 0) {
|
||||
return conventionRows;
|
||||
}
|
||||
return loadStaticManifestCatalogRowsForPluginIds({
|
||||
return loadManifestCatalogRowsForPluginIds({
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
index,
|
||||
@@ -120,5 +127,17 @@ export function loadStaticManifestCatalogRowsForList(params: {
|
||||
providerFilter,
|
||||
}),
|
||||
providerFilter,
|
||||
staticOnly: params.staticOnly,
|
||||
});
|
||||
}
|
||||
|
||||
export function loadStaticManifestCatalogRowsForList(params: {
|
||||
cfg: OpenClawConfig;
|
||||
providerFilter?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): readonly NormalizedModelCatalogRow[] {
|
||||
return loadManifestCatalogRowsForList({
|
||||
...params,
|
||||
staticOnly: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
loadStaticManifestCatalogRowsForList: vi.fn(),
|
||||
loadManifestCatalogRowsForList: vi.fn(),
|
||||
loadProviderIndexCatalogRowsForList: vi.fn(),
|
||||
hasProviderStaticCatalogForFilter: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./list.manifest-catalog.js", () => ({
|
||||
loadStaticManifestCatalogRowsForList: mocks.loadStaticManifestCatalogRowsForList,
|
||||
loadManifestCatalogRowsForList: mocks.loadManifestCatalogRowsForList,
|
||||
}));
|
||||
|
||||
vi.mock("./list.provider-index-catalog.js", () => ({
|
||||
@@ -33,14 +33,14 @@ const catalogRow = {
|
||||
describe("planAllModelListSources", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.loadStaticManifestCatalogRowsForList.mockReturnValue([]);
|
||||
mocks.loadManifestCatalogRowsForList.mockReturnValue([]);
|
||||
mocks.loadProviderIndexCatalogRowsForList.mockReturnValue([]);
|
||||
mocks.hasProviderStaticCatalogForFilter.mockResolvedValue(false);
|
||||
});
|
||||
|
||||
it("uses installed manifest rows before provider index or runtime catalog sources", async () => {
|
||||
const { planAllModelListSources } = await import("./list.source-plan.js");
|
||||
mocks.loadStaticManifestCatalogRowsForList.mockReturnValueOnce([catalogRow]);
|
||||
mocks.loadManifestCatalogRowsForList.mockReturnValueOnce([catalogRow]);
|
||||
|
||||
const plan = await planAllModelListSources({
|
||||
all: true,
|
||||
@@ -54,6 +54,11 @@ describe("planAllModelListSources", () => {
|
||||
skipRuntimeModelSuppression: true,
|
||||
});
|
||||
expect(plan.manifestCatalogRows).toEqual([catalogRow]);
|
||||
expect(mocks.loadManifestCatalogRowsForList).toHaveBeenCalledWith({
|
||||
cfg: {},
|
||||
providerFilter: "moonshot",
|
||||
staticOnly: true,
|
||||
});
|
||||
expect(mocks.loadProviderIndexCatalogRowsForList).not.toHaveBeenCalled();
|
||||
expect(mocks.hasProviderStaticCatalogForFilter).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -78,6 +83,35 @@ describe("planAllModelListSources", () => {
|
||||
expect(mocks.hasProviderStaticCatalogForFilter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses the registry when provider-filtered manifest rows are refreshable", async () => {
|
||||
const { planAllModelListSources } = await import("./list.source-plan.js");
|
||||
mocks.loadManifestCatalogRowsForList.mockReturnValueOnce([]).mockReturnValueOnce([catalogRow]);
|
||||
|
||||
const plan = await planAllModelListSources({
|
||||
all: true,
|
||||
providerFilter: "openai",
|
||||
cfg: {},
|
||||
});
|
||||
|
||||
expect(plan).toMatchObject({
|
||||
kind: "registry",
|
||||
requiresInitialRegistry: true,
|
||||
skipRuntimeModelSuppression: false,
|
||||
});
|
||||
expect(plan.manifestCatalogRows).toEqual([catalogRow]);
|
||||
expect(mocks.loadManifestCatalogRowsForList).toHaveBeenNthCalledWith(1, {
|
||||
cfg: {},
|
||||
providerFilter: "openai",
|
||||
staticOnly: true,
|
||||
});
|
||||
expect(mocks.loadManifestCatalogRowsForList).toHaveBeenNthCalledWith(2, {
|
||||
cfg: {},
|
||||
providerFilter: "openai",
|
||||
staticOnly: false,
|
||||
});
|
||||
expect(mocks.loadProviderIndexCatalogRowsForList).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps scoped runtime catalog fallback separate from broad registry loading", async () => {
|
||||
const { planAllModelListSources } = await import("./list.source-plan.js");
|
||||
|
||||
@@ -98,7 +132,7 @@ describe("planAllModelListSources", () => {
|
||||
it("keeps broad all-model lists on the registry path with cheap catalog supplements", async () => {
|
||||
const { planAllModelListSources } = await import("./list.source-plan.js");
|
||||
const providerIndexRow = { ...catalogRow, source: "provider-index" };
|
||||
mocks.loadStaticManifestCatalogRowsForList.mockReturnValueOnce([catalogRow]);
|
||||
mocks.loadManifestCatalogRowsForList.mockReturnValueOnce([catalogRow]);
|
||||
mocks.loadProviderIndexCatalogRowsForList.mockReturnValueOnce([providerIndexRow]);
|
||||
|
||||
const plan = await planAllModelListSources({
|
||||
@@ -113,6 +147,10 @@ describe("planAllModelListSources", () => {
|
||||
});
|
||||
expect(plan.manifestCatalogRows).toEqual([catalogRow]);
|
||||
expect(plan.providerIndexCatalogRows).toEqual([providerIndexRow]);
|
||||
expect(mocks.loadManifestCatalogRowsForList).toHaveBeenCalledWith({
|
||||
cfg: {},
|
||||
staticOnly: false,
|
||||
});
|
||||
expect(mocks.hasProviderStaticCatalogForFilter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -51,11 +51,20 @@ export async function planAllModelListSources(params: {
|
||||
return createRegistryModelListSourcePlan();
|
||||
}
|
||||
|
||||
const { loadStaticManifestCatalogRowsForList } = await import("./list.manifest-catalog.js");
|
||||
const manifestCatalogRows = loadStaticManifestCatalogRowsForList({
|
||||
const { loadManifestCatalogRowsForList } = await import("./list.manifest-catalog.js");
|
||||
const staticManifestCatalogRows = loadManifestCatalogRowsForList({
|
||||
cfg: params.cfg,
|
||||
...(params.providerFilter ? { providerFilter: params.providerFilter } : {}),
|
||||
staticOnly: Boolean(params.providerFilter),
|
||||
});
|
||||
const manifestCatalogRows =
|
||||
params.providerFilter && staticManifestCatalogRows.length === 0
|
||||
? loadManifestCatalogRowsForList({
|
||||
cfg: params.cfg,
|
||||
providerFilter: params.providerFilter,
|
||||
staticOnly: false,
|
||||
})
|
||||
: staticManifestCatalogRows;
|
||||
if (!params.providerFilter) {
|
||||
const { loadProviderIndexCatalogRowsForList } =
|
||||
await import("./list.provider-index-catalog.js");
|
||||
@@ -70,6 +79,13 @@ export async function planAllModelListSources(params: {
|
||||
}
|
||||
|
||||
if (manifestCatalogRows.length > 0) {
|
||||
if (staticManifestCatalogRows.length === 0) {
|
||||
return createSourcePlan({
|
||||
kind: "registry",
|
||||
manifestCatalogRows,
|
||||
requiresInitialRegistry: true,
|
||||
});
|
||||
}
|
||||
return createSourcePlan({
|
||||
kind: "manifest",
|
||||
manifestCatalogRows,
|
||||
|
||||
Reference in New Issue
Block a user