File size: 6,171 Bytes
f778c12 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | /** Reads the selected Gateway catalog or an explicitly identified local published view. */
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
import type {
ModelChoice,
ModelsListParams,
ModelsListResult,
} from "../../../packages/gateway-protocol/src/schema/agents-models-skills.js";
import { GATEWAY_SERVER_CAPS } from "../../../packages/gateway-protocol/src/server-capabilities.js";
import { sanitizeTerminalText } from "../../../packages/terminal-core/src/safe-text.js";
import { modelKey } from "../../agents/model-ref-shared.js";
import { ExpectedCliError } from "../../cli/failure-output.js";
import { requestExitAfterOneShotOutput } from "../../cli/one-shot-exit.js";
import { getRuntimeConfig } from "../../config/config.js";
import { callGateway, isImplicitLocalGatewayTarget } from "../../gateway/call.js";
import { readActiveGatewayLockIdentity } from "../../infra/gateway-lock.js";
import type { RuntimeEnv } from "../../runtime.js";
import { printModelTable } from "./list.table.js";
import type { ModelRow } from "./list.types.js";
import { loadModelsConfigWithSource } from "./load-config.js";
import { ensureFlagCompatibility, resolveModelsTargetAgent } from "./shared.js";
// The catalog worker permits three minutes; leave room for connection and result projection.
const MODEL_CATALOG_REFRESH_TIMEOUT_MS = 210_000;
function toCliModelRow(model: ModelChoice): ModelRow {
return {
key: modelKey(model.provider, model.id),
name: model.name,
input: model.input?.join("+") || "-",
contextWindow: model.contextWindow ?? null,
...(model.contextTokens !== undefined ? { contextTokens: model.contextTokens } : {}),
local: model.local ?? null,
available: model.available ?? null,
tags: [...new Set([...(model.tags ?? []), ...(model.alias ? [`alias:${model.alias}`] : [])])],
};
}
export async function modelsListCommand(
opts: {
all?: boolean;
refresh?: boolean;
local?: boolean;
provider?: string;
agent?: string;
json?: boolean;
plain?: boolean;
},
runtime: RuntimeEnv,
) {
ensureFlagCompatibility(opts);
const rawProvider = opts.provider?.trim();
if (rawProvider && /\s/u.test(rawProvider)) {
const message = `Invalid provider filter "${sanitizeTerminalText(rawProvider)}". Use a provider id such as "moonshot", not a display label.`;
throw new ExpectedCliError({ message, humanOutput: message, machineOutput: message });
}
const provider = rawProvider ? normalizeProviderId(rawProvider) : undefined;
// Gateway selection needs connection config, not local provider credentials or plugins.
const cfg = getRuntimeConfig({ skipPluginValidation: true });
const params: ModelsListParams = {
...(opts.agent?.trim() ? { agentId: opts.agent.trim() } : {}),
view: opts.all || provider ? "all" : "default",
...(provider ? { provider } : {}),
includeDetails: true,
...(opts.refresh ? { refresh: true } : {}),
};
const localTarget = await isImplicitLocalGatewayTarget({ config: cfg });
const explicitPort = Boolean(process.env.OPENCLAW_GATEWAY_PORT?.trim());
const gatewayOwner =
localTarget && !explicitPort
? await readActiveGatewayLockIdentity({ requireInspection: true })
: undefined;
let result: ModelsListResult;
if (!localTarget || explicitPort || gatewayOwner) {
// Once selected, this Gateway owns both success and failure. Never replace a failed
// connection or unsupported capability with a different local inventory.
result = await callGateway<ModelsListResult>({
config: cfg,
method: "models.list",
...(opts.refresh ? { timeoutMs: MODEL_CATALOG_REFRESH_TIMEOUT_MS } : {}),
requiredCapabilities: [GATEWAY_SERVER_CAPS.PUBLISHED_MODEL_CATALOG],
...(gatewayOwner ? { localPortOverride: gatewayOwner.port } : {}),
params,
});
} else {
runtime.error(
opts.refresh
? "Gateway is not running. Refreshing the local model catalog."
: "Gateway is not running. Showing the local cached model catalog. Use --refresh to discover provider models.",
);
const [
{ resolvePublishedModelCatalogOwner },
{ withPreparedModelCatalogOwner },
{ getPreparedModelRuntimeAuthMaterializations },
{ buildModelsListResult },
] = await Promise.all([
import("../../agents/prepared-model-catalog-owner.js"),
import("../../agents/prepared-model-catalog.js"),
import("../../agents/prepared-model-runtime-auth.js"),
import("../../gateway/server-methods/models-list-result.js"),
]);
const { resolvedConfig: localConfig } = await loadModelsConfigWithSource({
commandName: "models list",
runtime,
});
const { agentId, agentDir } = resolveModelsTargetAgent(localConfig, opts.agent, {
kind: "read",
});
result = await withPreparedModelCatalogOwner(
{
agentId,
agentDir,
config: localConfig,
readOnly: opts.refresh !== true,
...(opts.refresh ? { refreshFullCatalog: true } : {}),
},
async (snapshot) => {
const owner = resolvePublishedModelCatalogOwner(snapshot);
// Complete row projection and its final readiness reads before releasing a temporary owner.
return await buildModelsListResult({
source: {
kind: "published",
owner: {
...owner,
authMaterializations: getPreparedModelRuntimeAuthMaterializations(snapshot),
},
},
agentId,
params,
});
},
);
}
if (
result.refreshFailed ||
(opts.refresh && result.providerOutcomes?.some((outcome) => outcome.status !== "ready"))
) {
runtime.error(
"Model discovery could not refresh all providers. Showing the available published model list.",
);
}
const rows = result.models
.filter((model) => !opts.local || model.local === true)
.map(toCliModelRow);
if (rows.length === 0 && !opts.json && !opts.plain) {
runtime.log("No models found.");
} else {
printModelTable(rows, runtime, opts);
}
requestExitAfterOneShotOutput(runtime);
}
|