@tiens.nguyen/gu-cli 1.0.699 → 1.0.700

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.
@@ -349,3 +349,70 @@ export function agentRecoveryOptions({ keyProblem = false, host = "" } = {}) {
349
349
  { action: "provider", label: "add an OpenAI-compatible provider for the agent…" },
350
350
  ];
351
351
  }
352
+
353
+ // ---------------------------------------------------------------------------------------------
354
+ // #43 — /model's "change the Ollama server" flow, when the agent is NOT on an Ollama.
355
+ // ---------------------------------------------------------------------------------------------
356
+
357
+ /**
358
+ * Is this machine's agent model on an OpenAI-compatible PROVIDER rather than an Ollama?
359
+ *
360
+ * Reported (#43): client mode, /model → change the Ollama server offered the agent's URL —
361
+ * https://api.openai.com — as the "agent + embedding server", probed it for Ollama's /api/tags, got
362
+ * 404, said "check that Ollama is listening", and asked again, with no way forward but typing "no".
363
+ * A provider is known two ways: a provider row for that URL says so, or the URL is one of the hosted
364
+ * APIs everyone knows is not an Ollama.
365
+ */
366
+ const KNOWN_PROVIDER_HOSTS = [
367
+ "api.openai.com",
368
+ "openrouter.ai",
369
+ "api.together.xyz",
370
+ "api.moonshot.ai",
371
+ "api.groq.com",
372
+ "api.deepseek.com",
373
+ "api.mistral.ai",
374
+ "generativelanguage.googleapis.com",
375
+ ];
376
+
377
+ export function agentIsOnProvider(settings) {
378
+ const source = agentListSource(settings);
379
+ if (!source.url) return false;
380
+ if (source.dialect === "openai") return true;
381
+ let host = "";
382
+ try {
383
+ host = new URL(String(settings?.agentModelUrl ?? "")).hostname.toLowerCase();
384
+ } catch {
385
+ return false;
386
+ }
387
+ return KNOWN_PROVIDER_HOSTS.some((h) => host === h || host.endsWith(`.${h}`));
388
+ }
389
+
390
+ /**
391
+ * The address to offer for the CODING server: the coder's own URL when it is not a known provider,
392
+ * else this machine's Ollama. Never the agent's provider URL — that is what put api.openai.com in an
393
+ * Ollama prompt.
394
+ */
395
+ export function coderServerDefault(settings) {
396
+ const coder = String(settings?.agentCodingModelUrl ?? "").trim();
397
+ if (coder && !agentIsOnProvider({ ...settings, agentModelUrl: coder })) return coder;
398
+ const agent = String(settings?.agentModelUrl ?? "").trim();
399
+ if (agent && !agentIsOnProvider(settings)) return agent;
400
+ return "http://127.0.0.1:11434";
401
+ }
402
+
403
+ /**
404
+ * A server that ANSWERED but has no /api/tags is not an Ollama — an OpenAI-compatible API answers 404
405
+ * there. That is a different fix from "not listening", and saying the wrong one sends people to check
406
+ * a server that is working fine.
407
+ */
408
+ export function describeNotOllama(host, role) {
409
+ return role === "agent"
410
+ ? [
411
+ `${host} answers, but it is not an Ollama server (no /api/tags).`,
412
+ "an OpenAI-compatible agent is changed with /agent-model — or type 'no' to skip this role.",
413
+ ]
414
+ : [
415
+ `${host} answers, but it is not an Ollama server (no /api/tags).`,
416
+ "for an OpenAI-compatible coder use /model → add or change an OpenAI-compatible provider — or type 'no' to skip.",
417
+ ];
418
+ }
@@ -734,3 +734,37 @@ export async function seedSettings({
734
734
  await client.close().catch(() => {});
735
735
  }
736
736
  }
737
+
738
+ /**
739
+ * THE NEW OLLAMA SERVER'S ROWS, MERGED INTO WHAT IS ALREADY THERE — never replacing it (#43).
740
+ *
741
+ * Found while fixing #43: choosing a coding server in /model → "change the Ollama server" saved
742
+ * codingBackendsForClientExpert(...) AS the whole list — one Ollama row and the Kimi row — deleting
743
+ * every provider the user had added: an api.openai.com row the AGENT's key is found through, an
744
+ * ollama.com cloud row. Their keys, bound to those row ids, were orphaned.
745
+ *
746
+ * Replaced: the local/LAN Ollama row(s) — that is what "the Ollama server this machine uses" means —
747
+ * with the new one, which keeps the first replaced row's id so anything bound to it still resolves.
748
+ * Kept, ids and all: every OpenAI-compatible row, and any Ollama CLOUD row (a paid provider, not
749
+ * "this machine's server"). The template's default provider row is added only when no row already
750
+ * has its URL.
751
+ */
752
+ export function mergeOllamaServerBackends(existing, fresh, { isCloud } = {}) {
753
+ const cloud = typeof isCloud === "function" ? isCloud : () => false;
754
+ const norm = (u) => String(u ?? "").trim().replace(/\/+$/, "").toLowerCase();
755
+ const rows = Array.isArray(existing) ? existing.filter(Boolean) : [];
756
+ const replaced = rows.filter((b) => b.kind === "ollama" && !cloud(b.url));
757
+ const kept = rows.filter((b) => !(b.kind === "ollama" && !cloud(b.url)));
758
+ const out = [];
759
+ for (const f of Array.isArray(fresh) ? fresh : []) {
760
+ if (f.kind === "ollama") {
761
+ const id = replaced.find((b) => b.id)?.id;
762
+ out.push(id ? { ...f, id } : f);
763
+ }
764
+ }
765
+ out.push(...kept);
766
+ for (const f of Array.isArray(fresh) ? fresh : []) {
767
+ if (f.kind !== "ollama" && !out.some((b) => norm(b.url) === norm(f.url))) out.push(f);
768
+ }
769
+ return out;
770
+ }
package/gu-repl.mjs CHANGED
@@ -4721,12 +4721,13 @@ async function reconfigureOllamaServer() {
4721
4721
  const res = await withPendingStatus(`loading models from ${n.host}`, () =>
4722
4722
  proxyFetch(os.tagsUrlFor(n), { signal: AbortSignal.timeout(8000) })
4723
4723
  );
4724
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
4724
+ // The status is kept: a 404 here means "answered, but not an Ollama" (#43), not "unreachable".
4725
+ if (!res.ok) throw Object.assign(new Error(`HTTP ${res.status}`), { httpStatus: res.status });
4725
4726
  return am.parseOllamaTags(await res.json().catch(() => null));
4726
4727
  };
4727
4728
 
4728
4729
  /** Ask for one server and load it. Returns {normalized, models} or null (declined/failed). */
4729
- const askServer = async (label, dflt, { require = false, short = "server" } = {}) => {
4730
+ const askServer = async (label, dflt, { require = false, short = "server", role = "coder" } = {}) => {
4730
4731
  for (let attempt = 0; attempt < 3; attempt++) {
4731
4732
  // The address goes ABOVE, in full — it is long, it is the thing Enter accepts, and the
4732
4733
  // frozen row is too narrow to hold it without truncating exactly that. The row itself
@@ -4766,6 +4767,12 @@ async function reconfigureOllamaServer() {
4766
4767
  }
4767
4768
  }
4768
4769
  if (!n) {
4770
+ if (lastErr?.httpStatus === 404) {
4771
+ // IT ANSWERED — it is simply not an Ollama (#43). "Couldn't reach … is Ollama listening"
4772
+ // sent people to debug a server that was working.
4773
+ for (const l of am.describeNotOllama(candidates[0].host, role)) console.log(yellow(` ${l}`));
4774
+ continue;
4775
+ }
4769
4776
  // One address typed, one failure reported — naming every candidate would be two errors
4770
4777
  // for one mistake, and neither would be the address they gave.
4771
4778
  console.log(red(` couldn't reach ${candidates[0].host}: ${os.networkReason(lastErr)}`));
@@ -4795,18 +4802,31 @@ async function reconfigureOllamaServer() {
4795
4802
  return null;
4796
4803
  };
4797
4804
 
4805
+ // AN AGENT ON A PROVIDER IS NOT ASKED ABOUT HERE (#43). This flow is for Ollama boxes; offering
4806
+ // https://api.openai.com as the "agent + embedding server" could only fail its /api/tags probe and
4807
+ // ask again. Keep it, say where it is changed, and go on to the coder.
4808
+ const agentOnProvider = am.agentIsOnProvider(settings);
4798
4809
  const curAgent =
4799
4810
  String(settings.agentModelUrl ?? "").trim() || "https://ollama1.gomarsic.cc/v1";
4800
- const curCode = String(settings.agentCodingModelUrl ?? "").trim() || curAgent;
4811
+ const curCode = am.coderServerDefault(settings);
4801
4812
 
4802
- console.log(dim(" TWO servers may be used: the agent + embedder on one, the coder on another."));
4803
- console.log(dim(" Enter keeps the current address; type 'no' to skip a role."));
4804
- const agent = await askServer("Agent + embedding server", curAgent, {
4805
- require: true,
4806
- short: "agent + embed server",
4807
- });
4813
+ let agent = null;
4814
+ if (agentOnProvider) {
4815
+ const host = hostOf(String(settings.agentModelUrl ?? ""));
4816
+ console.log(dim(` The agent model is on ${host} (OpenAI-compatible) — kept as it is; change it with /agent-model.`));
4817
+ console.log(dim(" Enter keeps the current address; type 'no' to skip."));
4818
+ } else {
4819
+ console.log(dim(" TWO servers may be used: the agent + embedder on one, the coder on another."));
4820
+ console.log(dim(" Enter keeps the current address; type 'no' to skip a role."));
4821
+ agent = await askServer("Agent + embedding server", curAgent, {
4822
+ require: true,
4823
+ short: "agent + embed server",
4824
+ role: "agent",
4825
+ });
4826
+ }
4808
4827
  const coder = await askServer("Coding-model server", agent?.normalized?.url ?? curCode, {
4809
4828
  short: "coding server",
4829
+ role: "coder",
4810
4830
  });
4811
4831
 
4812
4832
  if (!agent && !coder) {
@@ -4912,12 +4932,18 @@ async function reconfigureOllamaServer() {
4912
4932
  agentCodingModelName: code,
4913
4933
  agentCodingModelKind: "ollama",
4914
4934
  agentCodingDefaultKind: "ollama",
4915
- agentCodingBackends: (await import("./client-expert-api.mjs")).codingBackendsForClientExpert({
4916
- url: coder.normalized.url,
4917
- chat: agent ? chat : code,
4918
- code,
4919
- altCoders: extras,
4920
- }),
4935
+ // MERGED, not replaced (#43): the providers already configured — and the keys bound to them —
4936
+ // survive choosing a new Ollama server.
4937
+ agentCodingBackends: (await import("./client-expert-api.mjs")).mergeOllamaServerBackends(
4938
+ (await import("./coding-provider-setup.mjs")).existingBackendRows(settings),
4939
+ (await import("./client-expert-api.mjs")).codingBackendsForClientExpert({
4940
+ url: coder.normalized.url,
4941
+ chat: agent ? chat : code,
4942
+ code,
4943
+ altCoders: extras,
4944
+ }),
4945
+ { isCloud: (await import("./coding-key-prompt.mjs")).isOllamaCloudUrl }
4946
+ ),
4921
4947
  });
4922
4948
  }
4923
4949
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.699",
3
+ "version": "1.0.700",
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",