@wrongstack/cli 0.306.4 → 0.307.0
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.
- package/dist/{auth-L3WGSGDX.js → auth-TVOOHL4Q.js} +45 -106
- package/dist/boot/short-circuit-flags.d.ts +2 -2
- package/dist/chimera-cascade-evidence.d.ts +1 -1
- package/dist/chunk-2VMJ3E5Z.js +71 -0
- package/dist/{chunk-RHLQT3EV.js → chunk-CB35UVXO.js} +1636 -1528
- package/dist/{chunk-YKBRFSGL.js → chunk-DOJLAULS.js} +28 -28
- package/dist/{chunk-KXN5HZDL.js → chunk-PKHGLWUJ.js} +24 -6
- package/dist/{chunk-HITHEXE3.js → chunk-TOG7OMKJ.js} +2 -2
- package/dist/{chunk-ARWGQUB3.js → chunk-XSG33WHN.js} +15 -5
- package/dist/{cli-main-4TZCCZR6.js → cli-main-YDOZZOZA.js} +21606 -20849
- package/dist/cli-main.d.ts +0 -20
- package/dist/{execution-547PM3NI.js → execution-CGT6J5ZN.js} +304 -272
- package/dist/fleet/host-learning-scheduler.d.ts +27 -0
- package/dist/fleet/host-shadow-manager.d.ts +64 -0
- package/dist/fleet/host.d.ts +5 -140
- package/dist/{hq-ZK6PGN3W.js → hq-PA6RH2UW.js} +2 -2
- package/dist/hq-server/mailbox-gateway-manager.d.ts +32 -0
- package/dist/hq-server/routes/auth/bootstrap-routes.d.ts +5 -0
- package/dist/hq-server/routes/auth/common.d.ts +11 -0
- package/dist/hq-server/routes/auth/password-routes.d.ts +9 -0
- package/dist/hq-server/routes/auth/session-audit-routes.d.ts +6 -0
- package/dist/hq-server/routes/auth/totp-routes.d.ts +9 -0
- package/dist/hq-server/routes/auth-handlers.d.ts +5 -132
- package/dist/hq-server/routes/command-handlers.d.ts +7 -0
- package/dist/hq-server/routes/data-handlers.d.ts +8 -0
- package/dist/hq-server/routes/mailbox-handlers.d.ts +22 -0
- package/dist/hq-server/routes.d.ts +3 -3
- package/dist/hq-server/server-lifecycle.d.ts +37 -0
- package/dist/hq-server/snapshot.d.ts +6 -5
- package/dist/hq-server/types.d.ts +4 -0
- package/dist/hq-server/upgrade-handler.d.ts +34 -0
- package/dist/hq-server/ws-client-events.d.ts +25 -0
- package/dist/hq-server/ws-leader.d.ts +47 -0
- package/dist/hq-server/ws-resume.d.ts +27 -0
- package/dist/hq-server/ws.d.ts +4 -71
- package/dist/{hq-server-IVZLVCFV.js → hq-server-PBRU275L.js} +2 -2
- package/dist/hq-server.d.ts +0 -38
- package/dist/index.js +1267 -122
- package/dist/{mcp-W7HHN7IA.js → mcp-CMLFHRG2.js} +14 -7
- package/dist/mcp-serve.d.ts +12 -1
- package/dist/{modeldiag-6VS5KDP5.js → modeldiag-ICMPHT5L.js} +868 -831
- package/dist/{providers-models-HQN3AT3K.js → providers-models-4FAKM2WO.js} +4 -4
- package/dist/repl-eternal-loop.d.ts +8 -0
- package/dist/repl-goal-banner.d.ts +21 -0
- package/dist/slash-commands/brain-council.d.ts +36 -0
- package/dist/slash-commands/brain-status.d.ts +12 -0
- package/dist/slash-commands/kanban-board-handlers.d.ts +13 -0
- package/dist/slash-commands/kanban-lifecycle-diagnostics.d.ts +16 -0
- package/dist/slash-commands/kanban-task-subcommands.d.ts +8 -0
- package/dist/slash-commands/memory-audience.d.ts +14 -0
- package/dist/slash-commands/memory-gather.d.ts +9 -0
- package/dist/slash-commands/memory-stats.d.ts +5 -0
- package/dist/slash-commands/settings-mutations.d.ts +6 -0
- package/dist/slash-commands/settings-view.d.ts +5 -0
- package/dist/subcommands/handlers/modeldiag-bench.d.ts +10 -0
- package/dist/subcommands/handlers/modeldiag-eval.d.ts +27 -0
- package/dist/subcommands/handlers/modeldiag-test.d.ts +8 -0
- package/dist/{version-help-ASKQYXDJ.js → version-help-TFD6VPF3.js} +2 -2
- package/dist/vibe-protocol-wiring.d.ts +10 -0
- package/dist/webui-server/credential-watcher.d.ts +15 -0
- package/dist/webui-server/domain-handlers.d.ts +21 -0
- package/dist/webui-server/route-contexts.d.ts +41 -0
- package/dist/{webui-server-FTG7JAZU.js → webui-server-IPHGJGXE.js} +515 -437
- package/dist/wiring/cli-execute-builder.d.ts +88 -0
- package/dist/wiring/cli-heap-watchdog.d.ts +33 -0
- package/dist/wiring/cli-prompt-and-tools-setup.d.ts +25 -0
- package/dist/wiring/cli-slash-commands-setup.d.ts +79 -0
- package/dist/wiring/heap-watchdog-setup.d.ts +53 -0
- package/dist/wiring/metrics.d.ts +1 -0
- package/dist/wiring/replay-governance-setup.d.ts +31 -0
- package/package.json +27 -27
|
@@ -1,208 +1,26 @@
|
|
|
1
1
|
import {
|
|
2
2
|
discoverAndMergeProviders
|
|
3
3
|
} from "./chunk-52C7PD2H.js";
|
|
4
|
+
import {
|
|
5
|
+
activeProfileConfigPath
|
|
6
|
+
} from "./chunk-YMXXOOFN.js";
|
|
4
7
|
import {
|
|
5
8
|
mutateConfigProviders,
|
|
6
9
|
normalizeKeys,
|
|
7
10
|
writeKeysBack
|
|
8
11
|
} from "./chunk-SZ42FYPT.js";
|
|
9
|
-
import {
|
|
10
|
-
activeProfileConfigPath
|
|
11
|
-
} from "./chunk-YMXXOOFN.js";
|
|
12
12
|
import "./chunk-7OCVIDC7.js";
|
|
13
13
|
|
|
14
14
|
// src/subcommands/handlers/modeldiag.ts
|
|
15
15
|
import * as fs from "node:fs/promises";
|
|
16
|
-
import { color as
|
|
17
|
-
import { makeProviderFromConfig, setOAuthTokenPersister } from "@wrongstack/providers";
|
|
16
|
+
import { color as color5 } from "@wrongstack/core/utils";
|
|
18
17
|
|
|
19
|
-
// src/subcommands/handlers/
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (typeof value !== "string") return void 0;
|
|
26
|
-
const values = value.split(",").map((part) => part.trim()).filter(Boolean);
|
|
27
|
-
return values.length > 0 ? values : void 0;
|
|
28
|
-
}
|
|
29
|
-
function positiveInteger(value, fallback, label) {
|
|
30
|
-
if (value === void 0) return fallback;
|
|
31
|
-
const parsed = typeof value === "string" ? Number(value) : Number.NaN;
|
|
32
|
-
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
33
|
-
throw new Error(`${label} must be a positive integer.`);
|
|
34
|
-
}
|
|
35
|
-
return parsed;
|
|
36
|
-
}
|
|
37
|
-
function parseModelSmokeOptions(args, flags = {}) {
|
|
38
|
-
const local = /* @__PURE__ */ new Map();
|
|
39
|
-
for (let i = 0; i < args.length; i++) {
|
|
40
|
-
const arg = args[i];
|
|
41
|
-
if (!arg?.startsWith("--")) continue;
|
|
42
|
-
const eq = arg.indexOf("=");
|
|
43
|
-
if (eq >= 0) {
|
|
44
|
-
local.set(arg.slice(2, eq), arg.slice(eq + 1));
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
const name = arg.slice(2);
|
|
48
|
-
const next = args[i + 1];
|
|
49
|
-
if (next && !next.startsWith("--")) {
|
|
50
|
-
local.set(name, next);
|
|
51
|
-
i++;
|
|
52
|
-
} else {
|
|
53
|
-
local.set(name, true);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
const get = (name) => local.get(name) ?? flags[name];
|
|
57
|
-
const providerFilter = csv(get("providers") ?? get("provider"));
|
|
58
|
-
const modelFilter = csv(get("models") ?? get("model"));
|
|
59
|
-
return {
|
|
60
|
-
allModels: get("all-models") === true || get("all-models") === "true",
|
|
61
|
-
planOnly: get("plan") === true || get("plan") === "true",
|
|
62
|
-
json: get("json") === true || get("json") === "true",
|
|
63
|
-
timeoutMs: positiveInteger(get("timeout"), DEFAULT_TIMEOUT_MS, "--timeout"),
|
|
64
|
-
maxTokens: positiveInteger(get("max-tokens"), DEFAULT_MAX_TOKENS, "--max-tokens"),
|
|
65
|
-
...providerFilter ? { providerFilter } : {},
|
|
66
|
-
...modelFilter ? { modelFilter } : {}
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
function pushUnique(target, values) {
|
|
70
|
-
const seen = new Set(target);
|
|
71
|
-
for (const value of values) {
|
|
72
|
-
const normalized = value?.trim();
|
|
73
|
-
if (!normalized || seen.has(normalized)) continue;
|
|
74
|
-
seen.add(normalized);
|
|
75
|
-
target.push(normalized);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
async function catalogModelsFor(providerId, providerConfig, registry) {
|
|
79
|
-
const direct = await registry.getProvider(providerId).catch(() => void 0);
|
|
80
|
-
const inherited = direct ?? (providerConfig?.type && providerConfig.type !== providerId ? await registry.getProvider(providerConfig.type).catch(() => void 0) : void 0);
|
|
81
|
-
return inherited?.models ?? [];
|
|
82
|
-
}
|
|
83
|
-
function supportsTextCompletion(model) {
|
|
84
|
-
const outputs = model?.modalities?.output;
|
|
85
|
-
return !outputs?.length || outputs.includes("text");
|
|
86
|
-
}
|
|
87
|
-
async function buildModelSmokeTargets(config, registry, options) {
|
|
88
|
-
const providerIds = Object.keys(config.providers ?? {});
|
|
89
|
-
if (config.provider && !providerIds.includes(config.provider)) providerIds.push(config.provider);
|
|
90
|
-
const providerFilter = options.providerFilter ? new Set(options.providerFilter.map((id) => id.toLowerCase())) : void 0;
|
|
91
|
-
const modelFilter = options.modelFilter ? new Set(options.modelFilter) : void 0;
|
|
92
|
-
const targets = [];
|
|
93
|
-
for (const providerId of providerIds) {
|
|
94
|
-
if (providerFilter && !providerFilter.has(providerId.toLowerCase())) continue;
|
|
95
|
-
const saved = config.providers?.[providerId];
|
|
96
|
-
const catalogModels = await catalogModelsFor(providerId, saved, registry);
|
|
97
|
-
const catalogById = new Map(catalogModels.map((model) => [model.id, model]));
|
|
98
|
-
const textCatalogModels = catalogModels.filter(supportsTextCompletion);
|
|
99
|
-
const candidates = [];
|
|
100
|
-
if (options.allModels) {
|
|
101
|
-
pushUnique(candidates, saved?.models ?? []);
|
|
102
|
-
pushUnique(candidates, Object.keys(saved?.customModels ?? {}));
|
|
103
|
-
pushUnique(
|
|
104
|
-
candidates,
|
|
105
|
-
textCatalogModels.map((model) => model.id)
|
|
106
|
-
);
|
|
107
|
-
if (providerId === config.provider) pushUnique(candidates, [config.model]);
|
|
108
|
-
pushUnique(candidates, [saved?.model]);
|
|
109
|
-
} else {
|
|
110
|
-
const representative = (providerId === config.provider ? config.model : void 0) ?? saved?.model ?? saved?.models?.find(Boolean) ?? Object.keys(saved?.customModels ?? {})[0] ?? await registry.suggestModel(providerId).catch(() => void 0) ?? textCatalogModels[0]?.id;
|
|
111
|
-
pushUnique(candidates, [representative]);
|
|
112
|
-
}
|
|
113
|
-
for (const modelId of candidates) {
|
|
114
|
-
if (modelFilter && !modelFilter.has(modelId)) continue;
|
|
115
|
-
if (!supportsTextCompletion(catalogById.get(modelId))) continue;
|
|
116
|
-
targets.push({ providerId, modelId });
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return targets;
|
|
120
|
-
}
|
|
121
|
-
function classifyFailure(err) {
|
|
122
|
-
if (err instanceof ProviderError || ProviderError.isProviderError(err)) {
|
|
123
|
-
const providerError = err;
|
|
124
|
-
return {
|
|
125
|
-
error: providerError.describe(),
|
|
126
|
-
errorKind: providerError.kind,
|
|
127
|
-
httpStatus: providerError.status
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
const isTimeout = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError") || /timed?\s*out/i.test(toErrorMessage(err));
|
|
131
|
-
return {
|
|
132
|
-
error: toErrorMessage(err),
|
|
133
|
-
errorKind: isTimeout ? "timeout" : "unknown"
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
async function runModelSmokeTests(params) {
|
|
137
|
-
const results = [];
|
|
138
|
-
let providerId;
|
|
139
|
-
let provider;
|
|
140
|
-
let providerError;
|
|
141
|
-
for (let index = 0; index < params.targets.length; index++) {
|
|
142
|
-
const target = params.targets[index];
|
|
143
|
-
params.onTargetStart?.(target, index, params.targets.length);
|
|
144
|
-
const started = Date.now();
|
|
145
|
-
if (target.providerId !== providerId) {
|
|
146
|
-
providerId = target.providerId;
|
|
147
|
-
provider = void 0;
|
|
148
|
-
providerError = void 0;
|
|
149
|
-
try {
|
|
150
|
-
provider = await params.createProvider(target.providerId);
|
|
151
|
-
} catch (err) {
|
|
152
|
-
providerError = err;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
if (!provider) {
|
|
156
|
-
const failure = classifyFailure(providerError ?? new Error("Provider could not be created."));
|
|
157
|
-
const result = {
|
|
158
|
-
providerId: target.providerId,
|
|
159
|
-
modelId: target.modelId,
|
|
160
|
-
status: "failed",
|
|
161
|
-
latencyMs: Date.now() - started,
|
|
162
|
-
...failure
|
|
163
|
-
};
|
|
164
|
-
results.push(result);
|
|
165
|
-
params.onTargetComplete?.(result, index, params.targets.length);
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
try {
|
|
169
|
-
const response = await provider.complete(
|
|
170
|
-
{
|
|
171
|
-
model: target.modelId,
|
|
172
|
-
messages: [
|
|
173
|
-
{
|
|
174
|
-
role: "user",
|
|
175
|
-
content: [{ type: "text", text: "Reply with exactly: OK" }]
|
|
176
|
-
}
|
|
177
|
-
],
|
|
178
|
-
maxTokens: params.options.maxTokens
|
|
179
|
-
},
|
|
180
|
-
{ signal: AbortSignal.timeout(params.options.timeoutMs) }
|
|
181
|
-
);
|
|
182
|
-
const result = {
|
|
183
|
-
providerId: target.providerId,
|
|
184
|
-
modelId: target.modelId,
|
|
185
|
-
status: "passed",
|
|
186
|
-
latencyMs: Date.now() - started,
|
|
187
|
-
stopReason: response.stopReason,
|
|
188
|
-
usage: response.usage
|
|
189
|
-
};
|
|
190
|
-
results.push(result);
|
|
191
|
-
params.onTargetComplete?.(result, index, params.targets.length);
|
|
192
|
-
} catch (err) {
|
|
193
|
-
const result = {
|
|
194
|
-
providerId: target.providerId,
|
|
195
|
-
modelId: target.modelId,
|
|
196
|
-
status: "failed",
|
|
197
|
-
latencyMs: Date.now() - started,
|
|
198
|
-
...classifyFailure(err)
|
|
199
|
-
};
|
|
200
|
-
results.push(result);
|
|
201
|
-
params.onTargetComplete?.(result, index, params.targets.length);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return results;
|
|
205
|
-
}
|
|
18
|
+
// src/subcommands/handlers/modeldiag-bench.ts
|
|
19
|
+
import { color as color3, toErrorMessage } from "@wrongstack/core/utils";
|
|
20
|
+
|
|
21
|
+
// src/subcommands/handlers/modeldiag-eval.ts
|
|
22
|
+
import { makeProviderFromConfig } from "@wrongstack/providers";
|
|
23
|
+
import { color as color2 } from "@wrongstack/core/utils";
|
|
206
24
|
|
|
207
25
|
// src/subcommands/handlers/modeldiag-profiles.ts
|
|
208
26
|
import { color } from "@wrongstack/core/utils";
|
|
@@ -447,7 +265,7 @@ function roleCat(role) {
|
|
|
447
265
|
return ROLE_CATEGORY[role] ?? "general";
|
|
448
266
|
}
|
|
449
267
|
|
|
450
|
-
// src/subcommands/handlers/modeldiag.ts
|
|
268
|
+
// src/subcommands/handlers/modeldiag-eval.ts
|
|
451
269
|
function createProviderForId(providerId, cfg) {
|
|
452
270
|
const savedCfg = cfg.providers?.[providerId];
|
|
453
271
|
const cfgWithType = Object.assign(
|
|
@@ -511,79 +329,836 @@ Output ONLY a ranked list, one per line:
|
|
|
511
329
|
return responses.map((_, i) => i);
|
|
512
330
|
}
|
|
513
331
|
}
|
|
514
|
-
async function
|
|
515
|
-
|
|
516
|
-
return `${color2.red("Models cache not available")}.`;
|
|
517
|
-
}
|
|
518
|
-
try {
|
|
519
|
-
const raw = await fs.readFile(cachePath, "utf8");
|
|
520
|
-
const parsed = JSON.parse(raw);
|
|
521
|
-
const payload = parsed.payload ?? parsed;
|
|
522
|
-
return Object.entries(payload).map(([id, p]) => ({
|
|
523
|
-
id: p.id ?? id,
|
|
524
|
-
name: p.name ?? id,
|
|
525
|
-
family: p.npm ?? id,
|
|
526
|
-
models: Object.values(p.models ?? {}).map(
|
|
527
|
-
(m) => ({
|
|
528
|
-
id: m.id,
|
|
529
|
-
name: m.name,
|
|
530
|
-
capabilities: {
|
|
531
|
-
contextWindow: m.limit?.context,
|
|
532
|
-
maxOutputTokens: m.limit?.output
|
|
533
|
-
},
|
|
534
|
-
pricing: m.cost
|
|
535
|
-
})
|
|
536
|
-
)
|
|
537
|
-
}));
|
|
538
|
-
} catch {
|
|
539
|
-
return `${color2.amber("Models cache not available")}. Run wstack sync-models.`;
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
function checkHasKey(pid, config) {
|
|
543
|
-
if (pid === config.provider && config.provider) return true;
|
|
544
|
-
const pc = config.providers?.[pid];
|
|
545
|
-
if (!pc) return false;
|
|
546
|
-
if (typeof pc.apiKey === "string" && pc.apiKey.length > 0) return true;
|
|
547
|
-
if (Array.isArray(pc.apiKeys) && pc.apiKeys.some((k) => k?.apiKey)) return true;
|
|
548
|
-
return false;
|
|
549
|
-
}
|
|
550
|
-
var modeldiagCmd = async (args, deps) => {
|
|
551
|
-
const sub = args[0]?.toLowerCase() || "full";
|
|
552
|
-
const cacheResult = await readProviders(deps.paths.modelsCache);
|
|
553
|
-
if (typeof cacheResult === "string") {
|
|
554
|
-
deps.renderer.write(`${cacheResult}
|
|
555
|
-
`);
|
|
556
|
-
return cacheResult.includes(color2.red("")) ? 1 : 0;
|
|
557
|
-
}
|
|
558
|
-
const providers = cacheResult;
|
|
559
|
-
const config = deps.config;
|
|
560
|
-
const modelMatrix = config.modelMatrix ?? {};
|
|
561
|
-
function hasKey(pid) {
|
|
562
|
-
return checkHasKey(pid, config);
|
|
563
|
-
}
|
|
564
|
-
function writeLine(line = "") {
|
|
332
|
+
async function runModeldiagEval(args, deps, providers, config, hasKey) {
|
|
333
|
+
const writeLine = (line = "") => {
|
|
565
334
|
deps.renderer.write(`${line}
|
|
566
335
|
`);
|
|
336
|
+
};
|
|
337
|
+
const evalArgs = args.slice(1);
|
|
338
|
+
const providersEq = evalArgs.find((a) => a.startsWith("--providers="));
|
|
339
|
+
const providersValue = providersEq?.replace("--providers=", "") ?? (typeof deps.flags?.["providers"] === "string" ? deps.flags["providers"] : typeof deps.flags?.["provider"] === "string" ? deps.flags["provider"] : void 0);
|
|
340
|
+
const providerFilter = providersValue ? providersValue.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
341
|
+
const maxEq = evalArgs.find((a) => a.startsWith("--max="));
|
|
342
|
+
const maxValue = maxEq?.replace("--max=", "") ?? (typeof deps.flags?.["max"] === "string" ? deps.flags["max"] : void 0);
|
|
343
|
+
const maxModels = maxValue ? Math.max(1, parseInt(maxValue, 10) || 2) : 2;
|
|
344
|
+
const quick = evalArgs.includes("--quick") || deps.flags?.["quick"] === true || deps.flags?.["quick"] === "true";
|
|
345
|
+
const modelsPerCat = quick ? 1 : maxModels;
|
|
346
|
+
const roleFilter = evalArgs.find((a) => !a.startsWith("--"));
|
|
347
|
+
const targetCategories = roleFilter ? EVAL_CATEGORIES.includes(roleCat(roleFilter)) ? [roleCat(roleFilter)] : [] : EVAL_CATEGORIES;
|
|
348
|
+
if (targetCategories.length === 0 && roleFilter) {
|
|
349
|
+
writeLine(
|
|
350
|
+
`${color2.amber("Unknown role/category")}: "${roleFilter}". Try: ${EVAL_CATEGORIES.join(", ")}`
|
|
351
|
+
);
|
|
352
|
+
return 1;
|
|
567
353
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
354
|
+
const keyedProviderIds = providers.filter((p) => hasKey(p.id)).map((p) => p.id);
|
|
355
|
+
let targetProviderIds;
|
|
356
|
+
if (providerFilter && providerFilter.length > 0) {
|
|
357
|
+
const unknown = providerFilter.filter((pid) => !keyedProviderIds.includes(pid));
|
|
358
|
+
targetProviderIds = providerFilter.filter((pid) => keyedProviderIds.includes(pid));
|
|
359
|
+
if (targetProviderIds.length === 0) {
|
|
360
|
+
const noKeyMsg = unknown.length > 0 ? `None of the specified providers (${unknown.join(", ")}) have API keys. Add keys with wstack auth.` : "None of the specified providers have API keys configured.";
|
|
361
|
+
writeLine(`${color2.amber(noKeyMsg)}`);
|
|
362
|
+
return 0;
|
|
574
363
|
}
|
|
575
|
-
|
|
576
|
-
writeLine(`${color2.
|
|
364
|
+
} else if (keyedProviderIds.length === 0) {
|
|
365
|
+
writeLine(`${color2.amber("No providers have API keys. Add keys with wstack auth.")}`);
|
|
577
366
|
return 0;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
367
|
+
} else if (keyedProviderIds.length === 1) {
|
|
368
|
+
targetProviderIds = keyedProviderIds;
|
|
369
|
+
} else {
|
|
370
|
+
const providerList = keyedProviderIds.map((pid, i) => {
|
|
371
|
+
const info = providers.find((p) => p.id === pid);
|
|
372
|
+
return ` ${color2.cyan(String(i + 1))}) ${color2.bold(pid.padEnd(16))} ${color2.dim(info?.name ?? "")}`;
|
|
373
|
+
}).join("\n");
|
|
374
|
+
deps.renderer.write(
|
|
375
|
+
`
|
|
376
|
+
${color2.bold("Select providers to evaluate")}
|
|
377
|
+
|
|
378
|
+
${providerList}
|
|
379
|
+
|
|
380
|
+
${color2.dim('Enter numbers or provider IDs (comma-separated, or "all"):')}
|
|
381
|
+
`
|
|
382
|
+
);
|
|
383
|
+
const input = await deps.reader.readLine(" > ");
|
|
384
|
+
const selected = input.trim().toLowerCase();
|
|
385
|
+
if (selected === "" || selected === "all") {
|
|
386
|
+
targetProviderIds = keyedProviderIds;
|
|
387
|
+
} else {
|
|
388
|
+
targetProviderIds = [];
|
|
389
|
+
for (const part of selected.split(",").map((s) => s.trim())) {
|
|
390
|
+
const idx = parseInt(part, 10);
|
|
391
|
+
if (idx >= 1 && idx <= keyedProviderIds.length) {
|
|
392
|
+
const pid = keyedProviderIds[idx - 1];
|
|
393
|
+
if (!targetProviderIds.includes(pid)) targetProviderIds.push(pid);
|
|
394
|
+
} else if (keyedProviderIds.includes(part)) {
|
|
395
|
+
if (!targetProviderIds.includes(part)) targetProviderIds.push(part);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (targetProviderIds.length === 0) {
|
|
400
|
+
writeLine(color2.dim("No providers selected."));
|
|
401
|
+
return 0;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const leaderModel = config.model ?? "unknown";
|
|
405
|
+
const unknownProviders = providerFilter ? providerFilter.filter((pid) => !keyedProviderIds.includes(pid)) : [];
|
|
406
|
+
const warningLine = unknownProviders.length > 0 ? ` ${color2.amber("\u26A0 skipped (no key):")} ${unknownProviders.join(", ")}
|
|
407
|
+
` : "";
|
|
408
|
+
writeLine(`${color2.bold("Model Competency Evaluation")}`);
|
|
409
|
+
writeLine(
|
|
410
|
+
color2.dim(
|
|
411
|
+
`Providers: ${targetProviderIds.join(", ")} | ${targetCategories.length} cats | ${modelsPerCat} model(s)/cat/provider`
|
|
412
|
+
)
|
|
413
|
+
);
|
|
414
|
+
writeLine(warningLine);
|
|
415
|
+
writeLine(color2.dim(`Leader (ranker): ${config.provider}/${leaderModel}`));
|
|
416
|
+
writeLine();
|
|
417
|
+
const collected = /* @__PURE__ */ new Map();
|
|
418
|
+
let total = 0;
|
|
419
|
+
let ok = 0;
|
|
420
|
+
for (const pid of targetProviderIds) {
|
|
421
|
+
const prov = createProviderForId(pid, config);
|
|
422
|
+
if (!prov) {
|
|
423
|
+
writeLine(color2.dim(` \u2298 ${pid}: provider unavailable, skipping`));
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
for (const cat of targetCategories) {
|
|
427
|
+
const task = EVAL_TASKS[cat];
|
|
428
|
+
if (!task) continue;
|
|
429
|
+
const candidates = rankModels(
|
|
430
|
+
providers,
|
|
431
|
+
(providerId) => providerId === pid && hasKey(providerId),
|
|
432
|
+
cat,
|
|
433
|
+
modelsPerCat
|
|
434
|
+
);
|
|
435
|
+
if (candidates.length === 0) continue;
|
|
436
|
+
if (!collected.has(cat)) collected.set(cat, /* @__PURE__ */ new Map());
|
|
437
|
+
for (const c of candidates) {
|
|
438
|
+
total++;
|
|
439
|
+
const modelKey = `${pid}/${c.model}`;
|
|
440
|
+
try {
|
|
441
|
+
const start = Date.now();
|
|
442
|
+
const resp = await prov.complete(
|
|
443
|
+
{
|
|
444
|
+
model: c.model,
|
|
445
|
+
system: [{ type: "text", text: "Be thorough and correct." }],
|
|
446
|
+
messages: [
|
|
447
|
+
{ role: "user", content: [{ type: "text", text: task.prompt }] }
|
|
448
|
+
],
|
|
449
|
+
maxTokens: 1024
|
|
450
|
+
},
|
|
451
|
+
{ signal: AbortSignal.timeout(45e3) }
|
|
452
|
+
);
|
|
453
|
+
const respText = resp.content[0] && "text" in resp.content[0] ? resp.content[0].text : "";
|
|
454
|
+
const respUsage = resp.usage;
|
|
455
|
+
collected.get(cat)?.set(modelKey, {
|
|
456
|
+
model: modelKey,
|
|
457
|
+
latency: Date.now() - start,
|
|
458
|
+
tokens: (respUsage?.input ?? 0) + (respUsage?.output ?? 0),
|
|
459
|
+
text: respText
|
|
460
|
+
});
|
|
461
|
+
ok++;
|
|
462
|
+
} catch {
|
|
463
|
+
collected.get(cat)?.set(modelKey, {
|
|
464
|
+
model: modelKey,
|
|
465
|
+
latency: -1,
|
|
466
|
+
tokens: 0,
|
|
467
|
+
text: ""
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
writeLine(`${color2.dim(`Phase 1: ${ok}/${total} calls succeeded`)}`);
|
|
474
|
+
writeLine();
|
|
475
|
+
if (collected.size === 0) {
|
|
476
|
+
writeLine(color2.amber("No responses collected. Check provider configuration."));
|
|
477
|
+
return 0;
|
|
478
|
+
}
|
|
479
|
+
const leaderProvider = createProviderForId(config.provider, config);
|
|
480
|
+
if (leaderProvider) {
|
|
481
|
+
writeLine(`${color2.bold("Phase 2")} \u2014 ${color2.dim("leader ranks responses")}`);
|
|
482
|
+
writeLine();
|
|
483
|
+
}
|
|
484
|
+
const rankings = /* @__PURE__ */ new Map();
|
|
485
|
+
for (const [cat, responses] of collected) {
|
|
486
|
+
const valid = Array.from(responses.values()).filter((r) => r.latency >= 0);
|
|
487
|
+
if (valid.length < 2) {
|
|
488
|
+
if (valid.length === 1) {
|
|
489
|
+
const m = valid[0]?.model;
|
|
490
|
+
if (m !== void 0) {
|
|
491
|
+
if (!rankings.has(m)) rankings.set(m, /* @__PURE__ */ new Map());
|
|
492
|
+
rankings.get(m)?.set(cat, { rank: 1, total: 1 });
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
const task = EVAL_TASKS[cat];
|
|
498
|
+
if (leaderProvider) {
|
|
499
|
+
const ranked = await rankResponses(leaderProvider, leaderModel, task.prompt, valid);
|
|
500
|
+
for (let i = 0; i < valid.length; i++) {
|
|
501
|
+
const m = valid[ranked[i] ?? i]?.model;
|
|
502
|
+
if (m === void 0) continue;
|
|
503
|
+
if (!rankings.has(m)) rankings.set(m, /* @__PURE__ */ new Map());
|
|
504
|
+
rankings.get(m)?.set(cat, { rank: i + 1, total: valid.length });
|
|
505
|
+
}
|
|
506
|
+
} else {
|
|
507
|
+
for (const r of valid) {
|
|
508
|
+
if (!rankings.has(r.model)) rankings.set(r.model, /* @__PURE__ */ new Map());
|
|
509
|
+
rankings.get(r.model)?.set(cat, { rank: 1, total: valid.length });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
writeLine(`${color2.bold("Competency Report")}`);
|
|
514
|
+
writeLine();
|
|
515
|
+
const allModels = [.../* @__PURE__ */ new Set([...rankings.keys()])].sort();
|
|
516
|
+
const catList = [...collected.keys()];
|
|
517
|
+
const modelColWidth = Math.max(24, ...allModels.map((m) => m.length)) + 2;
|
|
518
|
+
const cw = 12;
|
|
519
|
+
writeLine(
|
|
520
|
+
` ${color2.dim("model".padEnd(modelColWidth))}` + catList.map((c) => color2.dim((EVAL_TASKS[c]?.label ?? c).slice(0, cw).padEnd(cw + 2))).join("")
|
|
521
|
+
);
|
|
522
|
+
writeLine(` ${color2.dim("\u2500".repeat(modelColWidth + catList.length * (cw + 2)))}`);
|
|
523
|
+
for (const model of allModels) {
|
|
524
|
+
const mr = rankings.get(model);
|
|
525
|
+
const provFromModel = model.split("/")[0] ?? "";
|
|
526
|
+
const modelColor = provFromModel === config.provider ? color2.cyan : color2.green;
|
|
527
|
+
let row = ` ${modelColor(model.padEnd(modelColWidth))}`;
|
|
528
|
+
for (const cat of catList) {
|
|
529
|
+
const e = mr.get(cat);
|
|
530
|
+
if (e) {
|
|
531
|
+
const pct = Math.round((1 - (e.rank - 1) / Math.max(1, e.total - 1)) * 100);
|
|
532
|
+
const pc = pct >= 80 ? color2.green : pct >= 50 ? color2.amber : color2.red;
|
|
533
|
+
row += `${pc(`#${e.rank} ${pct}%`.padEnd(cw + 2))}`;
|
|
534
|
+
} else {
|
|
535
|
+
row += color2.dim("\u2014".padEnd(cw + 2));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
writeLine(row);
|
|
539
|
+
}
|
|
540
|
+
writeLine();
|
|
541
|
+
writeLine(color2.dim("#1 100% = best in category. \u2014 = not tested."));
|
|
542
|
+
writeLine();
|
|
543
|
+
writeLine(color2.dim("Pin: wstack setmodel set <role> <provider>/<model>"));
|
|
544
|
+
writeLine(
|
|
545
|
+
color2.dim(
|
|
546
|
+
"Full: wstack modeldiag eval Providers: wstack modeldiag eval --providers=id1,id2"
|
|
547
|
+
)
|
|
548
|
+
);
|
|
549
|
+
writeLine(
|
|
550
|
+
color2.dim("Max: wstack modeldiag eval --max=3 Quick: wstack modeldiag eval --quick")
|
|
551
|
+
);
|
|
552
|
+
return 0;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/subcommands/handlers/modeldiag-bench.ts
|
|
556
|
+
async function runModeldiagBench(args, deps, providers, config, hasKey) {
|
|
557
|
+
const writeLine = (line = "") => {
|
|
558
|
+
deps.renderer.write(`${line}
|
|
559
|
+
`);
|
|
560
|
+
};
|
|
561
|
+
const benchArgs = args.slice(1);
|
|
562
|
+
if (benchArgs.length < 2) {
|
|
563
|
+
writeLine(
|
|
564
|
+
`${color3.amber("Usage:")} wstack modeldiag bench <role> "<test prompt>" [--providers=p1,p2]`
|
|
565
|
+
);
|
|
566
|
+
writeLine();
|
|
567
|
+
writeLine(
|
|
568
|
+
color3.dim(
|
|
569
|
+
'Example: wstack modeldiag bench verify "Write a function that checks if a string is a palindrome"'
|
|
570
|
+
)
|
|
571
|
+
);
|
|
572
|
+
writeLine(
|
|
573
|
+
color3.dim(
|
|
574
|
+
"Tests the top 5 candidate models for the role with your prompt and reports results."
|
|
575
|
+
)
|
|
576
|
+
);
|
|
577
|
+
writeLine(color3.dim("Add --providers=anthropic,google to test across multiple providers."));
|
|
578
|
+
return 0;
|
|
579
|
+
}
|
|
580
|
+
const providersEqIdx = benchArgs.findIndex((a) => a.startsWith("--providers="));
|
|
581
|
+
let providerFilter;
|
|
582
|
+
if (providersEqIdx >= 0) {
|
|
583
|
+
const rawFilter = benchArgs[providersEqIdx]?.replace("--providers=", "").split(",");
|
|
584
|
+
if (rawFilter) {
|
|
585
|
+
providerFilter = rawFilter.map((s) => s.trim()).filter(Boolean);
|
|
586
|
+
benchArgs.splice(providersEqIdx, 1);
|
|
587
|
+
}
|
|
588
|
+
} else {
|
|
589
|
+
const rawFilter = deps.flags?.["providers"] ?? deps.flags?.["provider"];
|
|
590
|
+
if (typeof rawFilter === "string") {
|
|
591
|
+
providerFilter = rawFilter.split(",").map((s) => s.trim()).filter(Boolean);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
const benchRole = benchArgs[0];
|
|
595
|
+
if (!benchRole) {
|
|
596
|
+
writeLine(
|
|
597
|
+
`${color3.amber("No benchmark role specified")}. Usage: wstack diag bench <role> [prompt]`
|
|
598
|
+
);
|
|
599
|
+
return 1;
|
|
600
|
+
}
|
|
601
|
+
const benchPrompt = benchArgs.slice(1).join(" ");
|
|
602
|
+
const cat = roleCat(benchRole);
|
|
603
|
+
const candidates = rankModels(providers, hasKey, cat, 5);
|
|
604
|
+
if (candidates.length === 0) {
|
|
605
|
+
writeLine(
|
|
606
|
+
`${color3.amber("No candidate models found")} for role "${benchRole}" (category: ${cat}).`
|
|
607
|
+
);
|
|
608
|
+
return 0;
|
|
609
|
+
}
|
|
610
|
+
let targetCandidates;
|
|
611
|
+
if (providerFilter && providerFilter.length > 0) {
|
|
612
|
+
targetCandidates = candidates.filter((c) => providerFilter.includes(c.provider));
|
|
613
|
+
if (targetCandidates.length === 0) {
|
|
614
|
+
writeLine(
|
|
615
|
+
`${color3.amber("No candidates match the specified providers")}: ${providerFilter.join(", ")}`
|
|
616
|
+
);
|
|
617
|
+
writeLine(
|
|
618
|
+
`Candidate providers: ${[...new Set(candidates.map((c) => c.provider))].join(", ")}`
|
|
619
|
+
);
|
|
620
|
+
return 0;
|
|
621
|
+
}
|
|
622
|
+
} else {
|
|
623
|
+
targetCandidates = candidates;
|
|
624
|
+
}
|
|
625
|
+
writeLine(
|
|
626
|
+
`${color3.bold("Model Benchmark")} \u2014 ${color3.amber(benchRole)} ${color3.dim(`(category: ${cat})`)}`
|
|
627
|
+
);
|
|
628
|
+
writeLine(
|
|
629
|
+
`${color3.dim("Prompt:")} "${benchPrompt.slice(0, 120)}${benchPrompt.length > 120 ? "\u2026" : ""}"`
|
|
630
|
+
);
|
|
631
|
+
writeLine();
|
|
632
|
+
writeLine(
|
|
633
|
+
` ${color3.dim("# model".padEnd(52))} ${color3.dim("score".padEnd(12))} ${color3.dim("latency".padEnd(10))} ${color3.dim("tokens".padEnd(14))} ${color3.dim("first line")}`
|
|
634
|
+
);
|
|
635
|
+
writeLine(` ${color3.dim("\u2500".repeat(108))}`);
|
|
636
|
+
const providerInstances = /* @__PURE__ */ new Map();
|
|
637
|
+
for (const pid of [...new Set(targetCandidates.map((c) => c.provider))]) {
|
|
638
|
+
const prov = createProviderForId(pid, config);
|
|
639
|
+
if (prov) providerInstances.set(pid, prov);
|
|
640
|
+
}
|
|
641
|
+
let idx = 0;
|
|
642
|
+
for (const c of targetCandidates.slice(0, 20)) {
|
|
643
|
+
idx++;
|
|
644
|
+
const label = `${idx}`.padStart(2);
|
|
645
|
+
const modelKey = `${c.provider}/${c.model}`;
|
|
646
|
+
const prov = providerInstances.get(c.provider);
|
|
647
|
+
if (!prov) {
|
|
648
|
+
writeLine(
|
|
649
|
+
` ${label} ${color3.red(modelKey.padEnd(50))} ${scoreBar(c.score, 110).slice(0, 11)} ${color3.red("NO PROVIDER")}`
|
|
650
|
+
);
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
try {
|
|
654
|
+
const start = Date.now();
|
|
655
|
+
const resp = await prov.complete(
|
|
656
|
+
{
|
|
657
|
+
model: c.model,
|
|
658
|
+
messages: [{ role: "user", content: [{ type: "text", text: benchPrompt }] }],
|
|
659
|
+
maxTokens: 256
|
|
660
|
+
},
|
|
661
|
+
{ signal: AbortSignal.timeout(3e4) }
|
|
662
|
+
);
|
|
663
|
+
const latency = Date.now() - start;
|
|
664
|
+
const firstText = resp.content[0] && "text" in resp.content[0] ? resp.content[0].text : "";
|
|
665
|
+
const firstLineClean = firstText.replace(/\n/g, " ").slice(0, 80) || color3.dim("(empty)");
|
|
666
|
+
const provColor = c.provider === config.provider ? color3.green : color3.cyan;
|
|
667
|
+
const usage = resp.usage;
|
|
668
|
+
writeLine(
|
|
669
|
+
` ${label} ${provColor(modelKey.padEnd(50))} ${scoreBar(c.score, 110).slice(0, 11)} ${color3.amber(fmtMs(latency).padEnd(8))} ${color3.dim(`in${usage?.input ?? "?"}/out${usage?.output ?? "?"}`.padEnd(12))} ${firstLineClean}`
|
|
670
|
+
);
|
|
671
|
+
} catch (err) {
|
|
672
|
+
const errMsg = toErrorMessage(err);
|
|
673
|
+
writeLine(
|
|
674
|
+
` ${label} ${color3.red(modelKey.padEnd(50))} ${scoreBar(c.score, 110).slice(0, 11)} ${color3.red("FAILED")} ${color3.dim(errMsg.slice(0, 40))}`
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
const testedProviders = [...new Set(targetCandidates.map((c) => c.provider))];
|
|
679
|
+
writeLine();
|
|
680
|
+
writeLine(
|
|
681
|
+
color3.dim(
|
|
682
|
+
`Tested ${idx} model(s) across ${testedProviders.length} provider(s): ${testedProviders.join(", ")}.`
|
|
683
|
+
)
|
|
684
|
+
);
|
|
685
|
+
writeLine(color3.dim("Pin the best: wstack setmodel set <role> <provider>/<model>"));
|
|
686
|
+
return 0;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// src/subcommands/handlers/modeldiag-test.ts
|
|
690
|
+
import { color as color4, toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils";
|
|
691
|
+
import { makeProviderFromConfig as makeProviderFromConfig2, setOAuthTokenPersister } from "@wrongstack/providers";
|
|
692
|
+
|
|
693
|
+
// src/subcommands/handlers/model-smoke-test.ts
|
|
694
|
+
import { ProviderError } from "@wrongstack/core/types";
|
|
695
|
+
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
|
|
696
|
+
var DEFAULT_TIMEOUT_MS = 45e3;
|
|
697
|
+
var DEFAULT_MAX_TOKENS = 32;
|
|
698
|
+
function csv(value) {
|
|
699
|
+
if (typeof value !== "string") return void 0;
|
|
700
|
+
const values = value.split(",").map((part) => part.trim()).filter(Boolean);
|
|
701
|
+
return values.length > 0 ? values : void 0;
|
|
702
|
+
}
|
|
703
|
+
function positiveInteger(value, fallback, label) {
|
|
704
|
+
if (value === void 0) return fallback;
|
|
705
|
+
const parsed = typeof value === "string" ? Number(value) : Number.NaN;
|
|
706
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
707
|
+
throw new Error(`${label} must be a positive integer.`);
|
|
708
|
+
}
|
|
709
|
+
return parsed;
|
|
710
|
+
}
|
|
711
|
+
function parseModelSmokeOptions(args, flags = {}) {
|
|
712
|
+
const local = /* @__PURE__ */ new Map();
|
|
713
|
+
for (let i = 0; i < args.length; i++) {
|
|
714
|
+
const arg = args[i];
|
|
715
|
+
if (!arg?.startsWith("--")) continue;
|
|
716
|
+
const eq = arg.indexOf("=");
|
|
717
|
+
if (eq >= 0) {
|
|
718
|
+
local.set(arg.slice(2, eq), arg.slice(eq + 1));
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
const name = arg.slice(2);
|
|
722
|
+
const next = args[i + 1];
|
|
723
|
+
if (next && !next.startsWith("--")) {
|
|
724
|
+
local.set(name, next);
|
|
725
|
+
i++;
|
|
726
|
+
} else {
|
|
727
|
+
local.set(name, true);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
const get = (name) => local.get(name) ?? flags[name];
|
|
731
|
+
const providerFilter = csv(get("providers") ?? get("provider"));
|
|
732
|
+
const modelFilter = csv(get("models") ?? get("model"));
|
|
733
|
+
return {
|
|
734
|
+
allModels: get("all-models") === true || get("all-models") === "true",
|
|
735
|
+
planOnly: get("plan") === true || get("plan") === "true",
|
|
736
|
+
json: get("json") === true || get("json") === "true",
|
|
737
|
+
timeoutMs: positiveInteger(get("timeout"), DEFAULT_TIMEOUT_MS, "--timeout"),
|
|
738
|
+
maxTokens: positiveInteger(get("max-tokens"), DEFAULT_MAX_TOKENS, "--max-tokens"),
|
|
739
|
+
...providerFilter ? { providerFilter } : {},
|
|
740
|
+
...modelFilter ? { modelFilter } : {}
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
function pushUnique(target, values) {
|
|
744
|
+
const seen = new Set(target);
|
|
745
|
+
for (const value of values) {
|
|
746
|
+
const normalized = value?.trim();
|
|
747
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
748
|
+
seen.add(normalized);
|
|
749
|
+
target.push(normalized);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
async function catalogModelsFor(providerId, providerConfig, registry) {
|
|
753
|
+
const direct = await registry.getProvider(providerId).catch(() => void 0);
|
|
754
|
+
const inherited = direct ?? (providerConfig?.type && providerConfig.type !== providerId ? await registry.getProvider(providerConfig.type).catch(() => void 0) : void 0);
|
|
755
|
+
return inherited?.models ?? [];
|
|
756
|
+
}
|
|
757
|
+
function supportsTextCompletion(model) {
|
|
758
|
+
const outputs = model?.modalities?.output;
|
|
759
|
+
return !outputs?.length || outputs.includes("text");
|
|
760
|
+
}
|
|
761
|
+
async function buildModelSmokeTargets(config, registry, options) {
|
|
762
|
+
const providerIds = Object.keys(config.providers ?? {});
|
|
763
|
+
if (config.provider && !providerIds.includes(config.provider)) providerIds.push(config.provider);
|
|
764
|
+
const providerFilter = options.providerFilter ? new Set(options.providerFilter.map((id) => id.toLowerCase())) : void 0;
|
|
765
|
+
const modelFilter = options.modelFilter ? new Set(options.modelFilter) : void 0;
|
|
766
|
+
const targets = [];
|
|
767
|
+
for (const providerId of providerIds) {
|
|
768
|
+
if (providerFilter && !providerFilter.has(providerId.toLowerCase())) continue;
|
|
769
|
+
const saved = config.providers?.[providerId];
|
|
770
|
+
const catalogModels = await catalogModelsFor(providerId, saved, registry);
|
|
771
|
+
const catalogById = new Map(catalogModels.map((model) => [model.id, model]));
|
|
772
|
+
const textCatalogModels = catalogModels.filter(supportsTextCompletion);
|
|
773
|
+
const candidates = [];
|
|
774
|
+
if (options.allModels) {
|
|
775
|
+
pushUnique(candidates, saved?.models ?? []);
|
|
776
|
+
pushUnique(candidates, Object.keys(saved?.customModels ?? {}));
|
|
777
|
+
pushUnique(
|
|
778
|
+
candidates,
|
|
779
|
+
textCatalogModels.map((model) => model.id)
|
|
780
|
+
);
|
|
781
|
+
if (providerId === config.provider) pushUnique(candidates, [config.model]);
|
|
782
|
+
pushUnique(candidates, [saved?.model]);
|
|
783
|
+
} else {
|
|
784
|
+
const representative = (providerId === config.provider ? config.model : void 0) ?? saved?.model ?? saved?.models?.find(Boolean) ?? Object.keys(saved?.customModels ?? {})[0] ?? await registry.suggestModel(providerId).catch(() => void 0) ?? textCatalogModels[0]?.id;
|
|
785
|
+
pushUnique(candidates, [representative]);
|
|
786
|
+
}
|
|
787
|
+
for (const modelId of candidates) {
|
|
788
|
+
if (modelFilter && !modelFilter.has(modelId)) continue;
|
|
789
|
+
if (!supportsTextCompletion(catalogById.get(modelId))) continue;
|
|
790
|
+
targets.push({ providerId, modelId });
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return targets;
|
|
794
|
+
}
|
|
795
|
+
function classifyFailure(err) {
|
|
796
|
+
if (err instanceof ProviderError || ProviderError.isProviderError(err)) {
|
|
797
|
+
const providerError = err;
|
|
798
|
+
return {
|
|
799
|
+
error: providerError.describe(),
|
|
800
|
+
errorKind: providerError.kind,
|
|
801
|
+
httpStatus: providerError.status
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
const isTimeout = err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError") || /timed?\s*out/i.test(toErrorMessage2(err));
|
|
805
|
+
return {
|
|
806
|
+
error: toErrorMessage2(err),
|
|
807
|
+
errorKind: isTimeout ? "timeout" : "unknown"
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
async function runModelSmokeTests(params) {
|
|
811
|
+
const results = [];
|
|
812
|
+
let providerId;
|
|
813
|
+
let provider;
|
|
814
|
+
let providerError;
|
|
815
|
+
for (let index = 0; index < params.targets.length; index++) {
|
|
816
|
+
const target = params.targets[index];
|
|
817
|
+
params.onTargetStart?.(target, index, params.targets.length);
|
|
818
|
+
const started = Date.now();
|
|
819
|
+
if (target.providerId !== providerId) {
|
|
820
|
+
providerId = target.providerId;
|
|
821
|
+
provider = void 0;
|
|
822
|
+
providerError = void 0;
|
|
823
|
+
try {
|
|
824
|
+
provider = await params.createProvider(target.providerId);
|
|
825
|
+
} catch (err) {
|
|
826
|
+
providerError = err;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (!provider) {
|
|
830
|
+
const failure = classifyFailure(providerError ?? new Error("Provider could not be created."));
|
|
831
|
+
const result = {
|
|
832
|
+
providerId: target.providerId,
|
|
833
|
+
modelId: target.modelId,
|
|
834
|
+
status: "failed",
|
|
835
|
+
latencyMs: Date.now() - started,
|
|
836
|
+
...failure
|
|
837
|
+
};
|
|
838
|
+
results.push(result);
|
|
839
|
+
params.onTargetComplete?.(result, index, params.targets.length);
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
try {
|
|
843
|
+
const response = await provider.complete(
|
|
844
|
+
{
|
|
845
|
+
model: target.modelId,
|
|
846
|
+
messages: [
|
|
847
|
+
{
|
|
848
|
+
role: "user",
|
|
849
|
+
content: [{ type: "text", text: "Reply with exactly: OK" }]
|
|
850
|
+
}
|
|
851
|
+
],
|
|
852
|
+
maxTokens: params.options.maxTokens
|
|
853
|
+
},
|
|
854
|
+
{ signal: AbortSignal.timeout(params.options.timeoutMs) }
|
|
855
|
+
);
|
|
856
|
+
const result = {
|
|
857
|
+
providerId: target.providerId,
|
|
858
|
+
modelId: target.modelId,
|
|
859
|
+
status: "passed",
|
|
860
|
+
latencyMs: Date.now() - started,
|
|
861
|
+
stopReason: response.stopReason,
|
|
862
|
+
usage: response.usage
|
|
863
|
+
};
|
|
864
|
+
results.push(result);
|
|
865
|
+
params.onTargetComplete?.(result, index, params.targets.length);
|
|
866
|
+
} catch (err) {
|
|
867
|
+
const result = {
|
|
868
|
+
providerId: target.providerId,
|
|
869
|
+
modelId: target.modelId,
|
|
870
|
+
status: "failed",
|
|
871
|
+
latencyMs: Date.now() - started,
|
|
872
|
+
...classifyFailure(err)
|
|
873
|
+
};
|
|
874
|
+
results.push(result);
|
|
875
|
+
params.onTargetComplete?.(result, index, params.targets.length);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return results;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// src/subcommands/handlers/modeldiag-test.ts
|
|
882
|
+
async function runModeldiagTest(args, deps, config) {
|
|
883
|
+
const writeLine = (line = "") => {
|
|
884
|
+
deps.renderer.write(`${line}
|
|
885
|
+
`);
|
|
886
|
+
};
|
|
887
|
+
if (args.slice(1).includes("--help") || deps.flags?.["help"] === true) {
|
|
888
|
+
writeLine(`${color4.bold("Usage:")} wstack modeldiag test [options]`);
|
|
889
|
+
writeLine();
|
|
890
|
+
writeLine(" (no flags) Probe one representative model per configured provider");
|
|
891
|
+
writeLine(" --all-models Probe every catalog-visible model sequentially");
|
|
892
|
+
writeLine(" --plan Show targets without making model calls");
|
|
893
|
+
writeLine(" --provider=<id,...> Restrict provider ids");
|
|
894
|
+
writeLine(" --model=<id,...> Restrict model ids");
|
|
895
|
+
writeLine(" --timeout=<ms> Per-model timeout (default 45000)");
|
|
896
|
+
writeLine(" --max-tokens=<n> Output cap for the tiny probe (default 32)");
|
|
897
|
+
writeLine(" --json Emit machine-readable output");
|
|
898
|
+
writeLine(" --yes Confirm a run larger than 50 requests");
|
|
899
|
+
return 0;
|
|
900
|
+
}
|
|
901
|
+
let smokeOptions;
|
|
902
|
+
try {
|
|
903
|
+
smokeOptions = parseModelSmokeOptions(args.slice(1), deps.flags);
|
|
904
|
+
} catch (err) {
|
|
905
|
+
writeLine(`${color4.red("Invalid smoke-test options:")} ${toErrorMessage3(err)}`);
|
|
906
|
+
return 2;
|
|
907
|
+
}
|
|
908
|
+
if ("mergeOverlay" in deps.modelsRegistry) {
|
|
909
|
+
await discoverAndMergeProviders({
|
|
910
|
+
config,
|
|
911
|
+
registry: deps.modelsRegistry,
|
|
912
|
+
cacheDir: deps.paths.cacheDir
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
const targets = await buildModelSmokeTargets(
|
|
916
|
+
config,
|
|
917
|
+
deps.modelsRegistry,
|
|
918
|
+
smokeOptions
|
|
919
|
+
);
|
|
920
|
+
const byProvider = /* @__PURE__ */ new Map();
|
|
921
|
+
for (const target of targets) {
|
|
922
|
+
byProvider.set(target.providerId, (byProvider.get(target.providerId) ?? 0) + 1);
|
|
923
|
+
}
|
|
924
|
+
if (smokeOptions.planOnly) {
|
|
925
|
+
const plan = {
|
|
926
|
+
mode: smokeOptions.allModels ? "all-models" : "representative",
|
|
927
|
+
targetCount: targets.length,
|
|
928
|
+
providers: Object.fromEntries(byProvider),
|
|
929
|
+
targets
|
|
930
|
+
};
|
|
931
|
+
if (smokeOptions.json) {
|
|
932
|
+
writeLine(JSON.stringify(plan, null, 2));
|
|
933
|
+
} else {
|
|
934
|
+
writeLine(`${color4.bold("Provider/Model Smoke Test Plan")}`);
|
|
935
|
+
writeLine();
|
|
936
|
+
for (const [providerId, count] of byProvider) {
|
|
937
|
+
writeLine(` ${color4.cyan(providerId.padEnd(24))} ${count} model(s)`);
|
|
938
|
+
}
|
|
939
|
+
writeLine();
|
|
940
|
+
writeLine(
|
|
941
|
+
color4.dim(
|
|
942
|
+
`${targets.length} request(s) would run sequentially. Remove --plan to execute.`
|
|
943
|
+
)
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
return targets.length > 0 ? 0 : 2;
|
|
947
|
+
}
|
|
948
|
+
if (targets.length === 0) {
|
|
949
|
+
const message = "No provider/model targets matched. Check the active profile or remove filters.";
|
|
950
|
+
if (smokeOptions.json) {
|
|
951
|
+
writeLine(
|
|
952
|
+
JSON.stringify({ results: [], summary: { passed: 0, failed: 0 }, error: message })
|
|
953
|
+
);
|
|
954
|
+
} else {
|
|
955
|
+
writeLine(color4.amber(message));
|
|
956
|
+
}
|
|
957
|
+
return 2;
|
|
958
|
+
}
|
|
959
|
+
const confirmed = deps.flags?.["yes"] === true || deps.flags?.["yes"] === "true" || args.slice(1).includes("--yes");
|
|
960
|
+
if (targets.length > 50 && !confirmed) {
|
|
961
|
+
const message = `${targets.length} live requests are planned. Review with --plan, then add --yes to run them.`;
|
|
962
|
+
if (smokeOptions.json) {
|
|
963
|
+
writeLine(JSON.stringify({ error: message, targetCount: targets.length }));
|
|
964
|
+
} else {
|
|
965
|
+
writeLine(color4.amber(message));
|
|
966
|
+
}
|
|
967
|
+
return 2;
|
|
968
|
+
}
|
|
969
|
+
if (!smokeOptions.json) {
|
|
970
|
+
writeLine(`${color4.bold("Live Provider/Model Smoke Test")}`);
|
|
971
|
+
writeLine(
|
|
972
|
+
color4.dim(
|
|
973
|
+
`${targets.length} request(s), sequential, ${smokeOptions.timeoutMs}ms timeout, max ${smokeOptions.maxTokens} output tokens.`
|
|
974
|
+
)
|
|
975
|
+
);
|
|
976
|
+
writeLine();
|
|
977
|
+
}
|
|
978
|
+
const resultLines = (result, index, total) => {
|
|
979
|
+
if (smokeOptions.json) return;
|
|
980
|
+
const label = `[${String(index + 1).padStart(String(total).length)}/${total}]`;
|
|
981
|
+
const target = `${result.providerId}/${result.modelId}`;
|
|
982
|
+
if (result.status === "passed") {
|
|
983
|
+
writeLine(
|
|
984
|
+
` ${color4.green("\u2713")} ${color4.dim(label)} ${color4.cyan(target)} ${color4.dim(
|
|
985
|
+
`${fmtMs(result.latencyMs)} \xB7 in${result.usage.input}/out${result.usage.output} \xB7 ${result.stopReason}`
|
|
986
|
+
)}`
|
|
987
|
+
);
|
|
988
|
+
} else {
|
|
989
|
+
const status = result.httpStatus ? `HTTP ${result.httpStatus} \xB7 ` : "";
|
|
990
|
+
writeLine(
|
|
991
|
+
` ${color4.red("\u2717")} ${color4.dim(label)} ${color4.red(target)} ${color4.dim(
|
|
992
|
+
`${fmtMs(result.latencyMs)} \xB7 ${status}${result.errorKind}`
|
|
993
|
+
)}`
|
|
994
|
+
);
|
|
995
|
+
writeLine(` ${color4.red(result.error.slice(0, 240))}`);
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
let oauthWriteChain = Promise.resolve();
|
|
999
|
+
let oauthWriteScheduled = false;
|
|
1000
|
+
const profilePath = activeProfileConfigPath(deps.paths, config);
|
|
1001
|
+
setOAuthTokenPersister((providerId, credentials) => {
|
|
1002
|
+
oauthWriteScheduled = true;
|
|
1003
|
+
oauthWriteChain = oauthWriteChain.then(
|
|
1004
|
+
() => mutateConfigProviders(profilePath, deps.vault, (all) => {
|
|
1005
|
+
const providerConfig = all[providerId];
|
|
1006
|
+
if (!providerConfig) return;
|
|
1007
|
+
const keys = normalizeKeys(providerConfig);
|
|
1008
|
+
const active = providerConfig.activeKey ? keys.find((key) => key.label === providerConfig.activeKey) : keys[0];
|
|
1009
|
+
if (!active) return;
|
|
1010
|
+
active.apiKey = credentials.accessToken;
|
|
1011
|
+
active.refreshToken = credentials.refreshToken;
|
|
1012
|
+
active.expiresAt = new Date(credentials.expiresAt).toISOString();
|
|
1013
|
+
if (credentials.accountId) active.accountId = credentials.accountId;
|
|
1014
|
+
writeKeysBack(providerConfig, keys);
|
|
1015
|
+
})
|
|
1016
|
+
);
|
|
1017
|
+
});
|
|
1018
|
+
let results;
|
|
1019
|
+
try {
|
|
1020
|
+
results = await runModelSmokeTests({
|
|
1021
|
+
targets,
|
|
1022
|
+
options: smokeOptions,
|
|
1023
|
+
createProvider: async (providerId) => {
|
|
1024
|
+
const saved = config.providers?.[providerId];
|
|
1025
|
+
const resolved = await deps.modelsRegistry.getProvider(providerId).catch(() => void 0) ?? (saved?.type && saved.type !== providerId ? await deps.modelsRegistry.getProvider(saved.type).catch(() => void 0) : void 0);
|
|
1026
|
+
const providerConfig = {
|
|
1027
|
+
...providerId === config.provider ? {
|
|
1028
|
+
...config.apiKey ? { apiKey: config.apiKey } : {},
|
|
1029
|
+
...config.baseUrl ? { baseUrl: config.baseUrl } : {}
|
|
1030
|
+
} : {},
|
|
1031
|
+
...saved,
|
|
1032
|
+
type: providerId,
|
|
1033
|
+
...saved?.family ?? resolved?.family ? { family: saved?.family ?? resolved?.family } : {},
|
|
1034
|
+
...saved?.baseUrl ?? resolved?.apiBase ? { baseUrl: saved?.baseUrl ?? resolved?.apiBase } : {},
|
|
1035
|
+
...saved?.envVars ?? resolved?.envVars ? { envVars: saved?.envVars ?? resolved?.envVars } : {}
|
|
1036
|
+
};
|
|
1037
|
+
return makeProviderFromConfig2(providerId, providerConfig);
|
|
1038
|
+
},
|
|
1039
|
+
onTargetComplete: resultLines
|
|
1040
|
+
});
|
|
1041
|
+
} finally {
|
|
1042
|
+
setOAuthTokenPersister(void 0);
|
|
1043
|
+
}
|
|
1044
|
+
let oauthPersistenceError;
|
|
1045
|
+
if (oauthWriteScheduled) {
|
|
1046
|
+
try {
|
|
1047
|
+
await oauthWriteChain;
|
|
1048
|
+
} catch (err) {
|
|
1049
|
+
oauthPersistenceError = toErrorMessage3(err);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const passed = results.filter((result) => result.status === "passed").length;
|
|
1053
|
+
const failed = results.length - passed;
|
|
1054
|
+
const summary = { passed, failed, total: results.length };
|
|
1055
|
+
if (smokeOptions.json) {
|
|
1056
|
+
writeLine(
|
|
1057
|
+
JSON.stringify(
|
|
1058
|
+
{
|
|
1059
|
+
results,
|
|
1060
|
+
summary,
|
|
1061
|
+
...oauthPersistenceError ? {
|
|
1062
|
+
warning: `OAuth token refresh worked in-memory but could not be saved: ${oauthPersistenceError}`
|
|
1063
|
+
} : {}
|
|
1064
|
+
},
|
|
1065
|
+
null,
|
|
1066
|
+
2
|
|
1067
|
+
)
|
|
1068
|
+
);
|
|
1069
|
+
} else {
|
|
1070
|
+
writeLine();
|
|
1071
|
+
writeLine(
|
|
1072
|
+
`${color4.bold("Summary:")} ${color4.green(`${passed} passed`)} \xB7 ${failed > 0 ? color4.red(`${failed} failed`) : color4.dim("0 failed")}`
|
|
1073
|
+
);
|
|
1074
|
+
if (!smokeOptions.allModels) {
|
|
1075
|
+
writeLine(color4.dim("Use --all-models to test every catalog-visible model sequentially."));
|
|
1076
|
+
}
|
|
1077
|
+
if (oauthPersistenceError) {
|
|
1078
|
+
writeLine(
|
|
1079
|
+
color4.amber(
|
|
1080
|
+
`OAuth token refresh worked in-memory but could not be saved: ${oauthPersistenceError}`
|
|
1081
|
+
)
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return failed === 0 ? 0 : 1;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// src/subcommands/handlers/modeldiag.ts
|
|
1089
|
+
async function readProviders(cachePath) {
|
|
1090
|
+
if (!cachePath) {
|
|
1091
|
+
return `${color5.red("Models cache not available")}.`;
|
|
1092
|
+
}
|
|
1093
|
+
try {
|
|
1094
|
+
const raw = await fs.readFile(cachePath, "utf8");
|
|
1095
|
+
const parsed = JSON.parse(raw);
|
|
1096
|
+
const payload = parsed.payload ?? parsed;
|
|
1097
|
+
return Object.entries(payload).map(([id, p]) => ({
|
|
1098
|
+
id: p.id ?? id,
|
|
1099
|
+
name: p.name ?? id,
|
|
1100
|
+
family: p.npm ?? id,
|
|
1101
|
+
models: Object.values(p.models ?? {}).map(
|
|
1102
|
+
(m) => ({
|
|
1103
|
+
id: m.id,
|
|
1104
|
+
name: m.name,
|
|
1105
|
+
capabilities: {
|
|
1106
|
+
contextWindow: m.limit?.context,
|
|
1107
|
+
maxOutputTokens: m.limit?.output
|
|
1108
|
+
},
|
|
1109
|
+
pricing: m.cost
|
|
1110
|
+
})
|
|
1111
|
+
)
|
|
1112
|
+
}));
|
|
1113
|
+
} catch {
|
|
1114
|
+
return `${color5.amber("Models cache not available")}. Run wstack sync-models.`;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
function checkHasKey(pid, config) {
|
|
1118
|
+
if (pid === config.provider && config.provider) return true;
|
|
1119
|
+
const pc = config.providers?.[pid];
|
|
1120
|
+
if (!pc) return false;
|
|
1121
|
+
if (typeof pc.apiKey === "string" && pc.apiKey.length > 0) return true;
|
|
1122
|
+
if (Array.isArray(pc.apiKeys) && pc.apiKeys.some((k) => k?.apiKey)) return true;
|
|
1123
|
+
return false;
|
|
1124
|
+
}
|
|
1125
|
+
var modeldiagCmd = async (args, deps) => {
|
|
1126
|
+
const sub = args[0]?.toLowerCase() || "full";
|
|
1127
|
+
const cacheResult = await readProviders(deps.paths.modelsCache);
|
|
1128
|
+
if (typeof cacheResult === "string") {
|
|
1129
|
+
deps.renderer.write(`${cacheResult}
|
|
1130
|
+
`);
|
|
1131
|
+
return cacheResult.includes(color5.red("")) ? 1 : 0;
|
|
1132
|
+
}
|
|
1133
|
+
const providers = cacheResult;
|
|
1134
|
+
const config = deps.config;
|
|
1135
|
+
const modelMatrix = config.modelMatrix ?? {};
|
|
1136
|
+
function hasKey(pid) {
|
|
1137
|
+
return checkHasKey(pid, config);
|
|
1138
|
+
}
|
|
1139
|
+
function writeLine(line = "") {
|
|
1140
|
+
deps.renderer.write(`${line}
|
|
1141
|
+
`);
|
|
1142
|
+
}
|
|
1143
|
+
if (sub === "keys") {
|
|
1144
|
+
writeLine(`${color5.bold("API Key Status")}`);
|
|
1145
|
+
writeLine();
|
|
1146
|
+
for (const prov of providers) {
|
|
1147
|
+
const k = hasKey(prov.id);
|
|
1148
|
+
writeLine(` ${checkMark(k)} ${color5.bold(prov.id.padEnd(18))} ${color5.dim(prov.name)}`);
|
|
1149
|
+
}
|
|
1150
|
+
writeLine();
|
|
1151
|
+
writeLine(`${color5.dim(`Leader: ${config.provider}/${config.model}`)}`);
|
|
1152
|
+
return 0;
|
|
1153
|
+
}
|
|
1154
|
+
if (sub === "caps") {
|
|
1155
|
+
writeLine(`${color5.bold("Model Capabilities")} ${color5.dim("\u2014 matched to known profiles")}`);
|
|
1156
|
+
writeLine();
|
|
1157
|
+
for (const prov of providers) {
|
|
1158
|
+
if (!hasKey(prov.id)) continue;
|
|
1159
|
+
writeLine(` ${color5.bold(prov.id)} ${color5.dim(`(${prov.name})`)}`);
|
|
1160
|
+
const tiers = {
|
|
1161
|
+
premium: [],
|
|
587
1162
|
standard: [],
|
|
588
1163
|
budget: [],
|
|
589
1164
|
unknown: []
|
|
@@ -595,24 +1170,24 @@ var modeldiagCmd = async (args, deps) => {
|
|
|
595
1170
|
for (const tier of ["premium", "standard", "budget", "unknown"]) {
|
|
596
1171
|
const tierModels = tiers[tier];
|
|
597
1172
|
if (tierModels.length === 0) continue;
|
|
598
|
-
const label = tier === "unknown" ?
|
|
1173
|
+
const label = tier === "unknown" ? color5.dim("unmatched") : `${costLabel(tier)} ${tier}`;
|
|
599
1174
|
writeLine(` ${label}`);
|
|
600
1175
|
for (const m of tierModels) {
|
|
601
1176
|
const cap = m.capabilities;
|
|
602
1177
|
const ctx = cap?.contextWindow ?? 0;
|
|
603
1178
|
const maxOut = cap?.maxOutputTokens ?? 0;
|
|
604
1179
|
const profile = findProfile(prov.id, m.id);
|
|
605
|
-
const family = profile ? `${speedLabel(profile.speedTier)} ${
|
|
606
|
-
const pricing = m.pricing ? `${
|
|
1180
|
+
const family = profile ? `${speedLabel(profile.speedTier)} ${color5.green(profile.family)}` : color5.dim("no profile match");
|
|
1181
|
+
const pricing = m.pricing ? `${color5.dim("in")}${fmtPrice(m.pricing.input)} ${color5.dim("out")}${fmtPrice(m.pricing.output)}` : color5.dim("pricing ?");
|
|
607
1182
|
writeLine(
|
|
608
|
-
` ${
|
|
1183
|
+
` ${color5.cyan(m.id.padEnd(34))}${ctx > 0 ? `ctx ${fmtTokens(ctx).padEnd(6)}` : color5.dim("ctx ? ")}${maxOut > 0 ? `out ${fmtTokens(maxOut).padEnd(6)}` : " "}${family} ${pricing}`
|
|
609
1184
|
);
|
|
610
1185
|
}
|
|
611
1186
|
}
|
|
612
1187
|
writeLine();
|
|
613
1188
|
}
|
|
614
1189
|
writeLine(
|
|
615
|
-
|
|
1190
|
+
color5.dim(
|
|
616
1191
|
"Prices in USD per 1M tokens (input/output). ctx = context window, out = max output."
|
|
617
1192
|
)
|
|
618
1193
|
);
|
|
@@ -621,10 +1196,10 @@ var modeldiagCmd = async (args, deps) => {
|
|
|
621
1196
|
async function renderSuggest() {
|
|
622
1197
|
writeLine();
|
|
623
1198
|
writeLine(
|
|
624
|
-
`${
|
|
1199
|
+
`${color5.bold("Agent \u2192 Model Suggestions")} ${color5.amber("(heuristic \u2014 untested)")}`
|
|
625
1200
|
);
|
|
626
1201
|
writeLine(
|
|
627
|
-
|
|
1202
|
+
color5.dim(
|
|
628
1203
|
'These are profile-based best guesses. Test them with wstack modeldiag bench <role> "<prompt>".'
|
|
629
1204
|
)
|
|
630
1205
|
);
|
|
@@ -632,7 +1207,7 @@ var modeldiagCmd = async (args, deps) => {
|
|
|
632
1207
|
const keyedProviders = providers.filter((p) => hasKey(p.id));
|
|
633
1208
|
if (keyedProviders.length === 0) {
|
|
634
1209
|
writeLine(
|
|
635
|
-
` ${
|
|
1210
|
+
` ${color5.amber("No providers have API keys configured. Add keys with wstack auth.")}`
|
|
636
1211
|
);
|
|
637
1212
|
} else {
|
|
638
1213
|
const roles = [
|
|
@@ -654,606 +1229,68 @@ var modeldiagCmd = async (args, deps) => {
|
|
|
654
1229
|
const entry = modelMatrix[role];
|
|
655
1230
|
const p = entry.provider ?? config.provider;
|
|
656
1231
|
writeLine(
|
|
657
|
-
` ${
|
|
1232
|
+
` ${color5.dim(role.padEnd(20))} \u2192 ${color5.cyan(`${p}/${entry.model}`)} ${color5.dim("(user-configured)")}`
|
|
658
1233
|
);
|
|
659
1234
|
continue;
|
|
660
1235
|
}
|
|
661
1236
|
const cat = roleCat(role);
|
|
662
1237
|
const ranked = rankModels(providers, hasKey, cat, 3);
|
|
663
1238
|
if (ranked.length === 0) {
|
|
664
|
-
writeLine(` ${
|
|
1239
|
+
writeLine(` ${color5.dim(role.padEnd(20))} \u2192 ${color5.dim("no candidates")}`);
|
|
665
1240
|
continue;
|
|
666
1241
|
}
|
|
667
1242
|
const best = ranked[0];
|
|
668
|
-
const family = best.profile ? ` ${
|
|
1243
|
+
const family = best.profile ? ` ${color5.dim(`(${best.profile.family})`)}` : "";
|
|
669
1244
|
const bar = scoreBar(best.score, 110);
|
|
670
1245
|
writeLine(
|
|
671
|
-
` ${
|
|
1246
|
+
` ${color5.amber(role.padEnd(20))} \u2192 ${color5.cyan(`${best.provider}/${best.model}`)}${family}`
|
|
672
1247
|
);
|
|
673
|
-
writeLine(` ${" ".repeat(22)} ${bar} ${
|
|
1248
|
+
writeLine(` ${" ".repeat(22)} ${bar} ${color5.dim(cat)}`);
|
|
674
1249
|
if (ranked.length > 1 && (ranked[1]?.score ?? Number.NEGATIVE_INFINITY) >= best.score - 15) {
|
|
675
1250
|
for (const alt of ranked.slice(1)) {
|
|
676
1251
|
const af = alt.profile ? ` (${alt.profile.family})` : "";
|
|
677
1252
|
writeLine(
|
|
678
|
-
` ${" ".repeat(22)} ${
|
|
1253
|
+
` ${" ".repeat(22)} ${color5.dim(`${alt.provider}/${alt.model}${af} score ${alt.score}`)}`
|
|
679
1254
|
);
|
|
680
1255
|
}
|
|
681
1256
|
}
|
|
682
1257
|
}
|
|
683
1258
|
writeLine();
|
|
684
1259
|
writeLine(
|
|
685
|
-
` ${
|
|
1260
|
+
` ${color5.bold("leader".padEnd(20))} \u2192 ${color5.cyan(`${config.provider}/${config.model}`)}`
|
|
686
1261
|
);
|
|
687
1262
|
}
|
|
688
1263
|
}
|
|
689
1264
|
if (sub === "suggest") {
|
|
690
1265
|
await renderSuggest();
|
|
691
1266
|
writeLine();
|
|
692
|
-
writeLine(
|
|
693
|
-
writeLine(
|
|
1267
|
+
writeLine(color5.dim("Pin a suggestion: wstack setmodel set <role> <provider>/<model>"));
|
|
1268
|
+
writeLine(color5.dim('Test candidates: wstack modeldiag bench <role> "<test prompt>"'));
|
|
694
1269
|
return 0;
|
|
695
1270
|
}
|
|
696
1271
|
if (sub === "test") {
|
|
697
|
-
|
|
698
|
-
writeLine(`${color2.bold("Usage:")} wstack modeldiag test [options]`);
|
|
699
|
-
writeLine();
|
|
700
|
-
writeLine(" (no flags) Probe one representative model per configured provider");
|
|
701
|
-
writeLine(" --all-models Probe every catalog-visible model sequentially");
|
|
702
|
-
writeLine(" --plan Show targets without making model calls");
|
|
703
|
-
writeLine(" --provider=<id,...> Restrict provider ids");
|
|
704
|
-
writeLine(" --model=<id,...> Restrict model ids");
|
|
705
|
-
writeLine(" --timeout=<ms> Per-model timeout (default 45000)");
|
|
706
|
-
writeLine(" --max-tokens=<n> Output cap for the tiny probe (default 32)");
|
|
707
|
-
writeLine(" --json Emit machine-readable output");
|
|
708
|
-
writeLine(" --yes Confirm a run larger than 50 requests");
|
|
709
|
-
return 0;
|
|
710
|
-
}
|
|
711
|
-
let smokeOptions;
|
|
712
|
-
try {
|
|
713
|
-
smokeOptions = parseModelSmokeOptions(args.slice(1), deps.flags);
|
|
714
|
-
} catch (err) {
|
|
715
|
-
writeLine(`${color2.red("Invalid smoke-test options:")} ${toErrorMessage2(err)}`);
|
|
716
|
-
return 2;
|
|
717
|
-
}
|
|
718
|
-
if ("mergeOverlay" in deps.modelsRegistry) {
|
|
719
|
-
await discoverAndMergeProviders({
|
|
720
|
-
config,
|
|
721
|
-
registry: deps.modelsRegistry,
|
|
722
|
-
cacheDir: deps.paths.cacheDir
|
|
723
|
-
});
|
|
724
|
-
}
|
|
725
|
-
const targets = await buildModelSmokeTargets(
|
|
726
|
-
config,
|
|
727
|
-
deps.modelsRegistry,
|
|
728
|
-
smokeOptions
|
|
729
|
-
);
|
|
730
|
-
const byProvider = /* @__PURE__ */ new Map();
|
|
731
|
-
for (const target of targets) {
|
|
732
|
-
byProvider.set(target.providerId, (byProvider.get(target.providerId) ?? 0) + 1);
|
|
733
|
-
}
|
|
734
|
-
if (smokeOptions.planOnly) {
|
|
735
|
-
const plan = {
|
|
736
|
-
mode: smokeOptions.allModels ? "all-models" : "representative",
|
|
737
|
-
targetCount: targets.length,
|
|
738
|
-
providers: Object.fromEntries(byProvider),
|
|
739
|
-
targets
|
|
740
|
-
};
|
|
741
|
-
if (smokeOptions.json) {
|
|
742
|
-
writeLine(JSON.stringify(plan, null, 2));
|
|
743
|
-
} else {
|
|
744
|
-
writeLine(`${color2.bold("Provider/Model Smoke Test Plan")}`);
|
|
745
|
-
writeLine();
|
|
746
|
-
for (const [providerId, count] of byProvider) {
|
|
747
|
-
writeLine(` ${color2.cyan(providerId.padEnd(24))} ${count} model(s)`);
|
|
748
|
-
}
|
|
749
|
-
writeLine();
|
|
750
|
-
writeLine(
|
|
751
|
-
color2.dim(
|
|
752
|
-
`${targets.length} request(s) would run sequentially. Remove --plan to execute.`
|
|
753
|
-
)
|
|
754
|
-
);
|
|
755
|
-
}
|
|
756
|
-
return targets.length > 0 ? 0 : 2;
|
|
757
|
-
}
|
|
758
|
-
if (targets.length === 0) {
|
|
759
|
-
const message = "No provider/model targets matched. Check the active profile or remove filters.";
|
|
760
|
-
if (smokeOptions.json) {
|
|
761
|
-
writeLine(
|
|
762
|
-
JSON.stringify({ results: [], summary: { passed: 0, failed: 0 }, error: message })
|
|
763
|
-
);
|
|
764
|
-
} else {
|
|
765
|
-
writeLine(color2.amber(message));
|
|
766
|
-
}
|
|
767
|
-
return 2;
|
|
768
|
-
}
|
|
769
|
-
const confirmed = deps.flags?.["yes"] === true || deps.flags?.["yes"] === "true" || args.slice(1).includes("--yes");
|
|
770
|
-
if (targets.length > 50 && !confirmed) {
|
|
771
|
-
const message = `${targets.length} live requests are planned. Review with --plan, then add --yes to run them.`;
|
|
772
|
-
if (smokeOptions.json) {
|
|
773
|
-
writeLine(JSON.stringify({ error: message, targetCount: targets.length }));
|
|
774
|
-
} else {
|
|
775
|
-
writeLine(color2.amber(message));
|
|
776
|
-
}
|
|
777
|
-
return 2;
|
|
778
|
-
}
|
|
779
|
-
if (!smokeOptions.json) {
|
|
780
|
-
writeLine(`${color2.bold("Live Provider/Model Smoke Test")}`);
|
|
781
|
-
writeLine(
|
|
782
|
-
color2.dim(
|
|
783
|
-
`${targets.length} request(s), sequential, ${smokeOptions.timeoutMs}ms timeout, max ${smokeOptions.maxTokens} output tokens.`
|
|
784
|
-
)
|
|
785
|
-
);
|
|
786
|
-
writeLine();
|
|
787
|
-
}
|
|
788
|
-
const resultLines = (result, index, total) => {
|
|
789
|
-
if (smokeOptions.json) return;
|
|
790
|
-
const label = `[${String(index + 1).padStart(String(total).length)}/${total}]`;
|
|
791
|
-
const target = `${result.providerId}/${result.modelId}`;
|
|
792
|
-
if (result.status === "passed") {
|
|
793
|
-
writeLine(
|
|
794
|
-
` ${color2.green("\u2713")} ${color2.dim(label)} ${color2.cyan(target)} ${color2.dim(
|
|
795
|
-
`${fmtMs(result.latencyMs)} \xB7 in${result.usage.input}/out${result.usage.output} \xB7 ${result.stopReason}`
|
|
796
|
-
)}`
|
|
797
|
-
);
|
|
798
|
-
} else {
|
|
799
|
-
const status = result.httpStatus ? `HTTP ${result.httpStatus} \xB7 ` : "";
|
|
800
|
-
writeLine(
|
|
801
|
-
` ${color2.red("\u2717")} ${color2.dim(label)} ${color2.red(target)} ${color2.dim(
|
|
802
|
-
`${fmtMs(result.latencyMs)} \xB7 ${status}${result.errorKind}`
|
|
803
|
-
)}`
|
|
804
|
-
);
|
|
805
|
-
writeLine(` ${color2.red(result.error.slice(0, 240))}`);
|
|
806
|
-
}
|
|
807
|
-
};
|
|
808
|
-
let oauthWriteChain = Promise.resolve();
|
|
809
|
-
let oauthWriteScheduled = false;
|
|
810
|
-
const profilePath = activeProfileConfigPath(deps.paths, config);
|
|
811
|
-
setOAuthTokenPersister((providerId, credentials) => {
|
|
812
|
-
oauthWriteScheduled = true;
|
|
813
|
-
oauthWriteChain = oauthWriteChain.then(
|
|
814
|
-
() => mutateConfigProviders(profilePath, deps.vault, (all) => {
|
|
815
|
-
const providerConfig = all[providerId];
|
|
816
|
-
if (!providerConfig) return;
|
|
817
|
-
const keys = normalizeKeys(providerConfig);
|
|
818
|
-
const active = providerConfig.activeKey ? keys.find((key) => key.label === providerConfig.activeKey) : keys[0];
|
|
819
|
-
if (!active) return;
|
|
820
|
-
active.apiKey = credentials.accessToken;
|
|
821
|
-
active.refreshToken = credentials.refreshToken;
|
|
822
|
-
active.expiresAt = new Date(credentials.expiresAt).toISOString();
|
|
823
|
-
if (credentials.accountId) active.accountId = credentials.accountId;
|
|
824
|
-
writeKeysBack(providerConfig, keys);
|
|
825
|
-
})
|
|
826
|
-
);
|
|
827
|
-
});
|
|
828
|
-
let results;
|
|
829
|
-
try {
|
|
830
|
-
results = await runModelSmokeTests({
|
|
831
|
-
targets,
|
|
832
|
-
options: smokeOptions,
|
|
833
|
-
createProvider: async (providerId) => {
|
|
834
|
-
const saved = config.providers?.[providerId];
|
|
835
|
-
const resolved = await deps.modelsRegistry.getProvider(providerId).catch(() => void 0) ?? (saved?.type && saved.type !== providerId ? await deps.modelsRegistry.getProvider(saved.type).catch(() => void 0) : void 0);
|
|
836
|
-
const providerConfig = {
|
|
837
|
-
...providerId === config.provider ? {
|
|
838
|
-
...config.apiKey ? { apiKey: config.apiKey } : {},
|
|
839
|
-
...config.baseUrl ? { baseUrl: config.baseUrl } : {}
|
|
840
|
-
} : {},
|
|
841
|
-
...saved,
|
|
842
|
-
type: providerId,
|
|
843
|
-
...saved?.family ?? resolved?.family ? { family: saved?.family ?? resolved?.family } : {},
|
|
844
|
-
...saved?.baseUrl ?? resolved?.apiBase ? { baseUrl: saved?.baseUrl ?? resolved?.apiBase } : {},
|
|
845
|
-
...saved?.envVars ?? resolved?.envVars ? { envVars: saved?.envVars ?? resolved?.envVars } : {}
|
|
846
|
-
};
|
|
847
|
-
return makeProviderFromConfig(providerId, providerConfig);
|
|
848
|
-
},
|
|
849
|
-
onTargetComplete: resultLines
|
|
850
|
-
});
|
|
851
|
-
} finally {
|
|
852
|
-
setOAuthTokenPersister(void 0);
|
|
853
|
-
}
|
|
854
|
-
let oauthPersistenceError;
|
|
855
|
-
if (oauthWriteScheduled) {
|
|
856
|
-
try {
|
|
857
|
-
await oauthWriteChain;
|
|
858
|
-
} catch (err) {
|
|
859
|
-
oauthPersistenceError = toErrorMessage2(err);
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
const passed = results.filter((result) => result.status === "passed").length;
|
|
863
|
-
const failed = results.length - passed;
|
|
864
|
-
const summary = { passed, failed, total: results.length };
|
|
865
|
-
if (smokeOptions.json) {
|
|
866
|
-
writeLine(
|
|
867
|
-
JSON.stringify(
|
|
868
|
-
{
|
|
869
|
-
results,
|
|
870
|
-
summary,
|
|
871
|
-
...oauthPersistenceError ? {
|
|
872
|
-
warning: `OAuth token refresh worked in-memory but could not be saved: ${oauthPersistenceError}`
|
|
873
|
-
} : {}
|
|
874
|
-
},
|
|
875
|
-
null,
|
|
876
|
-
2
|
|
877
|
-
)
|
|
878
|
-
);
|
|
879
|
-
} else {
|
|
880
|
-
writeLine();
|
|
881
|
-
writeLine(
|
|
882
|
-
`${color2.bold("Summary:")} ${color2.green(`${passed} passed`)} \xB7 ${failed > 0 ? color2.red(`${failed} failed`) : color2.dim("0 failed")}`
|
|
883
|
-
);
|
|
884
|
-
if (!smokeOptions.allModels) {
|
|
885
|
-
writeLine(color2.dim("Use --all-models to test every catalog-visible model sequentially."));
|
|
886
|
-
}
|
|
887
|
-
if (oauthPersistenceError) {
|
|
888
|
-
writeLine(
|
|
889
|
-
color2.amber(
|
|
890
|
-
`OAuth token refresh worked in-memory but could not be saved: ${oauthPersistenceError}`
|
|
891
|
-
)
|
|
892
|
-
);
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
return failed === 0 ? 0 : 1;
|
|
1272
|
+
return runModeldiagTest(args, deps, config);
|
|
896
1273
|
}
|
|
897
1274
|
if (sub === "bench") {
|
|
898
|
-
|
|
899
|
-
if (benchArgs.length < 2) {
|
|
900
|
-
writeLine(
|
|
901
|
-
`${color2.amber("Usage:")} wstack modeldiag bench <role> "<test prompt>" [--providers=p1,p2]`
|
|
902
|
-
);
|
|
903
|
-
writeLine();
|
|
904
|
-
writeLine(
|
|
905
|
-
color2.dim(
|
|
906
|
-
'Example: wstack modeldiag bench verify "Write a function that checks if a string is a palindrome"'
|
|
907
|
-
)
|
|
908
|
-
);
|
|
909
|
-
writeLine(
|
|
910
|
-
color2.dim(
|
|
911
|
-
"Tests the top 5 candidate models for the role with your prompt and reports results."
|
|
912
|
-
)
|
|
913
|
-
);
|
|
914
|
-
writeLine(color2.dim("Add --providers=anthropic,google to test across multiple providers."));
|
|
915
|
-
return 0;
|
|
916
|
-
}
|
|
917
|
-
const providersEqIdx = benchArgs.findIndex((a) => a.startsWith("--providers="));
|
|
918
|
-
let providerFilter;
|
|
919
|
-
if (providersEqIdx >= 0) {
|
|
920
|
-
const rawFilter = benchArgs[providersEqIdx]?.replace("--providers=", "").split(",");
|
|
921
|
-
if (rawFilter) {
|
|
922
|
-
providerFilter = rawFilter.map((s) => s.trim()).filter(Boolean);
|
|
923
|
-
benchArgs.splice(providersEqIdx, 1);
|
|
924
|
-
}
|
|
925
|
-
} else {
|
|
926
|
-
const rawFilter = deps.flags?.["providers"] ?? deps.flags?.["provider"];
|
|
927
|
-
if (typeof rawFilter === "string") {
|
|
928
|
-
providerFilter = rawFilter.split(",").map((s) => s.trim()).filter(Boolean);
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
const benchRole = benchArgs[0];
|
|
932
|
-
if (!benchRole) {
|
|
933
|
-
writeLine(
|
|
934
|
-
`${color2.amber("No benchmark role specified")}. Usage: wstack diag bench <role> [prompt]`
|
|
935
|
-
);
|
|
936
|
-
return 1;
|
|
937
|
-
}
|
|
938
|
-
const benchPrompt = benchArgs.slice(1).join(" ");
|
|
939
|
-
const cat = roleCat(benchRole);
|
|
940
|
-
const candidates = rankModels(providers, hasKey, cat, 5);
|
|
941
|
-
if (candidates.length === 0) {
|
|
942
|
-
writeLine(
|
|
943
|
-
`${color2.amber("No candidate models found")} for role "${benchRole}" (category: ${cat}).`
|
|
944
|
-
);
|
|
945
|
-
return 0;
|
|
946
|
-
}
|
|
947
|
-
let targetCandidates;
|
|
948
|
-
if (providerFilter && providerFilter.length > 0) {
|
|
949
|
-
targetCandidates = candidates.filter((c) => providerFilter.includes(c.provider));
|
|
950
|
-
if (targetCandidates.length === 0) {
|
|
951
|
-
writeLine(
|
|
952
|
-
`${color2.amber("No candidates match the specified providers")}: ${providerFilter.join(", ")}`
|
|
953
|
-
);
|
|
954
|
-
writeLine(
|
|
955
|
-
`Candidate providers: ${[...new Set(candidates.map((c) => c.provider))].join(", ")}`
|
|
956
|
-
);
|
|
957
|
-
return 0;
|
|
958
|
-
}
|
|
959
|
-
} else {
|
|
960
|
-
targetCandidates = candidates;
|
|
961
|
-
}
|
|
962
|
-
writeLine(
|
|
963
|
-
`${color2.bold("Model Benchmark")} \u2014 ${color2.amber(benchRole)} ${color2.dim(`(category: ${cat})`)}`
|
|
964
|
-
);
|
|
965
|
-
writeLine(
|
|
966
|
-
`${color2.dim("Prompt:")} "${benchPrompt.slice(0, 120)}${benchPrompt.length > 120 ? "\u2026" : ""}"`
|
|
967
|
-
);
|
|
968
|
-
writeLine();
|
|
969
|
-
writeLine(
|
|
970
|
-
` ${color2.dim("# model".padEnd(52))} ${color2.dim("score".padEnd(12))} ${color2.dim("latency".padEnd(10))} ${color2.dim("tokens".padEnd(14))} ${color2.dim("first line")}`
|
|
971
|
-
);
|
|
972
|
-
writeLine(` ${color2.dim("\u2500".repeat(108))}`);
|
|
973
|
-
const providerInstances = /* @__PURE__ */ new Map();
|
|
974
|
-
for (const pid of [...new Set(targetCandidates.map((c) => c.provider))]) {
|
|
975
|
-
const prov = createProviderForId(pid, config);
|
|
976
|
-
if (prov) providerInstances.set(pid, prov);
|
|
977
|
-
}
|
|
978
|
-
let idx = 0;
|
|
979
|
-
for (const c of targetCandidates.slice(0, 20)) {
|
|
980
|
-
idx++;
|
|
981
|
-
const label = `${idx}`.padStart(2);
|
|
982
|
-
const modelKey = `${c.provider}/${c.model}`;
|
|
983
|
-
const prov = providerInstances.get(c.provider);
|
|
984
|
-
if (!prov) {
|
|
985
|
-
writeLine(
|
|
986
|
-
` ${label} ${color2.red(modelKey.padEnd(50))} ${scoreBar(c.score, 110).slice(0, 11)} ${color2.red("NO PROVIDER")}`
|
|
987
|
-
);
|
|
988
|
-
continue;
|
|
989
|
-
}
|
|
990
|
-
try {
|
|
991
|
-
const start = Date.now();
|
|
992
|
-
const resp = await prov.complete(
|
|
993
|
-
{
|
|
994
|
-
model: c.model,
|
|
995
|
-
messages: [{ role: "user", content: [{ type: "text", text: benchPrompt }] }],
|
|
996
|
-
maxTokens: 256
|
|
997
|
-
},
|
|
998
|
-
{ signal: AbortSignal.timeout(3e4) }
|
|
999
|
-
);
|
|
1000
|
-
const latency = Date.now() - start;
|
|
1001
|
-
const firstText = resp.content[0] && "text" in resp.content[0] ? resp.content[0].text : "";
|
|
1002
|
-
const firstLineClean = firstText.replace(/\n/g, " ").slice(0, 80) || color2.dim("(empty)");
|
|
1003
|
-
const provColor = c.provider === config.provider ? color2.green : color2.cyan;
|
|
1004
|
-
const usage = resp.usage;
|
|
1005
|
-
writeLine(
|
|
1006
|
-
` ${label} ${provColor(modelKey.padEnd(50))} ${scoreBar(c.score, 110).slice(0, 11)} ${color2.amber(fmtMs(latency).padEnd(8))} ${color2.dim(`in${usage?.input ?? "?"}/out${usage?.output ?? "?"}`.padEnd(12))} ${firstLineClean}`
|
|
1007
|
-
);
|
|
1008
|
-
} catch (err) {
|
|
1009
|
-
const errMsg = toErrorMessage2(err);
|
|
1010
|
-
writeLine(
|
|
1011
|
-
` ${label} ${color2.red(modelKey.padEnd(50))} ${scoreBar(c.score, 110).slice(0, 11)} ${color2.red("FAILED")} ${color2.dim(errMsg.slice(0, 40))}`
|
|
1012
|
-
);
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
const testedProviders = [...new Set(targetCandidates.map((c) => c.provider))];
|
|
1016
|
-
writeLine();
|
|
1017
|
-
writeLine(
|
|
1018
|
-
color2.dim(
|
|
1019
|
-
`Tested ${idx} model(s) across ${testedProviders.length} provider(s): ${testedProviders.join(", ")}.`
|
|
1020
|
-
)
|
|
1021
|
-
);
|
|
1022
|
-
writeLine(color2.dim("Pin the best: wstack setmodel set <role> <provider>/<model>"));
|
|
1023
|
-
return 0;
|
|
1275
|
+
return runModeldiagBench(args, deps, providers, config, hasKey);
|
|
1024
1276
|
}
|
|
1025
1277
|
if (sub === "eval" || sub === "evall") {
|
|
1026
|
-
|
|
1027
|
-
const providersEq = evalArgs.find((a) => a.startsWith("--providers="));
|
|
1028
|
-
const providersValue = providersEq?.replace("--providers=", "") ?? (typeof deps.flags?.["providers"] === "string" ? deps.flags["providers"] : typeof deps.flags?.["provider"] === "string" ? deps.flags["provider"] : void 0);
|
|
1029
|
-
const providerFilter = providersValue ? providersValue.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
1030
|
-
const maxEq = evalArgs.find((a) => a.startsWith("--max="));
|
|
1031
|
-
const maxValue = maxEq?.replace("--max=", "") ?? (typeof deps.flags?.["max"] === "string" ? deps.flags["max"] : void 0);
|
|
1032
|
-
const maxModels = maxValue ? Math.max(1, parseInt(maxValue, 10) || 2) : 2;
|
|
1033
|
-
const quick = evalArgs.includes("--quick") || deps.flags?.["quick"] === true || deps.flags?.["quick"] === "true";
|
|
1034
|
-
const modelsPerCat = quick ? 1 : maxModels;
|
|
1035
|
-
const roleFilter = evalArgs.find((a) => !a.startsWith("--"));
|
|
1036
|
-
const targetCategories = roleFilter ? EVAL_CATEGORIES.includes(roleCat(roleFilter)) ? [roleCat(roleFilter)] : [] : EVAL_CATEGORIES;
|
|
1037
|
-
if (targetCategories.length === 0 && roleFilter) {
|
|
1038
|
-
writeLine(
|
|
1039
|
-
`${color2.amber("Unknown role/category")}: "${roleFilter}". Try: ${EVAL_CATEGORIES.join(", ")}`
|
|
1040
|
-
);
|
|
1041
|
-
return 1;
|
|
1042
|
-
}
|
|
1043
|
-
const keyedProviderIds = providers.filter((p) => hasKey(p.id)).map((p) => p.id);
|
|
1044
|
-
let targetProviderIds;
|
|
1045
|
-
if (providerFilter && providerFilter.length > 0) {
|
|
1046
|
-
const unknown = providerFilter.filter((pid) => !keyedProviderIds.includes(pid));
|
|
1047
|
-
targetProviderIds = providerFilter.filter((pid) => keyedProviderIds.includes(pid));
|
|
1048
|
-
if (targetProviderIds.length === 0) {
|
|
1049
|
-
const noKeyMsg = unknown.length > 0 ? `None of the specified providers (${unknown.join(", ")}) have API keys. Add keys with wstack auth.` : "None of the specified providers have API keys configured.";
|
|
1050
|
-
writeLine(`${color2.amber(noKeyMsg)}`);
|
|
1051
|
-
return 0;
|
|
1052
|
-
}
|
|
1053
|
-
} else if (keyedProviderIds.length === 0) {
|
|
1054
|
-
writeLine(`${color2.amber("No providers have API keys. Add keys with wstack auth.")}`);
|
|
1055
|
-
return 0;
|
|
1056
|
-
} else if (keyedProviderIds.length === 1) {
|
|
1057
|
-
targetProviderIds = keyedProviderIds;
|
|
1058
|
-
} else {
|
|
1059
|
-
const providerList = keyedProviderIds.map((pid, i) => {
|
|
1060
|
-
const info = providers.find((p) => p.id === pid);
|
|
1061
|
-
return ` ${color2.cyan(String(i + 1))}) ${color2.bold(pid.padEnd(16))} ${color2.dim(info?.name ?? "")}`;
|
|
1062
|
-
}).join("\n");
|
|
1063
|
-
deps.renderer.write(
|
|
1064
|
-
`
|
|
1065
|
-
${color2.bold("Select providers to evaluate")}
|
|
1066
|
-
|
|
1067
|
-
${providerList}
|
|
1068
|
-
|
|
1069
|
-
${color2.dim('Enter numbers or provider IDs (comma-separated, or "all"):')}
|
|
1070
|
-
`
|
|
1071
|
-
);
|
|
1072
|
-
const input = await deps.reader.readLine(" > ");
|
|
1073
|
-
const selected = input.trim().toLowerCase();
|
|
1074
|
-
if (selected === "" || selected === "all") {
|
|
1075
|
-
targetProviderIds = keyedProviderIds;
|
|
1076
|
-
} else {
|
|
1077
|
-
targetProviderIds = [];
|
|
1078
|
-
for (const part of selected.split(",").map((s) => s.trim())) {
|
|
1079
|
-
const idx = parseInt(part, 10);
|
|
1080
|
-
if (idx >= 1 && idx <= keyedProviderIds.length) {
|
|
1081
|
-
const pid = keyedProviderIds[idx - 1];
|
|
1082
|
-
if (!targetProviderIds.includes(pid)) targetProviderIds.push(pid);
|
|
1083
|
-
} else if (keyedProviderIds.includes(part)) {
|
|
1084
|
-
if (!targetProviderIds.includes(part)) targetProviderIds.push(part);
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
if (targetProviderIds.length === 0) {
|
|
1089
|
-
writeLine(color2.dim("No providers selected."));
|
|
1090
|
-
return 0;
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
const leaderModel = config.model ?? "unknown";
|
|
1094
|
-
const unknownProviders = providerFilter ? providerFilter.filter((pid) => !keyedProviderIds.includes(pid)) : [];
|
|
1095
|
-
const warningLine = unknownProviders.length > 0 ? ` ${color2.amber("\u26A0 skipped (no key):")} ${unknownProviders.join(", ")}
|
|
1096
|
-
` : "";
|
|
1097
|
-
writeLine(`${color2.bold("Model Competency Evaluation")}`);
|
|
1098
|
-
writeLine(
|
|
1099
|
-
color2.dim(
|
|
1100
|
-
`Providers: ${targetProviderIds.join(", ")} | ${targetCategories.length} cats | ${modelsPerCat} model(s)/cat/provider`
|
|
1101
|
-
)
|
|
1102
|
-
);
|
|
1103
|
-
writeLine(warningLine);
|
|
1104
|
-
writeLine(color2.dim(`Leader (ranker): ${config.provider}/${leaderModel}`));
|
|
1105
|
-
writeLine();
|
|
1106
|
-
const collected = /* @__PURE__ */ new Map();
|
|
1107
|
-
let total = 0;
|
|
1108
|
-
let ok = 0;
|
|
1109
|
-
for (const pid of targetProviderIds) {
|
|
1110
|
-
const prov = createProviderForId(pid, config);
|
|
1111
|
-
if (!prov) {
|
|
1112
|
-
writeLine(color2.dim(` \u2298 ${pid}: provider unavailable, skipping`));
|
|
1113
|
-
continue;
|
|
1114
|
-
}
|
|
1115
|
-
for (const cat of targetCategories) {
|
|
1116
|
-
const task = EVAL_TASKS[cat];
|
|
1117
|
-
if (!task) continue;
|
|
1118
|
-
const candidates = rankModels(
|
|
1119
|
-
providers,
|
|
1120
|
-
(providerId) => providerId === pid && hasKey(providerId),
|
|
1121
|
-
cat,
|
|
1122
|
-
modelsPerCat
|
|
1123
|
-
);
|
|
1124
|
-
if (candidates.length === 0) continue;
|
|
1125
|
-
if (!collected.has(cat)) collected.set(cat, /* @__PURE__ */ new Map());
|
|
1126
|
-
for (const c of candidates) {
|
|
1127
|
-
total++;
|
|
1128
|
-
const modelKey = `${pid}/${c.model}`;
|
|
1129
|
-
try {
|
|
1130
|
-
const start = Date.now();
|
|
1131
|
-
const resp = await prov.complete(
|
|
1132
|
-
{
|
|
1133
|
-
model: c.model,
|
|
1134
|
-
system: [{ type: "text", text: "Be thorough and correct." }],
|
|
1135
|
-
messages: [
|
|
1136
|
-
{ role: "user", content: [{ type: "text", text: task.prompt }] }
|
|
1137
|
-
],
|
|
1138
|
-
maxTokens: 1024
|
|
1139
|
-
},
|
|
1140
|
-
{ signal: AbortSignal.timeout(45e3) }
|
|
1141
|
-
);
|
|
1142
|
-
const respText = resp.content[0] && "text" in resp.content[0] ? resp.content[0].text : "";
|
|
1143
|
-
const respUsage = resp.usage;
|
|
1144
|
-
collected.get(cat)?.set(modelKey, {
|
|
1145
|
-
model: modelKey,
|
|
1146
|
-
latency: Date.now() - start,
|
|
1147
|
-
tokens: (respUsage?.input ?? 0) + (respUsage?.output ?? 0),
|
|
1148
|
-
text: respText
|
|
1149
|
-
});
|
|
1150
|
-
ok++;
|
|
1151
|
-
} catch {
|
|
1152
|
-
collected.get(cat)?.set(modelKey, {
|
|
1153
|
-
model: modelKey,
|
|
1154
|
-
latency: -1,
|
|
1155
|
-
tokens: 0,
|
|
1156
|
-
text: ""
|
|
1157
|
-
});
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
writeLine(`${color2.dim(`Phase 1: ${ok}/${total} calls succeeded`)}`);
|
|
1163
|
-
writeLine();
|
|
1164
|
-
if (collected.size === 0) {
|
|
1165
|
-
writeLine(color2.amber("No responses collected. Check provider configuration."));
|
|
1166
|
-
return 0;
|
|
1167
|
-
}
|
|
1168
|
-
const leaderProvider = createProviderForId(config.provider, config);
|
|
1169
|
-
if (leaderProvider) {
|
|
1170
|
-
writeLine(`${color2.bold("Phase 2")} \u2014 ${color2.dim("leader ranks responses")}`);
|
|
1171
|
-
writeLine();
|
|
1172
|
-
}
|
|
1173
|
-
const rankings = /* @__PURE__ */ new Map();
|
|
1174
|
-
for (const [cat, responses] of collected) {
|
|
1175
|
-
const valid = Array.from(responses.values()).filter((r) => r.latency >= 0);
|
|
1176
|
-
if (valid.length < 2) {
|
|
1177
|
-
if (valid.length === 1) {
|
|
1178
|
-
const m = valid[0]?.model;
|
|
1179
|
-
if (m !== void 0) {
|
|
1180
|
-
if (!rankings.has(m)) rankings.set(m, /* @__PURE__ */ new Map());
|
|
1181
|
-
rankings.get(m)?.set(cat, { rank: 1, total: 1 });
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
continue;
|
|
1185
|
-
}
|
|
1186
|
-
const task = EVAL_TASKS[cat];
|
|
1187
|
-
if (leaderProvider) {
|
|
1188
|
-
const ranked = await rankResponses(leaderProvider, leaderModel, task.prompt, valid);
|
|
1189
|
-
for (let i = 0; i < valid.length; i++) {
|
|
1190
|
-
const m = valid[ranked[i] ?? i]?.model;
|
|
1191
|
-
if (m === void 0) continue;
|
|
1192
|
-
if (!rankings.has(m)) rankings.set(m, /* @__PURE__ */ new Map());
|
|
1193
|
-
rankings.get(m)?.set(cat, { rank: i + 1, total: valid.length });
|
|
1194
|
-
}
|
|
1195
|
-
} else {
|
|
1196
|
-
for (const r of valid) {
|
|
1197
|
-
if (!rankings.has(r.model)) rankings.set(r.model, /* @__PURE__ */ new Map());
|
|
1198
|
-
rankings.get(r.model)?.set(cat, { rank: 1, total: valid.length });
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
writeLine(`${color2.bold("Competency Report")}`);
|
|
1203
|
-
writeLine();
|
|
1204
|
-
const allModels = [.../* @__PURE__ */ new Set([...rankings.keys()])].sort();
|
|
1205
|
-
const catList = [...collected.keys()];
|
|
1206
|
-
const modelColWidth = Math.max(24, ...allModels.map((m) => m.length)) + 2;
|
|
1207
|
-
const cw = 12;
|
|
1208
|
-
writeLine(
|
|
1209
|
-
` ${color2.dim("model".padEnd(modelColWidth))}` + catList.map((c) => color2.dim((EVAL_TASKS[c]?.label ?? c).slice(0, cw).padEnd(cw + 2))).join("")
|
|
1210
|
-
);
|
|
1211
|
-
writeLine(` ${color2.dim("\u2500".repeat(modelColWidth + catList.length * (cw + 2)))}`);
|
|
1212
|
-
for (const model of allModels) {
|
|
1213
|
-
const mr = rankings.get(model);
|
|
1214
|
-
const provFromModel = model.split("/")[0] ?? "";
|
|
1215
|
-
const modelColor = provFromModel === config.provider ? color2.cyan : color2.green;
|
|
1216
|
-
let row = ` ${modelColor(model.padEnd(modelColWidth))}`;
|
|
1217
|
-
for (const cat of catList) {
|
|
1218
|
-
const e = mr.get(cat);
|
|
1219
|
-
if (e) {
|
|
1220
|
-
const pct = Math.round((1 - (e.rank - 1) / Math.max(1, e.total - 1)) * 100);
|
|
1221
|
-
const pc = pct >= 80 ? color2.green : pct >= 50 ? color2.amber : color2.red;
|
|
1222
|
-
row += `${pc(`#${e.rank} ${pct}%`.padEnd(cw + 2))}`;
|
|
1223
|
-
} else {
|
|
1224
|
-
row += color2.dim("\u2014".padEnd(cw + 2));
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
writeLine(row);
|
|
1228
|
-
}
|
|
1229
|
-
writeLine();
|
|
1230
|
-
writeLine(color2.dim("#1 100% = best in category. \u2014 = not tested."));
|
|
1231
|
-
writeLine();
|
|
1232
|
-
writeLine(color2.dim("Pin: wstack setmodel set <role> <provider>/<model>"));
|
|
1233
|
-
writeLine(
|
|
1234
|
-
color2.dim(
|
|
1235
|
-
"Full: wstack modeldiag eval Providers: wstack modeldiag eval --providers=id1,id2"
|
|
1236
|
-
)
|
|
1237
|
-
);
|
|
1238
|
-
writeLine(
|
|
1239
|
-
color2.dim("Max: wstack modeldiag eval --max=3 Quick: wstack modeldiag eval --quick")
|
|
1240
|
-
);
|
|
1241
|
-
return 0;
|
|
1278
|
+
return runModeldiagEval(args, deps, providers, config, hasKey);
|
|
1242
1279
|
}
|
|
1243
|
-
writeLine(`${
|
|
1280
|
+
writeLine(`${color5.bold("API Key Status")}`);
|
|
1244
1281
|
writeLine();
|
|
1245
1282
|
for (const prov of providers) {
|
|
1246
1283
|
const k = hasKey(prov.id);
|
|
1247
|
-
writeLine(` ${checkMark(k)} ${
|
|
1284
|
+
writeLine(` ${checkMark(k)} ${color5.bold(prov.id.padEnd(18))} ${color5.dim(prov.name)}`);
|
|
1248
1285
|
}
|
|
1249
1286
|
writeLine();
|
|
1250
|
-
writeLine(`${
|
|
1287
|
+
writeLine(`${color5.dim(`Leader: ${config.provider}/${config.model}`)}`);
|
|
1251
1288
|
writeLine();
|
|
1252
|
-
writeLine(`${
|
|
1289
|
+
writeLine(`${color5.bold("Model Capabilities")} ${color5.dim("\u2014 matched to known profiles")}`);
|
|
1253
1290
|
writeLine();
|
|
1254
1291
|
for (const prov of providers) {
|
|
1255
1292
|
if (!hasKey(prov.id)) continue;
|
|
1256
|
-
writeLine(` ${
|
|
1293
|
+
writeLine(` ${color5.bold(prov.id)} ${color5.dim(`(${prov.name})`)}`);
|
|
1257
1294
|
const tiers = {
|
|
1258
1295
|
premium: [],
|
|
1259
1296
|
standard: [],
|
|
@@ -1267,17 +1304,17 @@ ${color2.dim('Enter numbers or provider IDs (comma-separated, or "all"):')}
|
|
|
1267
1304
|
for (const tier of ["premium", "standard", "budget", "unknown"]) {
|
|
1268
1305
|
const tierModels = tiers[tier];
|
|
1269
1306
|
if (tierModels.length === 0) continue;
|
|
1270
|
-
const label = tier === "unknown" ?
|
|
1307
|
+
const label = tier === "unknown" ? color5.dim("unmatched") : `${costLabel(tier)} ${tier}`;
|
|
1271
1308
|
writeLine(` ${label}`);
|
|
1272
1309
|
for (const m of tierModels) {
|
|
1273
1310
|
const cap = m.capabilities;
|
|
1274
1311
|
const ctx = cap?.contextWindow ?? 0;
|
|
1275
1312
|
const maxOut = cap?.maxOutputTokens ?? 0;
|
|
1276
1313
|
const profile = findProfile(prov.id, m.id);
|
|
1277
|
-
const family = profile ? `${speedLabel(profile.speedTier)} ${
|
|
1278
|
-
const pricing = m.pricing ? `${
|
|
1314
|
+
const family = profile ? `${speedLabel(profile.speedTier)} ${color5.green(profile.family)}` : color5.dim("no profile match");
|
|
1315
|
+
const pricing = m.pricing ? `${color5.dim("in")}${fmtPrice(m.pricing.input)} ${color5.dim("out")}${fmtPrice(m.pricing.output)}` : color5.dim("pricing ?");
|
|
1279
1316
|
writeLine(
|
|
1280
|
-
` ${
|
|
1317
|
+
` ${color5.cyan(m.id.padEnd(34))}${ctx > 0 ? `ctx ${fmtTokens(ctx).padEnd(6)}` : color5.dim("ctx ? ")}${maxOut > 0 ? `out ${fmtTokens(maxOut).padEnd(6)}` : " "}${family} ${pricing}`
|
|
1281
1318
|
);
|
|
1282
1319
|
}
|
|
1283
1320
|
}
|
|
@@ -1285,11 +1322,11 @@ ${color2.dim('Enter numbers or provider IDs (comma-separated, or "all"):')}
|
|
|
1285
1322
|
}
|
|
1286
1323
|
await renderSuggest();
|
|
1287
1324
|
writeLine();
|
|
1288
|
-
writeLine(
|
|
1289
|
-
writeLine(
|
|
1325
|
+
writeLine(color5.dim("Pin a suggestion: wstack setmodel set <role> <provider>/<model>"));
|
|
1326
|
+
writeLine(color5.dim('Test candidates: wstack modeldiag bench <role> "<test prompt>"'));
|
|
1290
1327
|
return 0;
|
|
1291
1328
|
};
|
|
1292
1329
|
export {
|
|
1293
1330
|
modeldiagCmd
|
|
1294
1331
|
};
|
|
1295
|
-
//# sourceMappingURL=modeldiag-
|
|
1332
|
+
//# sourceMappingURL=modeldiag-ICMPHT5L.js.map
|