promptimizer-cli 0.1.23 → 0.1.25

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,6 +299,7 @@ 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
305
  else if (meta.prefix_cache_hit) bits.push(color(ANSI.green, "prefix cache"));
@@ -303,11 +360,14 @@ async function cmdConnect(flags, positional) {
303
360
  const provider = String(flags.provider || positional[0] || "").trim();
304
361
  const baseURL = flags["base-url"] || flags.baseUrl;
305
362
  if (!provider && !baseURL) {
306
- die("Usage: promptimizer connect <provider>\n promptimizer connect custom --base-url https://…");
363
+ die(
364
+ "Usage: promptimizer connect <provider> --key <vendor-key>\n promptimizer connect custom --base-url https://… --key …",
365
+ );
307
366
  }
308
367
 
309
368
  const mock = provider === "simulator" || provider === "mock";
310
369
  let vendorKey = flags.key || flags.k;
370
+ let label = flags.label;
311
371
  if (!mock && !baseURL && provider && provider !== "custom") {
312
372
  const catalog = await request("/v1/providers", { gatewayURL });
313
373
  const found = (catalog.data ?? []).find(
@@ -318,6 +378,7 @@ async function cmdConnect(flags, positional) {
318
378
  if (!vendorKey && found.id !== "ollama") {
319
379
  die(`Missing API key for ${found.label}. Pass --key or set ${found.env}.`);
320
380
  }
381
+ label = label || found.label;
321
382
  } else if (!mock && !vendorKey && provider !== "ollama") {
322
383
  die("Missing provider key. Pass --key.");
323
384
  }
@@ -331,6 +392,7 @@ async function cmdConnect(flags, positional) {
331
392
  ? { mode: "mock", label: "Promptimizer simulator" }
332
393
  : {
333
394
  mode: "byok",
395
+ label,
334
396
  provider: provider && provider !== "custom" ? provider : undefined,
335
397
  base_url: baseURL,
336
398
  api_key: vendorKey,
@@ -338,8 +400,57 @@ async function cmdConnect(flags, positional) {
338
400
  });
339
401
 
340
402
  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}`);
403
+ out();
404
+ printFleetSummary(session, { added: label || provider || session.label });
405
+ out();
406
+ return session;
407
+ }
408
+
409
+ async function cmdDisconnect(flags, positional) {
410
+ const config = readConfig();
411
+ const gatewayURL = gateway(flags, config);
412
+ const provider = String(flags.provider || flags.host || positional[0] || "").trim();
413
+ if (!provider) die("Usage: promptimizer disconnect <provider>\n promptimizer disconnect baseten");
414
+
415
+ const { apiKey, sessionId } = authFromConfig(flags, config);
416
+ const session = await request("/v1/providers/disconnect", {
417
+ method: "POST",
418
+ gatewayURL,
419
+ apiKey,
420
+ sessionId,
421
+ body: { provider },
422
+ });
423
+
424
+ writeConfig({ ...config, gatewayURL, apiKey, sessionId: session.session_id ?? sessionId });
425
+ out();
426
+ printFleetSummary(session, { removed: session.removed?.label || provider });
427
+ out();
428
+ return session;
429
+ }
430
+
431
+ async function cmdHosts(flags) {
432
+ const config = readConfig();
433
+ const session = await loadSession(flags, config);
434
+ const connections = session.connections ?? [];
435
+ out();
436
+ if (!connections.length) {
437
+ out(` ${session.label} (simulator)`);
438
+ out(` ${session.models?.length ?? 0} models`);
439
+ out();
440
+ return session;
441
+ }
442
+ const width = Math.max(8, ...connections.map((c) => String(c.label).length));
443
+ for (const c of connections) {
444
+ const count = (session.models ?? []).filter((m) => m.provider_id === c.id).length;
445
+ out(` ${color(ANSI.green, "✓")} ${c.label.padEnd(width + 2)}${count} models`);
446
+ out(` ${color(ANSI.dim, c.base_url)}`);
447
+ }
448
+ out();
449
+ out(` ${session.models?.length ?? 0} models total · baseline ${session.baseline_model ?? "—"}`);
450
+ out(` ${color(ANSI.dim, "Add: promptimizer connect <host> --key …")}`);
451
+ out(` ${color(ANSI.dim, "Remove: promptimizer disconnect <host>")}`);
452
+ out();
453
+ return session;
343
454
  }
344
455
 
345
456
  async function cmdChat(flags, positional) {
@@ -359,15 +470,30 @@ async function cmdModels(flags) {
359
470
  const config = readConfig();
360
471
  const gatewayURL = gateway(flags, config);
361
472
  const { apiKey, sessionId } = authFromConfig(flags, config);
362
- const data = await request("/v1/models", { gatewayURL, apiKey, sessionId });
473
+ const [data, session] = await Promise.all([
474
+ request("/v1/models", { gatewayURL, apiKey, sessionId }),
475
+ request("/v1/session", { gatewayURL, apiKey, sessionId }).catch(() => null),
476
+ ]);
363
477
  const models = data.data ?? [];
364
- const width = Math.max(8, ...models.map((model) => String(model.tier).length));
478
+ const labelById = new Map((session?.connections ?? []).map((c) => [c.id, c.label]));
479
+ const hostWidth = Math.max(
480
+ 4,
481
+ ...models.map((m) => String(m.provider_label || labelById.get(m.provider_id) || m.provider_id || "—").length),
482
+ );
483
+ const tierWidth = Math.max(8, ...models.map((m) => String(m.tier).length));
365
484
  out();
366
485
  for (const model of models) {
486
+ const host = model.provider_label || labelById.get(model.provider_id) || model.provider_id || "—";
367
487
  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}`);
488
+ out(
489
+ ` ${color(ANSI.dim, String(model.tier).padEnd(tierWidth + 2))}${color(ANSI.cyan, String(host).padEnd(hostWidth + 2))}${model.id}${mark}`,
490
+ );
369
491
  }
370
492
  out();
493
+ if (session?.connections?.length) {
494
+ out(color(ANSI.dim, ` ${session.connections.length} host(s) · ${models.length} models`));
495
+ out();
496
+ }
371
497
  }
372
498
 
373
499
  async function cmdSavings(flags) {
@@ -419,13 +545,16 @@ async function interactive(flags) {
419
545
 
420
546
  const slashHelp = () => {
421
547
  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`);
548
+ out(` ${color(ANSI.bold, "/help")} this list`);
549
+ out(` ${color(ANSI.bold, "/hosts")} connected hosts`);
550
+ out(` ${color(ANSI.bold, "/models")} fleet + host + tier`);
551
+ out(` ${color(ANSI.bold, "/connect <host> --key …")} add a host (keeps others)`);
552
+ out(` ${color(ANSI.bold, "/disconnect <host>")} remove a host`);
553
+ out(` ${color(ANSI.bold, "/savings")} account ledger`);
554
+ out(` ${color(ANSI.bold, "/session")} provider status`);
555
+ out(` ${color(ANSI.bold, "/clear")} clear chat history`);
556
+ out(` ${color(ANSI.bold, "/logout")} remove saved API key and exit`);
557
+ out(` ${color(ANSI.bold, "/quit")} exit`);
429
558
  out();
430
559
  };
431
560
 
@@ -463,8 +592,15 @@ async function interactive(flags) {
463
592
  try {
464
593
  session = await loadSession(flags, readConfig());
465
594
  out();
466
- out(` ${session.label} · ${session.mode}`);
467
- out(` ${session.base_url}`);
595
+ out(` ${hostSummary(session)} · ${session.mode}`);
596
+ if (session.connections?.length) {
597
+ for (const c of session.connections) {
598
+ const count = session.models.filter((m) => m.provider_id === c.id).length;
599
+ out(` ${color(ANSI.green, "✓")} ${c.label} · ${count} models`);
600
+ }
601
+ } else {
602
+ out(` ${session.base_url}`);
603
+ }
468
604
  out(` ${session.models.length} models · baseline ${session.baseline_model}`);
469
605
  out();
470
606
  } catch (error) {
@@ -473,6 +609,15 @@ async function interactive(flags) {
473
609
  continue;
474
610
  }
475
611
 
612
+ if (trimmed === "/hosts" || trimmed === "/providers") {
613
+ try {
614
+ session = await cmdHosts(flags);
615
+ } catch (error) {
616
+ out(color(ANSI.yellow, ` ${error instanceof Error ? error.message : String(error)}`));
617
+ }
618
+ continue;
619
+ }
620
+
476
621
  if (trimmed === "/models") {
477
622
  try {
478
623
  await cmdModels(flags);
@@ -491,6 +636,29 @@ async function interactive(flags) {
491
636
  continue;
492
637
  }
493
638
 
639
+ if (
640
+ trimmed.startsWith("/connect") ||
641
+ trimmed.startsWith("/add ") ||
642
+ trimmed === "/add" ||
643
+ trimmed.startsWith("/disconnect") ||
644
+ trimmed.startsWith("/remove") ||
645
+ trimmed.startsWith("/rm ")
646
+ ) {
647
+ const body = trimmed.replace(/^\/(connect|add|disconnect|remove|rm)\s*/i, "");
648
+ const { flags: slashFlags, positional } = parseLineArgs(body);
649
+ const mergedFlags = { ...flags, ...slashFlags };
650
+ const isDisconnect = /^\/(disconnect|remove|rm)\b/i.test(trimmed);
651
+ try {
652
+ session = isDisconnect
653
+ ? await cmdDisconnect(mergedFlags, positional)
654
+ : await cmdConnect(mergedFlags, positional);
655
+ } catch (error) {
656
+ out(color(ANSI.yellow, ` ${error instanceof Error ? error.message : String(error)}`));
657
+ out();
658
+ }
659
+ continue;
660
+ }
661
+
494
662
  if (trimmed.startsWith("/")) {
495
663
  out(color(ANSI.dim, " Unknown command. Try /help"));
496
664
  continue;
@@ -550,7 +718,11 @@ async function main() {
550
718
  if (command === "login") return await cmdLogin(flags);
551
719
  if (command === "logout") return cmdLogout();
552
720
  if (command === "providers") return await cmdProviders(flags);
553
- if (command === "connect") return await cmdConnect(flags, rest);
721
+ if (command === "connect" || command === "add") return await cmdConnect(flags, rest);
722
+ if (command === "disconnect" || command === "remove" || command === "rm") {
723
+ return await cmdDisconnect(flags, rest);
724
+ }
725
+ if (command === "hosts") return await cmdHosts(flags);
554
726
  if (command === "chat") return await cmdChat(flags, rest);
555
727
  if (command === "models") return await cmdModels(flags);
556
728
  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.23",
3
+ "version": "0.1.25",
4
4
  "description": "Interactive Promptimizer CLI — Gemini-style REPL for quality-aware routing.",
5
5
  "type": "module",
6
6
  "bin": {