@tiens.nguyen/gu-cli 1.0.735 → 1.0.736

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.
@@ -233,6 +233,24 @@ export function modelListRequest(url, key = "") {
233
233
  return { url: modelsUrlFor(url), headers: keyed ? { Authorization: `Bearer ${k}` } : {} };
234
234
  }
235
235
 
236
+ /**
237
+ * Why a provider's model list could not be shown, and what to type instead (#266). Reported:
238
+ * "did not list its models without a key — type the id … what id?". The line now says WHY
239
+ * (no key saved / key refused / unreachable) and WHAT to type: the model's exact name as that
240
+ * provider calls it, with the current one as the example.
241
+ * reason: "no-key" | "refused" | "failed"
242
+ */
243
+ export function describeListFailure({ host = "", reason = "failed", status = 0, keyName = "", example = "" } = {}) {
244
+ const what = `type the model's name exactly as ${host} calls it${example ? ` (e.g. ${example})` : ""}`;
245
+ if (reason === "no-key") {
246
+ return [`no API key is saved for ${host}, so it cannot list its models —`, `${what}, or add a key with ctrl+k first.`];
247
+ }
248
+ if (reason === "refused") {
249
+ return [`${host} refused the key${keyName ? ` "${keyName}"` : ""}${status ? ` (HTTP ${status})` : ""} when listing its models —`, `${what}, or fix the key with ctrl+k.`];
250
+ }
251
+ return [`couldn't list ${host}'s models${status ? ` (HTTP ${status})` : " (no answer)"} —`, `${what}.`];
252
+ }
253
+
236
254
  /**
237
255
  * The hosted providers offered by name when adding one (#261), before "another URL". Claude has
238
256
  * no embedding models (Anthropic points to Voyage AI), so it is not offered for the embedder.
package/gu-repl.mjs CHANGED
@@ -5356,7 +5356,7 @@ async function chooseCodingModelFrom(ps, all, question, current) {
5356
5356
  let list = coders;
5357
5357
  let showingAll = filtered === 0;
5358
5358
  for (;;) {
5359
- const rows = [...list, "type a model id…"];
5359
+ const rows = [...list, "type a model name…"];
5360
5360
  // The row that lifts the filter, offered only while it is on and only when it hides something.
5361
5361
  if (!showingAll) rows.push(`show all ${all.length} models…`);
5362
5362
  // Opens on the CURRENT model where there is one — a long list that opens at the top tells the
@@ -5369,7 +5369,7 @@ async function chooseCodingModelFrom(ps, all, question, current) {
5369
5369
  const idx = await pickFromList(rows, at >= 0 ? at : 0);
5370
5370
  if (idx < 0) return null;
5371
5371
  if (idx === list.length) {
5372
- const typed = (await ask(" model id: ")).trim();
5372
+ const typed = (await ask(" model name: ")).trim();
5373
5373
  return typed || null;
5374
5374
  }
5375
5375
  if (idx === list.length + 1) {
@@ -5395,13 +5395,13 @@ async function chooseCodingModelFrom(ps, all, question, current) {
5395
5395
  * unlisted one may be exactly right.
5396
5396
  */
5397
5397
  async function chooseFromModelList(all, question, current = 0) {
5398
- const rows = [...all, "type a model id…"];
5398
+ const rows = [...all, "type a model name…"];
5399
5399
  const at = typeof current === "string" ? all.indexOf(current) : Number(current) || 0;
5400
5400
  console.log(dim(` ${question} (${PICK_HINT})`));
5401
5401
  const idx = await pickFromList(rows, at >= 0 ? at : 0);
5402
5402
  if (idx < 0) return null;
5403
5403
  if (idx === all.length) {
5404
- const typed = (await ask(" model id: ")).trim();
5404
+ const typed = (await ask(" model name: ")).trim();
5405
5405
  return typed || null;
5406
5406
  }
5407
5407
  return all[idx];
@@ -5459,15 +5459,22 @@ async function changeProviderModel(ps, rows, p) {
5459
5459
  const md = await import("./model-doctor.mjs");
5460
5460
  const { proxyFetch } = await import("./proxy-dispatcher.mjs");
5461
5461
 
5462
- // PROBED WITHOUT A KEY, DELIBERATELY. The terminal does not hold this provider's secret, and
5463
- // the one key it could fetch is the ACCOUNT DEFAULT — which may belong to a different provider
5464
- // entirely. Sending it here to be helpful would be the exact leak this task spent a phase
5465
- // preventing. Many servers (Ollama, vLLM, LM Studio) list models unauthenticated; a hosted one
5466
- // will refuse, and then the id is typed, which always works.
5462
+ // LISTED WITH THIS PROVIDER'S OWN KEY (#266). This used to probe with NO key, on purpose: the
5463
+ // only key the terminal could fetch was the ACCOUNT DEFAULT, which may belong to another provider.
5464
+ // Since #245 a key belongs to its server, so the key used is the one bound to THIS row — the same
5465
+ // check addServer makes — and never another provider's. Keyed providers (Claude, OpenAI, Kimi,
5466
+ // ollama.com) can finally list their models here instead of asking for an id.
5467
+ const sk = await import("./server-keys.mjs");
5468
+ const own = await withPendingStatus(`reading the key for ${p.host}`, async () => {
5469
+ const keys = await fetchServerKeys(p.id);
5470
+ const inUse = sk.keyInUse(keys ?? [], p.id, sessionCodingKeyId);
5471
+ if (!inUse || inUse.backendId !== p.id) return { key: "", name: "" };
5472
+ const r = await fetchCodingKey(p.id);
5473
+ return r.keyId && r.keyId !== inUse.id ? { key: "", name: "" } : { key: r.key, name: String(inUse.name ?? "") };
5474
+ });
5467
5475
  const probe = await withPendingStatus(`asking ${p.host} what it has`, async () => {
5468
5476
  try {
5469
- // Keyless, as above — but in the list's own dialect (the Claude API's headers, #261).
5470
- const lr = ps.modelListRequest(p.url);
5477
+ const lr = ps.modelListRequest(p.url, own.key); // the list's own dialect too (Claude's headers, #261)
5471
5478
  const res = await proxyFetch(lr.url, { headers: lr.headers, signal: AbortSignal.timeout(10_000) });
5472
5479
  if (!res.ok) return { status: res.status };
5473
5480
  return { body: await res.json().catch(() => null) };
@@ -5487,7 +5494,11 @@ async function changeProviderModel(ps, rows, p) {
5487
5494
  const rn = ps.retiredNote(parsed.retired, p.host);
5488
5495
  if (rn) console.log(GUTTER + dim(rn));
5489
5496
  } else {
5490
- console.log(GUTTER + dim(`${p.host} did not list its models without a key — type the id.`));
5497
+ const refused = probe.status === 401 || probe.status === 403;
5498
+ const reason = refused ? (own.key ? "refused" : "no-key") : "failed";
5499
+ for (const l of ps.describeListFailure({ host: p.host, reason, status: probe.status, keyName: own.name, example: p.model })) {
5500
+ console.log(GUTTER + dim(l));
5501
+ }
5491
5502
  }
5492
5503
 
5493
5504
  let model = "";
@@ -5499,7 +5510,7 @@ async function changeProviderModel(ps, rows, p) {
5499
5510
  }
5500
5511
  } else {
5501
5512
  console.log(GUTTER + dim(`Enter keeps ${p.model || "it unset"}`));
5502
- model = (await ask(" model id: ")).trim();
5513
+ model = (await ask(" model name: ")).trim();
5503
5514
  }
5504
5515
  if (!model) {
5505
5516
  console.log(GUTTER + dim("(nothing changed)\n"));
@@ -5837,7 +5848,7 @@ async function addServer({ role = "coding", dialect = "openai", url: presetUrl =
5837
5848
  return;
5838
5849
  }
5839
5850
  } else {
5840
- model = String((await ask(" model id: ")) ?? "").trim();
5851
+ model = String((await ask(" model name: ")) ?? "").trim();
5841
5852
  }
5842
5853
  if (!model) {
5843
5854
  console.log(GUTTER + dim("(cancelled — nothing was saved)\n"));
@@ -6125,10 +6136,12 @@ async function chooseModel() {
6125
6136
  // Task #125: remember the chosen backend's coding URL so the GLOBAL output-token total is
6126
6137
  // keyed to it. Default pick → "" so activeCodingUrl() falls back to defaultCodingUrl.
6127
6138
  sessionCodingUrl = chosen.id === def ? "" : (chosen.url ?? "");
6139
+ // The server goes WITH the model, before any await (#263): set after the save below, the bar
6140
+ // could repaint in between and show the NEW model under the OLD server for a moment.
6141
+ activeCoderWhere = coderWhere(chosen.kind, chosen.url);
6128
6142
  // WRITTEN NOW, not at the end of the next turn: `/model` then `/exit` is how the choice is
6129
6143
  // made and then tested, and that path never reaches saveSession.
6130
6144
  await saveSessionCoding(resolve(process.cwd()));
6131
- activeCoderWhere = coderWhere(chosen.kind, chosen.url);
6132
6145
  const where = showTag && !activeCoderWhere ? ` (${tags[idx]})` : "";
6133
6146
  console.log(
6134
6147
  accent(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.735",
3
+ "version": "1.0.736",
4
4
  "description": "Gu CLI — the gu terminal plus the local worker that runs agent / OCR / PDF / embedding jobs on your Mac (Ollama / MLX / OpenAI-compatible).",
5
5
  "type": "module",
6
6
  "license": "MIT",