traderclaw-cli 1.0.51 → 1.0.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { execSync, spawn } from "child_process";
1
+ import { execFileSync, execSync, spawn } from "child_process";
2
2
  import { randomBytes } from "crypto";
3
3
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, statSync, writeFileSync } from "fs";
4
4
  import { homedir, tmpdir } from "os";
@@ -1214,8 +1214,21 @@ function persistXProfileIdentities(configPath, modeConfig, identities) {
1214
1214
  }
1215
1215
 
1216
1216
  function listProviderModels(provider) {
1217
- const cmd = `openclaw models list --all --provider ${shellQuote(provider)} --json`;
1218
- const raw = getCommandOutput(cmd);
1217
+ let raw;
1218
+ try {
1219
+ raw = execFileSync(
1220
+ "openclaw",
1221
+ ["models", "list", "--all", "--provider", provider, "--json"],
1222
+ {
1223
+ encoding: "utf-8",
1224
+ maxBuffer: 25 * 1024 * 1024,
1225
+ timeout: 20_000,
1226
+ env: NO_COLOR_ENV,
1227
+ },
1228
+ ).trim();
1229
+ } catch {
1230
+ return [];
1231
+ }
1219
1232
  if (!raw) return [];
1220
1233
  const parsed = extractJson(raw);
1221
1234
  if (!parsed) return [];
@@ -211,14 +211,21 @@ export function modelPreferenceScore(provider, modelId) {
211
211
  return score;
212
212
  }
213
213
 
214
+ /** Cap list size after scoring so huge catalogs stay fast for sort + JSON payloads. */
215
+ export const MAX_MODELS_PER_PROVIDER_SORT = 1500;
216
+
214
217
  /**
215
218
  * Best-first order for dropdowns and validation.
219
+ * Uses a score cache because sort() may compare the same ids many times.
216
220
  */
217
221
  export function sortModelsByPreference(provider, modelIds) {
218
222
  const items = [...new Set((modelIds || []).filter(Boolean))];
219
- return items.sort(
220
- (a, b) => modelPreferenceScore(provider, b) - modelPreferenceScore(provider, a) || String(a).localeCompare(String(b)),
221
- );
223
+ const cache = new Map();
224
+ const score = (id) => {
225
+ if (!cache.has(id)) cache.set(id, modelPreferenceScore(provider, id));
226
+ return cache.get(id);
227
+ };
228
+ return items.sort((a, b) => score(b) - score(a) || String(a).localeCompare(String(b)));
222
229
  }
223
230
 
224
231
  export function choosePreferredProviderModel(provider, models = []) {
@@ -5,11 +5,17 @@ import { readFileSync, writeFileSync, mkdirSync, appendFileSync, existsSync } fr
5
5
  import { join } from "path";
6
6
  import { homedir } from "os";
7
7
  import { randomUUID, createPrivateKey, sign as cryptoSign } from "crypto";
8
- import { execSync } from "child_process";
8
+ import { execFile, execSync } from "child_process";
9
+ import { promisify } from "util";
9
10
  import { createServer } from "http";
10
- import { sortModelsByPreference } from "./llm-model-preference.mjs";
11
+ import { sortModelsByPreference, MAX_MODELS_PER_PROVIDER_SORT } from "./llm-model-preference.mjs";
11
12
  import { resolvePluginPackageRoot } from "./resolve-plugin-root.mjs";
12
13
 
14
+ const execFileAsync = promisify(execFile);
15
+
16
+ /** Parallel per-provider `openclaw models list` — wall time ~max(single), not sum of all providers. */
17
+ const OPENCLAW_MODELS_PER_PROVIDER_TIMEOUT_MS = 16_000;
18
+
13
19
  const PLUGIN_ROOT = resolvePluginPackageRoot(import.meta.url);
14
20
  const PACKAGE_JSON = JSON.parse(readFileSync(join(PLUGIN_ROOT, "package.json"), "utf-8"));
15
21
  const VERSION = PACKAGE_JSON.version;
@@ -1766,7 +1772,7 @@ function parseJsonBody(req) {
1766
1772
  });
1767
1773
  }
1768
1774
 
1769
- function loadWizardLlmCatalog() {
1775
+ async function loadWizardLlmCatalogAsync() {
1770
1776
  const supportedProviders = new Set([
1771
1777
  "anthropic",
1772
1778
  "openai",
@@ -1830,34 +1836,54 @@ function loadWizardLlmCatalog() {
1830
1836
  return { ...fallback, warning: "openclaw_not_found" };
1831
1837
  }
1832
1838
 
1839
+ const providerIds = [...supportedProviders].sort((a, b) => a.localeCompare(b));
1840
+
1841
+ async function fetchModelsForProvider(provider) {
1842
+ try {
1843
+ const { stdout } = await execFileAsync(
1844
+ "openclaw",
1845
+ ["models", "list", "--all", "--provider", provider, "--json"],
1846
+ {
1847
+ encoding: "utf-8",
1848
+ maxBuffer: 25 * 1024 * 1024,
1849
+ timeout: OPENCLAW_MODELS_PER_PROVIDER_TIMEOUT_MS,
1850
+ env: NO_COLOR_ENV,
1851
+ },
1852
+ );
1853
+ return { provider, stdout };
1854
+ } catch (err) {
1855
+ return { provider, error: err };
1856
+ }
1857
+ }
1858
+
1833
1859
  try {
1834
- const raw = execSync("openclaw models list --all --json", {
1835
- encoding: "utf-8",
1836
- stdio: ["ignore", "pipe", "pipe"],
1837
- maxBuffer: 50 * 1024 * 1024,
1838
- timeout: 30_000,
1839
- env: NO_COLOR_ENV,
1840
- });
1841
- const parsed = extractJson(raw);
1842
- if (!parsed) throw new Error(`Could not extract JSON from openclaw models list output (first 200 chars): ${stripAnsi(raw).slice(0, 200)}`);
1843
- const models = Array.isArray(parsed?.models) ? parsed.models : [];
1860
+ const t0 = Date.now();
1861
+ const batches = await Promise.all(providerIds.map((p) => fetchModelsForProvider(p)));
1844
1862
  const providerMap = new Map();
1845
- for (const entry of models) {
1846
- if (!entry || typeof entry.key !== "string") continue;
1847
- const modelId = String(entry.key);
1848
- const slash = modelId.indexOf("/");
1849
- if (slash <= 0 || slash === modelId.length - 1) continue;
1850
- const provider = modelId.slice(0, slash);
1851
- const existing = providerMap.get(provider) || [];
1852
- existing.push({
1853
- id: modelId,
1854
- name: typeof entry.name === "string" && entry.name.trim() ? entry.name : modelId,
1855
- });
1856
- providerMap.set(provider, existing);
1863
+
1864
+ for (const batch of batches) {
1865
+ if (batch.error || !batch.stdout) continue;
1866
+ const parsed = extractJson(batch.stdout);
1867
+ if (!parsed) continue;
1868
+ const models = Array.isArray(parsed?.models) ? parsed.models : [];
1869
+ const want = batch.provider;
1870
+ for (const entry of models) {
1871
+ if (!entry || typeof entry.key !== "string") continue;
1872
+ const modelId = String(entry.key);
1873
+ const slash = modelId.indexOf("/");
1874
+ if (slash <= 0 || slash === modelId.length - 1) continue;
1875
+ const provider = modelId.slice(0, slash);
1876
+ if (provider !== want) continue;
1877
+ const existing = providerMap.get(provider) || [];
1878
+ existing.push({
1879
+ id: modelId,
1880
+ name: typeof entry.name === "string" && entry.name.trim() ? entry.name : modelId,
1881
+ });
1882
+ providerMap.set(provider, existing);
1883
+ }
1857
1884
  }
1858
1885
 
1859
- const providers = [...providerMap.keys()]
1860
- .sort((a, b) => a.localeCompare(b))
1886
+ const providers = providerIds
1861
1887
  .map((id) => {
1862
1888
  const rawModels = providerMap.get(id) || [];
1863
1889
  const sortedIds = sortModelsByPreference(
@@ -1865,7 +1891,8 @@ function loadWizardLlmCatalog() {
1865
1891
  rawModels.map((m) => m.id),
1866
1892
  );
1867
1893
  const byId = new Map(rawModels.map((m) => [m.id, m]));
1868
- const models = sortedIds.map((mid) => byId.get(mid)).filter(Boolean);
1894
+ const limitedIds = sortedIds.slice(0, MAX_MODELS_PER_PROVIDER_SORT);
1895
+ const models = limitedIds.map((mid) => byId.get(mid)).filter(Boolean);
1869
1896
  return { id, models };
1870
1897
  })
1871
1898
  .filter((entry) => supportedProviders.has(entry.id))
@@ -1875,10 +1902,12 @@ function loadWizardLlmCatalog() {
1875
1902
  return { ...fallback, warning: "openclaw_model_catalog_empty" };
1876
1903
  }
1877
1904
 
1905
+ const elapsedMs = Date.now() - t0;
1878
1906
  return {
1879
1907
  source: "openclaw",
1880
1908
  providers,
1881
1909
  generatedAt: new Date().toISOString(),
1910
+ catalogFetchMs: elapsedMs,
1882
1911
  };
1883
1912
  } catch (err) {
1884
1913
  const detail = err?.message || String(err);
@@ -2629,7 +2658,15 @@ async function cmdInstall(args) {
2629
2658
  }
2630
2659
 
2631
2660
  if (req.method === "GET" && req.url === "/api/llm/options") {
2632
- respondJson(200, loadWizardLlmCatalog());
2661
+ try {
2662
+ const payload = await loadWizardLlmCatalogAsync();
2663
+ respondJson(200, payload);
2664
+ } catch (err) {
2665
+ respondJson(500, {
2666
+ source: "error",
2667
+ message: err instanceof Error ? err.message : String(err),
2668
+ });
2669
+ }
2633
2670
  return;
2634
2671
  }
2635
2672
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "traderclaw-cli",
3
- "version": "1.0.51",
3
+ "version": "1.0.52",
4
4
  "description": "Global TraderClaw CLI (install --wizard, setup, precheck). Installs solana-traderclaw as a dependency for OpenClaw plugin files.",
5
5
  "type": "module",
6
6
  "bin": {