fix: show provider catalog models in all list

This commit is contained in:
Shakker
2026-04-22 01:48:18 +01:00
committed by Shakker
parent 11f38afbfc
commit cc78dd2044
6 changed files with 204 additions and 2 deletions

View File

@@ -17,6 +17,9 @@ const listProfilesForProvider = vi.fn().mockReturnValue([]);
const resolveEnvApiKey = vi.fn().mockReturnValue(undefined);
const resolveAwsSdkEnvVarName = vi.fn().mockReturnValue(undefined);
const hasUsableCustomProviderApiKey = vi.fn().mockReturnValue(false);
const loadProviderCatalogModelsForList = vi.fn<() => Promise<Array<Record<string, unknown>>>>(
async () => [],
);
const shouldSuppressBuiltInModel = vi.fn().mockReturnValue(false);
const modelRegistryState = {
models: [] as Array<Record<string, unknown>>,
@@ -72,6 +75,7 @@ vi.mock("./models/list.runtime.js", () => {
resolveAwsSdkEnvVarName,
hasUsableCustomProviderApiKey,
loadModelCatalog: vi.fn(async () => []),
loadProviderCatalogModelsForList,
discoverAuthStorage: () => ({}) as unknown,
discoverModels: () => new MockModelRegistry() as unknown,
resolveModelWithRegistry: ({
@@ -132,6 +136,8 @@ beforeEach(() => {
getRuntimeConfig.mockReturnValue({});
listProfilesForProvider.mockReturnValue([]);
ensureOpenClawModelsJson.mockClear();
loadProviderCatalogModelsForList.mockReset();
loadProviderCatalogModelsForList.mockResolvedValue([]);
shouldSuppressBuiltInModel.mockReset();
shouldSuppressBuiltInModel.mockReturnValue(false);
readConfigFileSnapshotForWrite.mockClear();
@@ -179,6 +185,14 @@ describe("models list/status", () => {
baseUrl: "https://chatgpt.com/backend-api",
contextWindow: 128000,
};
const MOONSHOT_MODEL = {
provider: "moonshot",
id: "kimi-k2.6",
name: "Kimi K2.6",
input: ["text", "image"],
baseUrl: "https://api.moonshot.ai/v1",
contextWindow: 262144,
};
const AZURE_OPENAI_SPARK_MODEL = {
provider: "azure-openai-responses",
id: "gpt-5.3-codex-spark",
@@ -337,6 +351,24 @@ describe("models list/status", () => {
expect(payload.models[0]?.available).toBe(false);
});
it("models list all includes unauthenticated provider catalog rows", async () => {
setDefaultZaiRegistry({ available: false });
loadProviderCatalogModelsForList.mockResolvedValueOnce([MOONSHOT_MODEL]);
const runtime = makeRuntime();
await modelsListCommand({ all: true, provider: "moonshot", json: true }, runtime);
const payload = parseJsonLog(runtime);
expect(payload.models).toEqual([
expect.objectContaining({
key: "moonshot/kimi-k2.6",
name: "Kimi K2.6",
available: false,
missing: false,
}),
]);
});
it("models list does not treat availability-unavailable code as discovery fallback", async () => {
configureGoogleAntigravityModel("claude-opus-4-6-thinking");
modelRegistryState.getAllError = Object.assign(new Error("model discovery failed"), {

View File

@@ -58,8 +58,10 @@ const mocks = vi.hoisted(() => {
loadModelsConfigWithSource: vi.fn(),
ensureOpenClawModelsJson: vi.fn(),
ensureAuthProfileStore: vi.fn(),
resolveOpenClawAgentDir: vi.fn(),
loadModelRegistry: vi.fn(),
loadModelCatalog: vi.fn(),
loadProviderCatalogModelsForList: vi.fn(),
resolveConfiguredEntries: vi.fn(),
printModelTable: vi.fn(),
listProfilesForProvider: vi.fn(),
@@ -75,6 +77,7 @@ function resetMocks() {
});
mocks.ensureOpenClawModelsJson.mockResolvedValue({ wrote: false });
mocks.ensureAuthProfileStore.mockReturnValue({ version: 1, profiles: {}, order: {} });
mocks.resolveOpenClawAgentDir.mockReturnValue("/tmp/openclaw-agent");
mocks.loadModelRegistry.mockResolvedValue({
models: [],
availableKeys: new Set(),
@@ -83,6 +86,7 @@ function resetMocks() {
},
});
mocks.loadModelCatalog.mockResolvedValue([]);
mocks.loadProviderCatalogModelsForList.mockResolvedValue([]);
mocks.resolveConfiguredEntries.mockReturnValue({
entries: [
{
@@ -138,8 +142,10 @@ function installModelsListCommandForwardCompatMocks() {
vi.doMock("./list.runtime.js", () => ({
ensureOpenClawModelsJson: mocks.ensureOpenClawModelsJson,
ensureAuthProfileStore: mocks.ensureAuthProfileStore,
resolveOpenClawAgentDir: mocks.resolveOpenClawAgentDir,
listProfilesForProvider: mocks.listProfilesForProvider,
loadModelCatalog: mocks.loadModelCatalog,
loadProviderCatalogModelsForList: mocks.loadProviderCatalogModelsForList,
resolveModelWithRegistry: mocks.resolveModelWithRegistry,
resolveEnvApiKey: vi.fn().mockReturnValue(undefined),
resolveAwsSdkEnvVarName: vi.fn().mockReturnValue(undefined),
@@ -160,6 +166,7 @@ async function buildAllOpenAiCodexRows(opts: { supplementCatalog?: boolean } = {
const rows: unknown[] = [];
const context = {
cfg: mocks.resolvedConfig,
agentDir: "/tmp/openclaw-agent",
authStore: mocks.ensureAuthProfileStore(),
availableKeys: loaded.availableKeys,
configuredByKey: new Map(),

View File

@@ -26,12 +26,14 @@ export async function modelsListCommand(
runtime: RuntimeEnv,
) {
ensureFlagCompatibility(opts);
const { ensureAuthProfileStore, ensureOpenClawModelsJson } = await import("./list.runtime.js");
const { ensureAuthProfileStore, ensureOpenClawModelsJson, resolveOpenClawAgentDir } =
await import("./list.runtime.js");
const { sourceConfig, resolvedConfig: cfg } = await loadModelsConfigWithSource({
commandName: "models list",
runtime,
});
const authStore = ensureAuthProfileStore();
const agentDir = resolveOpenClawAgentDir();
const providerFilter = (() => {
const raw = opts.provider?.trim();
if (!raw) {
@@ -70,6 +72,7 @@ export async function modelsListCommand(
const rows: ModelRow[] = [];
const rowContext = {
cfg,
agentDir,
authStore,
availableKeys,
configuredByKey,

View File

@@ -0,0 +1,121 @@
import type { Api, Model } from "@mariozechner/pi-ai";
import { normalizeProviderId } from "../../agents/provider-id.js";
import type { ModelProviderConfig } from "../../config/types.models.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import {
groupPluginDiscoveryProvidersByOrder,
normalizePluginDiscoveryResult,
resolvePluginDiscoveryProviders,
runProviderCatalog,
} from "../../plugins/provider-discovery.js";
import { resolveOwningPluginIdsForProvider } from "../../plugins/providers.js";
const CATALOG_DISPLAY_API_KEY = "__openclaw_catalog_display__";
const DISCOVERY_ORDERS = ["simple", "profile", "paired", "late"] as const;
const SELF_HOSTED_DISCOVERY_PROVIDER_IDS = new Set(["lmstudio", "ollama", "sglang", "vllm"]);
function modelFromProviderCatalog(params: {
provider: string;
providerConfig: ModelProviderConfig;
model: ModelProviderConfig["models"][number];
}): Model<Api> {
return {
id: params.model.id,
name: params.model.name || params.model.id,
provider: params.provider,
api: params.model.api ?? params.providerConfig.api ?? "openai-responses",
baseUrl: params.providerConfig.baseUrl,
reasoning: params.model.reasoning,
input: params.model.input ?? ["text"],
cost: params.model.cost,
contextWindow: params.model.contextWindow,
contextTokens: params.model.contextTokens,
maxTokens: params.model.maxTokens,
headers: params.model.headers,
compat: params.model.compat,
} as Model<Api>;
}
export async function loadProviderCatalogModelsForList(params: {
cfg: OpenClawConfig;
agentDir: string;
env?: NodeJS.ProcessEnv;
providerFilter?: string;
}): Promise<Model<Api>[]> {
const env = params.env ?? process.env;
const providerFilter = params.providerFilter ? normalizeProviderId(params.providerFilter) : "";
const onlyPluginIds = providerFilter
? resolveOwningPluginIdsForProvider({
provider: providerFilter,
config: params.cfg,
env,
})
: undefined;
const providers = await resolvePluginDiscoveryProviders({
config: params.cfg,
env,
...(onlyPluginIds ? { onlyPluginIds } : {}),
});
const byOrder = groupPluginDiscoveryProvidersByOrder(providers);
const rows: Model<Api>[] = [];
const seen = new Set<string>();
for (const order of DISCOVERY_ORDERS) {
for (const provider of byOrder[order] ?? []) {
if (!providerFilter && SELF_HOSTED_DISCOVERY_PROVIDER_IDS.has(provider.id)) {
continue;
}
let result: Awaited<ReturnType<typeof runProviderCatalog>> | null;
try {
result = await runProviderCatalog({
provider,
config: params.cfg,
agentDir: params.agentDir,
env,
resolveProviderApiKey: () => ({
apiKey: CATALOG_DISPLAY_API_KEY,
}),
resolveProviderAuth: () => ({
apiKey: CATALOG_DISPLAY_API_KEY,
mode: "api_key",
source: "env",
}),
});
} catch {
result = null;
}
const normalized = normalizePluginDiscoveryResult({ provider, result });
for (const [providerIdRaw, providerConfig] of Object.entries(normalized)) {
const providerId = normalizeProviderId(providerIdRaw);
if (providerFilter && providerId !== providerFilter) {
continue;
}
if (!providerId || !Array.isArray(providerConfig.models)) {
continue;
}
for (const model of providerConfig.models) {
const key = `${providerId}/${model.id}`;
if (seen.has(key)) {
continue;
}
seen.add(key);
rows.push(
modelFromProviderCatalog({
provider: providerId,
providerConfig,
model,
}),
);
}
}
}
}
return rows.toSorted((left, right) => {
const provider = left.provider.localeCompare(right.provider);
if (provider !== 0) {
return provider;
}
return left.id.localeCompare(right.id);
});
}

View File

@@ -5,7 +5,11 @@ import { shouldSuppressBuiltInModel } from "../../agents/model-suppression.js";
import { normalizeProviderId } from "../../agents/provider-id.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { loadModelRegistry, toModelRow } from "./list.registry.js";
import { loadModelCatalog, resolveModelWithRegistry } from "./list.runtime.js";
import {
loadModelCatalog,
loadProviderCatalogModelsForList,
resolveModelWithRegistry,
} from "./list.runtime.js";
import type { ConfiguredEntry, ModelRow } from "./list.types.js";
import { isLocalBaseUrl, modelKey } from "./shared.js";
@@ -18,6 +22,7 @@ type RowFilter = {
type RowBuilderContext = {
cfg: OpenClawConfig;
agentDir: string;
authStore: AuthProfileStore;
availableKeys?: Set<string>;
configuredByKey: ConfiguredByKey;
@@ -154,6 +159,39 @@ export async function appendCatalogSupplementRows(params: {
);
params.seenKeys.add(key);
}
for (const model of await loadProviderCatalogModelsForList({
cfg: params.context.cfg,
agentDir: params.context.agentDir,
providerFilter: params.context.filter.provider,
})) {
if (!matchesRowFilter(params.context.filter, model)) {
continue;
}
if (
shouldSuppressBuiltInModel({
provider: model.provider,
id: model.id,
baseUrl: model.baseUrl,
config: params.context.cfg,
})
) {
continue;
}
const key = modelKey(model.provider, model.id);
if (params.seenKeys.has(key)) {
continue;
}
params.rows.push(
buildRow({
model,
key,
context: params.context,
allowProviderAvailabilityFallback: !params.context.discoveredKeys.has(key),
}),
);
params.seenKeys.add(key);
}
}
export function appendConfiguredRows(params: {

View File

@@ -10,3 +10,4 @@ export {
export { loadModelCatalog } from "../../agents/model-catalog.js";
export { resolveModelWithRegistry } from "../../agents/pi-embedded-runner/model.js";
export { discoverAuthStorage, discoverModels } from "../../agents/pi-model-discovery.js";
export { loadProviderCatalogModelsForList } from "./list.provider-catalog.js";