promptimizer-cli 0.1.24 → 0.1.26

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/README.md CHANGED
@@ -1,21 +1,22 @@
1
1
  # promptimizer-cli
2
2
 
3
- Gemini-style interactive routing CLI for Promptimizer.
3
+ Gemini-style interactive routing CLI for Promptimizer. Multi-host fleets: add and remove providers; the router picks across the merged model set.
4
4
 
5
5
  ```bash
6
- npm install -g promptimizer-cli@latest
6
+ npm install -g ./packages/cli
7
+ # or after publish: npm install -g promptimizer-cli@latest
7
8
  promptimizer
8
9
  ```
9
10
 
10
- Requires **≥ 0.1.22** for REPL `/logout`. If `/help` does not list `/logout`, upgrade the global binary.
11
+ Requires **≥ 0.1.23** for `/hosts`, `/connect`, `/disconnect`.
11
12
 
12
13
  ```text
13
- Promptimizer v0.1.22
14
- Type a prompt, or /help /models /savings /clear /logout /quit
15
- What is 17 * 24?
16
-
17
- 408
18
- model · economy · bootstrap_heuristic · saved $0.0001 · miss
14
+ Promptimizer v0.1.23
15
+ Baseten (16) · NVIDIA NIM (74) · 90 models
16
+ 2 hosts merged · router picks across all
17
+ › /hosts
18
+ ✓ Baseten 16 models
19
+ NVIDIA NIM 74 models
19
20
  ```
20
21
 
21
22
  ## Commands
@@ -25,9 +26,18 @@ Requires **≥ 0.1.22** for REPL `/logout`. If `/help` does not list `/logout`,
25
26
  | `promptimizer` | Interactive multi-turn session |
26
27
  | `promptimizer login --key pmz_live_…` | Save API key |
27
28
  | `promptimizer logout` / REPL `/logout` | Remove `~/.promptimizer/config.json` |
28
- | `promptimizer connect baseten --key $BASETEN_API_KEY` | Attach provider |
29
+ | `promptimizer connect baseten --key $BASETEN_API_KEY` | **Add** a host (keeps existing) |
30
+ | `promptimizer connect nvidia --key $NVIDIA_API_KEY` | Add another host |
31
+ | `promptimizer disconnect nvidia` | **Remove** a host |
32
+ | `promptimizer hosts` | List connected hosts |
33
+ | `promptimizer models` | List merged fleet (tier · host · id) |
29
34
  | `promptimizer chat "…"` | One-shot completion |
30
- | `promptimizer models` | List fleet |
31
35
  | `promptimizer savings` | Account ledger |
32
36
 
37
+ Aliases: `add` → connect, `remove` / `rm` → disconnect.
38
+
39
+ ### REPL slash commands
40
+
41
+ `/hosts` · `/models` · `/connect <host> --key …` · `/disconnect <host>` · `/savings` · `/clear` · `/logout` · `/quit`
42
+
33
43
  Defaults to the hosted gateway. Override with `--url` or `PROMPTIMIZER_URL`.
@@ -30,9 +30,11 @@ const COMMANDS = [
30
30
  ["", "Start interactive session (Gemini-style REPL)"],
31
31
  ["login", "Save a Promptimizer API key"],
32
32
  ["logout", "Remove the saved key"],
33
- ["connect", "Attach a model provider"],
33
+ ["connect", "Add a model host (keeps existing hosts)"],
34
+ ["disconnect", "Remove a model host from the fleet"],
35
+ ["hosts", "List connected hosts"],
34
36
  ["chat", "Route one completion"],
35
- ["models", "List the connected fleet"],
37
+ ["models", "List the merged fleet"],
36
38
  ["savings", "Show account savings"],
37
39
  ["providers", "List known provider URLs"],
38
40
  ];
@@ -50,7 +52,20 @@ const COMMAND_HELP = {
50
52
  " promptimizer connect <provider> --key <vendor-key>",
51
53
  " promptimizer connect custom --base-url <url> --key <vendor-key>",
52
54
  " promptimizer connect simulator",
55
+ "",
56
+ "Adds a host to your fleet without replacing others.",
57
+ "Aliases: add",
58
+ ],
59
+ disconnect: [
60
+ "Usage",
61
+ " promptimizer disconnect <provider>",
62
+ " promptimizer disconnect baseten",
63
+ " promptimizer disconnect nvidia",
64
+ "",
65
+ "Removes that host and its models. Other hosts stay.",
66
+ "Aliases: remove, rm",
53
67
  ],
68
+ hosts: ["Usage", " promptimizer hosts", "", "Show connected hosts and model counts."],
54
69
  chat: [
55
70
  "Usage",
56
71
  ' promptimizer chat "What is 17 * 24?"',
@@ -103,6 +118,15 @@ function parse(argv) {
103
118
  return { flags, positional };
104
119
  }
105
120
 
121
+ /** Parse slash-command args inside the REPL: `/connect baseten --key sk-…` */
122
+ function parseLineArgs(line) {
123
+ const parts = [];
124
+ const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
125
+ let m;
126
+ while ((m = re.exec(line))) parts.push(m[1] ?? m[2] ?? m[3]);
127
+ return parse(parts);
128
+ }
129
+
106
130
  function readConfig() {
107
131
  try {
108
132
  return JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
@@ -133,10 +157,24 @@ function printVersion() {
133
157
  return pkg.version;
134
158
  }
135
159
 
160
+ function hostSummary(session) {
161
+ const connections = session?.connections ?? [];
162
+ if (connections.length) {
163
+ return connections
164
+ .map((c) => {
165
+ const count = (session.models ?? []).filter((m) => m.provider_id === c.id).length;
166
+ return `${c.label}${count ? ` (${count})` : ""}`;
167
+ })
168
+ .join(" · ");
169
+ }
170
+ return session?.label || "not connected";
171
+ }
172
+
136
173
  function banner(session, version) {
137
- const label = session?.label || "not connected";
174
+ const hosts = hostSummary(session);
138
175
  const models = session?.models?.length ?? 0;
139
176
  const baseline = session?.baseline_model || "—";
177
+ const hostCount = session?.connections?.length ?? 0;
140
178
  out();
141
179
  out(color(ANSI.cyan, " ██████╗ ███╗ ███╗███████╗"));
142
180
  out(color(ANSI.cyan, " ██╔══██╗████╗ ████║╚══███╔╝"));
@@ -148,14 +186,30 @@ function banner(session, version) {
148
186
  out(` ${color(ANSI.bold, "Promptimizer")} ${color(ANSI.dim, `v${version}`)}`);
149
187
  out(` ${color(ANSI.dim, "Quality-aware routing · OpenAI-compatible")}`);
150
188
  out();
151
- out(` ${color(ANSI.green, "●")} ${label}${models ? ` · ${models} models` : ""}`);
189
+ out(` ${color(ANSI.green, "●")} ${hosts}${models ? ` · ${models} models` : ""}`);
190
+ if (hostCount > 1) out(` ${color(ANSI.dim, `${hostCount} hosts merged · router picks across all`)}`);
152
191
  out(` ${color(ANSI.dim, `baseline ${baseline}`)}`);
153
192
  out();
154
- out(` ${color(ANSI.dim, "Type a prompt, or /help /models /savings /clear /logout /quit")}`);
193
+ out(
194
+ ` ${color(ANSI.dim, "Type a prompt, or /help /hosts /models /connect /disconnect /clear /quit")}`,
195
+ );
155
196
  out(` ${color(ANSI.dim, "Cache keys on full history — /clear then repeat a prompt to see cache hit")}`);
156
197
  out();
157
198
  }
158
199
 
200
+ function printFleetSummary(session, { added, removed } = {}) {
201
+ const connections = session.connections ?? [];
202
+ if (added) out(`${color(ANSI.green, "✓")} Added ${added}`);
203
+ if (removed) out(`${color(ANSI.green, "✓")} Removed ${removed}`);
204
+ if (!added && !removed) out(`${color(ANSI.green, "✓")} ${session.label}`);
205
+ if (connections.length) {
206
+ out(` hosts ${connections.map((c) => c.label).join(" · ")}`);
207
+ } else {
208
+ out(` ${session.label} ${session.base_url}`);
209
+ }
210
+ out(` models ${session.models?.length ?? 0} · baseline ${session.baseline_model ?? "—"}`);
211
+ }
212
+
159
213
  function help() {
160
214
  out("Usage: promptimizer [--url <gateway>] [command] [options]");
161
215
  out();
@@ -178,8 +232,10 @@ function help() {
178
232
  out(" promptimizer");
179
233
  out(" promptimizer login --key pmz_live_…");
180
234
  out(" promptimizer connect baseten --key $BASETEN_API_KEY");
235
+ out(" promptimizer connect nvidia --key $NVIDIA_API_KEY");
236
+ out(" promptimizer hosts");
237
+ out(" promptimizer disconnect nvidia");
181
238
  out(' promptimizer chat "What is 17 * 24?"');
182
- out(" promptimizer savings");
183
239
  out();
184
240
  }
185
241
 
@@ -243,10 +299,20 @@ function printMeta(result) {
243
299
  const saved = result.usage?.cost?.saved_usd;
244
300
  const bits = [meta.model || result.model, meta.tier].filter(Boolean);
245
301
  if (meta.routing_policy) bits.push(String(meta.routing_policy));
302
+ if (meta.provider_id || meta.host) bits.push(String(meta.provider_id || meta.host));
246
303
  if (saved != null) bits.push(`saved ${usd(saved)}`);
247
304
  if (meta.exact_cache_hit) bits.push(color(ANSI.green, "cache hit"));
248
- else if (meta.prefix_cache_hit) bits.push(color(ANSI.green, "prefix cache"));
305
+ else if (meta.semantic_cache_hit) {
306
+ const mode = meta.semantic_cache_mode === "full" ? "semantic full" : "semantic hybrid";
307
+ const sim =
308
+ meta.semantic_similarity != null ? ` ${Math.round(Number(meta.semantic_similarity) * 100)}%` : "";
309
+ bits.push(color(ANSI.green, `${mode}${sim}`));
310
+ } else if (meta.prefix_cache_hit) bits.push(color(ANSI.green, "prefix cache"));
249
311
  else if ("cache_hit" in meta) bits.push(color(ANSI.gray, "miss"));
312
+ if (meta.quality_gate) bits.push(`gate:${meta.quality_gate}`);
313
+ if (meta.quality_audit) {
314
+ bits.push(meta.quality_audit_pass === false ? color(ANSI.yellow, "audit fail") : "audit ok");
315
+ }
250
316
  if (meta.escalated) bits.push(meta.escalation_reason ? `escalated:${meta.escalation_reason}` : "escalated");
251
317
  if (meta.latency_ms != null) bits.push(`${Math.round(Number(meta.latency_ms))}ms`);
252
318
  out(color(ANSI.dim, ` ↳ ${bits.join(" · ")}`));
@@ -303,11 +369,14 @@ async function cmdConnect(flags, positional) {
303
369
  const provider = String(flags.provider || positional[0] || "").trim();
304
370
  const baseURL = flags["base-url"] || flags.baseUrl;
305
371
  if (!provider && !baseURL) {
306
- die("Usage: promptimizer connect <provider>\n promptimizer connect custom --base-url https://…");
372
+ die(
373
+ "Usage: promptimizer connect <provider> --key <vendor-key>\n promptimizer connect custom --base-url https://… --key …",
374
+ );
307
375
  }
308
376
 
309
377
  const mock = provider === "simulator" || provider === "mock";
310
378
  let vendorKey = flags.key || flags.k;
379
+ let label = flags.label;
311
380
  if (!mock && !baseURL && provider && provider !== "custom") {
312
381
  const catalog = await request("/v1/providers", { gatewayURL });
313
382
  const found = (catalog.data ?? []).find(
@@ -318,6 +387,7 @@ async function cmdConnect(flags, positional) {
318
387
  if (!vendorKey && found.id !== "ollama") {
319
388
  die(`Missing API key for ${found.label}. Pass --key or set ${found.env}.`);
320
389
  }
390
+ label = label || found.label;
321
391
  } else if (!mock && !vendorKey && provider !== "ollama") {
322
392
  die("Missing provider key. Pass --key.");
323
393
  }
@@ -331,6 +401,7 @@ async function cmdConnect(flags, positional) {
331
401
  ? { mode: "mock", label: "Promptimizer simulator" }
332
402
  : {
333
403
  mode: "byok",
404
+ label,
334
405
  provider: provider && provider !== "custom" ? provider : undefined,
335
406
  base_url: baseURL,
336
407
  api_key: vendorKey,
@@ -338,8 +409,57 @@ async function cmdConnect(flags, positional) {
338
409
  });
339
410
 
340
411
  writeConfig({ ...config, gatewayURL, apiKey, sessionId: session.session_id });
341
- out(`${color(ANSI.green, "✓")} ${session.label} ${session.base_url}`);
342
- out(` ${session.models.length} models · baseline ${session.baseline_model}`);
412
+ out();
413
+ printFleetSummary(session, { added: label || provider || session.label });
414
+ out();
415
+ return session;
416
+ }
417
+
418
+ async function cmdDisconnect(flags, positional) {
419
+ const config = readConfig();
420
+ const gatewayURL = gateway(flags, config);
421
+ const provider = String(flags.provider || flags.host || positional[0] || "").trim();
422
+ if (!provider) die("Usage: promptimizer disconnect <provider>\n promptimizer disconnect baseten");
423
+
424
+ const { apiKey, sessionId } = authFromConfig(flags, config);
425
+ const session = await request("/v1/providers/disconnect", {
426
+ method: "POST",
427
+ gatewayURL,
428
+ apiKey,
429
+ sessionId,
430
+ body: { provider },
431
+ });
432
+
433
+ writeConfig({ ...config, gatewayURL, apiKey, sessionId: session.session_id ?? sessionId });
434
+ out();
435
+ printFleetSummary(session, { removed: session.removed?.label || provider });
436
+ out();
437
+ return session;
438
+ }
439
+
440
+ async function cmdHosts(flags) {
441
+ const config = readConfig();
442
+ const session = await loadSession(flags, config);
443
+ const connections = session.connections ?? [];
444
+ out();
445
+ if (!connections.length) {
446
+ out(` ${session.label} (simulator)`);
447
+ out(` ${session.models?.length ?? 0} models`);
448
+ out();
449
+ return session;
450
+ }
451
+ const width = Math.max(8, ...connections.map((c) => String(c.label).length));
452
+ for (const c of connections) {
453
+ const count = (session.models ?? []).filter((m) => m.provider_id === c.id).length;
454
+ out(` ${color(ANSI.green, "✓")} ${c.label.padEnd(width + 2)}${count} models`);
455
+ out(` ${color(ANSI.dim, c.base_url)}`);
456
+ }
457
+ out();
458
+ out(` ${session.models?.length ?? 0} models total · baseline ${session.baseline_model ?? "—"}`);
459
+ out(` ${color(ANSI.dim, "Add: promptimizer connect <host> --key …")}`);
460
+ out(` ${color(ANSI.dim, "Remove: promptimizer disconnect <host>")}`);
461
+ out();
462
+ return session;
343
463
  }
344
464
 
345
465
  async function cmdChat(flags, positional) {
@@ -359,15 +479,30 @@ async function cmdModels(flags) {
359
479
  const config = readConfig();
360
480
  const gatewayURL = gateway(flags, config);
361
481
  const { apiKey, sessionId } = authFromConfig(flags, config);
362
- const data = await request("/v1/models", { gatewayURL, apiKey, sessionId });
482
+ const [data, session] = await Promise.all([
483
+ request("/v1/models", { gatewayURL, apiKey, sessionId }),
484
+ request("/v1/session", { gatewayURL, apiKey, sessionId }).catch(() => null),
485
+ ]);
363
486
  const models = data.data ?? [];
364
- const width = Math.max(8, ...models.map((model) => String(model.tier).length));
487
+ const labelById = new Map((session?.connections ?? []).map((c) => [c.id, c.label]));
488
+ const hostWidth = Math.max(
489
+ 4,
490
+ ...models.map((m) => String(m.provider_label || labelById.get(m.provider_id) || m.provider_id || "—").length),
491
+ );
492
+ const tierWidth = Math.max(8, ...models.map((m) => String(m.tier).length));
365
493
  out();
366
494
  for (const model of models) {
495
+ const host = model.provider_label || labelById.get(model.provider_id) || model.provider_id || "—";
367
496
  const mark = model.id === data.baseline_model ? color(ANSI.yellow, " baseline") : "";
368
- out(` ${color(ANSI.dim, String(model.tier).padEnd(width + 2))}${model.id}${mark}`);
497
+ out(
498
+ ` ${color(ANSI.dim, String(model.tier).padEnd(tierWidth + 2))}${color(ANSI.cyan, String(host).padEnd(hostWidth + 2))}${model.id}${mark}`,
499
+ );
369
500
  }
370
501
  out();
502
+ if (session?.connections?.length) {
503
+ out(color(ANSI.dim, ` ${session.connections.length} host(s) · ${models.length} models`));
504
+ out();
505
+ }
371
506
  }
372
507
 
373
508
  async function cmdSavings(flags) {
@@ -419,13 +554,16 @@ async function interactive(flags) {
419
554
 
420
555
  const slashHelp = () => {
421
556
  out();
422
- out(` ${color(ANSI.bold, "/help")} this list`);
423
- out(` ${color(ANSI.bold, "/models")} fleet + tiers`);
424
- out(` ${color(ANSI.bold, "/savings")} account ledger`);
425
- out(` ${color(ANSI.bold, "/session")} provider status`);
426
- out(` ${color(ANSI.bold, "/clear")} clear chat history`);
427
- out(` ${color(ANSI.bold, "/logout")} remove saved API key and exit`);
428
- out(` ${color(ANSI.bold, "/quit")} exit`);
557
+ out(` ${color(ANSI.bold, "/help")} this list`);
558
+ out(` ${color(ANSI.bold, "/hosts")} connected hosts`);
559
+ out(` ${color(ANSI.bold, "/models")} fleet + host + tier`);
560
+ out(` ${color(ANSI.bold, "/connect <host> --key …")} add a host (keeps others)`);
561
+ out(` ${color(ANSI.bold, "/disconnect <host>")} remove a host`);
562
+ out(` ${color(ANSI.bold, "/savings")} account ledger`);
563
+ out(` ${color(ANSI.bold, "/session")} provider status`);
564
+ out(` ${color(ANSI.bold, "/clear")} clear chat history`);
565
+ out(` ${color(ANSI.bold, "/logout")} remove saved API key and exit`);
566
+ out(` ${color(ANSI.bold, "/quit")} exit`);
429
567
  out();
430
568
  };
431
569
 
@@ -463,8 +601,15 @@ async function interactive(flags) {
463
601
  try {
464
602
  session = await loadSession(flags, readConfig());
465
603
  out();
466
- out(` ${session.label} · ${session.mode}`);
467
- out(` ${session.base_url}`);
604
+ out(` ${hostSummary(session)} · ${session.mode}`);
605
+ if (session.connections?.length) {
606
+ for (const c of session.connections) {
607
+ const count = session.models.filter((m) => m.provider_id === c.id).length;
608
+ out(` ${color(ANSI.green, "✓")} ${c.label} · ${count} models`);
609
+ }
610
+ } else {
611
+ out(` ${session.base_url}`);
612
+ }
468
613
  out(` ${session.models.length} models · baseline ${session.baseline_model}`);
469
614
  out();
470
615
  } catch (error) {
@@ -473,6 +618,15 @@ async function interactive(flags) {
473
618
  continue;
474
619
  }
475
620
 
621
+ if (trimmed === "/hosts" || trimmed === "/providers") {
622
+ try {
623
+ session = await cmdHosts(flags);
624
+ } catch (error) {
625
+ out(color(ANSI.yellow, ` ${error instanceof Error ? error.message : String(error)}`));
626
+ }
627
+ continue;
628
+ }
629
+
476
630
  if (trimmed === "/models") {
477
631
  try {
478
632
  await cmdModels(flags);
@@ -491,6 +645,29 @@ async function interactive(flags) {
491
645
  continue;
492
646
  }
493
647
 
648
+ if (
649
+ trimmed.startsWith("/connect") ||
650
+ trimmed.startsWith("/add ") ||
651
+ trimmed === "/add" ||
652
+ trimmed.startsWith("/disconnect") ||
653
+ trimmed.startsWith("/remove") ||
654
+ trimmed.startsWith("/rm ")
655
+ ) {
656
+ const body = trimmed.replace(/^\/(connect|add|disconnect|remove|rm)\s*/i, "");
657
+ const { flags: slashFlags, positional } = parseLineArgs(body);
658
+ const mergedFlags = { ...flags, ...slashFlags };
659
+ const isDisconnect = /^\/(disconnect|remove|rm)\b/i.test(trimmed);
660
+ try {
661
+ session = isDisconnect
662
+ ? await cmdDisconnect(mergedFlags, positional)
663
+ : await cmdConnect(mergedFlags, positional);
664
+ } catch (error) {
665
+ out(color(ANSI.yellow, ` ${error instanceof Error ? error.message : String(error)}`));
666
+ out();
667
+ }
668
+ continue;
669
+ }
670
+
494
671
  if (trimmed.startsWith("/")) {
495
672
  out(color(ANSI.dim, " Unknown command. Try /help"));
496
673
  continue;
@@ -550,7 +727,11 @@ async function main() {
550
727
  if (command === "login") return await cmdLogin(flags);
551
728
  if (command === "logout") return cmdLogout();
552
729
  if (command === "providers") return await cmdProviders(flags);
553
- if (command === "connect") return await cmdConnect(flags, rest);
730
+ if (command === "connect" || command === "add") return await cmdConnect(flags, rest);
731
+ if (command === "disconnect" || command === "remove" || command === "rm") {
732
+ return await cmdDisconnect(flags, rest);
733
+ }
734
+ if (command === "hosts") return await cmdHosts(flags);
554
735
  if (command === "chat") return await cmdChat(flags, rest);
555
736
  if (command === "models") return await cmdModels(flags);
556
737
  if (command === "savings") return await cmdSavings(flags);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptimizer-cli",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "Interactive Promptimizer CLI — Gemini-style REPL for quality-aware routing.",
5
5
  "type": "module",
6
6
  "bin": {