@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.
- package/agent-model-command.mjs +67 -0
- package/client-expert-api.mjs +34 -0
- package/gu-repl.mjs +41 -15
- package/package.json +1 -1
package/agent-model-command.mjs
CHANGED
|
@@ -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
|
+
}
|
package/client-expert-api.mjs
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
4811
|
+
const curCode = am.coderServerDefault(settings);
|
|
4801
4812
|
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
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
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
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.
|
|
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",
|