mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:31:06 +00:00
fix: show provider catalog models in all list
This commit is contained in:
@@ -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"), {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
121
src/commands/models/list.provider-catalog.ts
Normal file
121
src/commands/models/list.provider-catalog.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user