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);
}