@tiens.nguyen/gu-cli 1.0.734 → 1.0.735

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.
@@ -95,7 +95,46 @@ export function ollamaWhere(kind, url) {
95
95
  return (local ? "127.0.0.1" : host) + port;
96
96
  }
97
97
 
98
- /** The coder's name as shown: "ollama.com/kimi-k3" for an Ollama coder, the bare name otherwise. */
98
+ /**
99
+ * WHICH SERVER A CODER RUNS ON — for EVERY coder, not only Ollama ones (#263). Reported: "you
100
+ * should show the server before the model, correctly — later I can have another model on a
101
+ * different server". A bare "claude-opus-5" cannot say which provider (or which of two gateways)
102
+ * it goes to; "api.anthropic.com/claude-opus-5" can.
103
+ * · Ollama keeps ollamaWhere's rules (ollama.com, 127.0.0.1, a non-default port).
104
+ * · Anything else: its host, this machine as 127.0.0.1, and the port when it is not the
105
+ * scheme's default — two local servers on two ports are two servers.
106
+ */
107
+ export function coderWhere(kind, url) {
108
+ if (kind === "ollama") return ollamaWhere(kind, url);
109
+ let u;
110
+ try {
111
+ u = new URL(String(url ?? ""));
112
+ } catch {
113
+ return "";
114
+ }
115
+ const host = u.hostname.toLowerCase().replace(/\.$/, "").replace(/^\[|\]$/g, "");
116
+ if (!host) return "";
117
+ const local = host === "localhost" || host === "127.0.0.1" || host === "::1";
118
+ const port = u.port ? `:${u.port}` : ""; // URL() already drops :443 on https and :80 on http
119
+ return (local ? "127.0.0.1" : host) + port;
120
+ }
121
+
122
+ /**
123
+ * WHERE THE CODER IN USE RUNS — the host the bar and banner put in front of its name (#263).
124
+ * The folder's pick (a choice id, or a bare model name), else the account default. No list (an
125
+ * older API) → "", never a guess from raw payload fields (key-choice-regression.test.mjs).
126
+ * One rule for startup and for every path that changes the coder: computing it in only one of them
127
+ * is what left "ollama.com/" in front of a newly added Claude model.
128
+ */
129
+ export function whereOfActiveCoder(choices, sessionModel) {
130
+ const list = Array.isArray(choices) ? choices : [];
131
+ const pick = String(sessionModel ?? "");
132
+ const active =
133
+ (pick && list.find((c) => c.id === pick || c.model === pick)) || list.find((c) => c.isDefault) || null;
134
+ return active ? coderWhere(active.kind, active.url) : "";
135
+ }
136
+
137
+ /** The coder's name as shown: "<server>/<model>" ("api.anthropic.com/claude-opus-5"), bare when no server is known. */
99
138
  export function coderDisplayName(name, where) {
100
139
  const n = String(name ?? "").trim();
101
140
  return where && n ? `${where}/${n}` : n;
@@ -1152,6 +1152,18 @@ def _anthropic_usage(u) -> "types.SimpleNamespace":
1152
1152
  cache_read_input_tokens=cr, cache_creation_input_tokens=cw)
1153
1153
 
1154
1154
 
1155
+ def _log_anthropic_usage(u) -> None:
1156
+ """One content-free line per Claude request with Anthropic's OWN cache figures (task #264).
1157
+ Without it the log could not say whether caching worked — the reported question — although
1158
+ every response carried the answer."""
1159
+ g = (lambda n: int(_usage_field(u, n) or 0))
1160
+ read, write, fresh = g("cache_read_input_tokens"), g("cache_creation_input_tokens"), g("input_tokens")
1161
+ total = read + write + fresh
1162
+ pct = f"{100 * read // total}%" if total else "—"
1163
+ _log(f"claude usage: prompt {total} = cache read {read} + cache write {write} + uncached {fresh} "
1164
+ f"({pct} read from cache) · output {g('output_tokens')}")
1165
+
1166
+
1155
1167
  class _AnthropicChatAdapter:
1156
1168
  """client.chat.completions.create() backed by Anthropic's native Messages API (task #262)."""
1157
1169
 
@@ -1176,6 +1188,7 @@ class _AnthropicChatAdapter:
1176
1188
  if kw.get("stream"):
1177
1189
  return self._stream(req)
1178
1190
  msg = self._sdk.messages.create(**req)
1191
+ _log_anthropic_usage(getattr(msg, "usage", None))
1179
1192
  import types
1180
1193
  text = "".join(getattr(b, "text", "") or "" for b in (msg.content or []) if getattr(b, "type", "") == "text")
1181
1194
  return types.SimpleNamespace(
@@ -1217,6 +1230,7 @@ class _AnthropicChatAdapter:
1217
1230
  usage["output_tokens"] = int(out)
1218
1231
  if stop_reason == "refusal":
1219
1232
  _log("claude: the model declined this request (stop_reason=refusal)")
1233
+ _log_anthropic_usage(usage)
1220
1234
  yield chunk(content="", finish=_ANTHROPIC_FINISH.get(stop_reason, "stop"))
1221
1235
  yield chunk(usage=_anthropic_usage(usage))
1222
1236
 
@@ -8243,12 +8257,18 @@ def run_agent_chat(cfg):
8243
8257
  # Variable text — the deploy target and the current date/time — lives in the
8244
8258
  # variable tail next to the task instead.
8245
8259
  tool_hint = (
8260
+ # ONE batching rule, the system prompt's (task #265): this line used to say "ONE code block
8261
+ # that calls a SINGLE tool", contradicting the scaffold's BATCH rule from nearer the task —
8262
+ # measured as a 102-step turn in which every file read was its own full round trip.
8246
8263
  f"Solve the TASK step by step (up to {max_steps} steps). At EACH step write a "
8247
- "brief Thought, then ONE code block that calls a SINGLE tool. You will SEE that "
8248
- "tool's result (Observation) before the next step — use it to decide what to do "
8249
- "next (e.g. web_search to find a URL, THEN fetch_url to read it). When you have "
8250
- "enough to answer, call final_answer(<your answer>) in a code block. Prefer "
8251
- "FEWER steps, and never repeat a tool call that already succeeded.\n\n"
8264
+ "brief Thought, then ONE code block. BATCH INDEPENDENT LOOKUPS into that block — "
8265
+ "several reads, greps or listings whose arguments you already know, printing each "
8266
+ "result under a header (up to about 8). Make ONE CHANGE per block (an edit, a file "
8267
+ "write or a command): if a line fails, the lines after it never run. Take a separate "
8268
+ "step only when a call depends on a result you have not seen yet (e.g. web_search to "
8269
+ "find a URL, THEN fetch_url to read it). You SEE the Observation before the next step. "
8270
+ "When you have enough to answer, call final_answer(<your answer>) in a code block. "
8271
+ "Prefer FEWER steps, and never repeat a tool call that already succeeded.\n\n"
8252
8272
  "{{TOOL_LIST}}\n" # filled in below, once from the REAL registered tool objects
8253
8273
  + _rag_tool_block + _ws_tool_block + _auto_test_block +
8254
8274
  "\n"
package/gu-repl.mjs CHANGED
@@ -120,7 +120,7 @@ import {
120
120
  } from "./terminal-layout.mjs";
121
121
  import { completedFlush } from "./completed-flush.mjs";
122
122
  import { codingBannerLabel, isOllamaCloudUrl, SECRET_KEY_HINT } from "./coding-key-prompt.mjs";
123
- import { codingChoiceTags, coderDisplayName, ollamaWhere } from "./coding-choice-tags.mjs";
123
+ import { codingChoiceTags, coderDisplayName, coderWhere, whereOfActiveCoder } from "./coding-choice-tags.mjs";
124
124
  import {
125
125
  isDoubleClick,
126
126
  clickRun,
@@ -5242,6 +5242,7 @@ async function reconfigureOllamaServer() {
5242
5242
  // Persist the CLEARING too. The stored override names a model on the OLD box; leaving it on
5243
5243
  // disk would resurrect it at the next launch, against a server that has never heard of it.
5244
5244
  await saveSessionCoding(resolve(process.cwd()));
5245
+ await syncActiveCoderWhere(); // the bar now names the account default — with ITS host (#263)
5245
5246
  // The GLOBAL output-token total is keyed to the coding backend (task #125), and the backend
5246
5247
  // just moved — re-fetch it for the new one, exactly as /model does after a pick, or the
5247
5248
  // footer keeps counting against the box that is no longer in use.
@@ -5533,6 +5534,7 @@ async function changeProviderModel(ps, rows, p) {
5533
5534
  sessionCodingLabel = "";
5534
5535
  sessionCodingUrl = "";
5535
5536
  await saveSessionCoding(resolve(process.cwd()));
5537
+ await syncActiveCoderWhere();
5536
5538
  console.log(GUTTER + dim("this folder's pick named the old model — cleared it."));
5537
5539
  }
5538
5540
  console.log(accent(` ✓ ${p.host} → ${model}`) + dim(" · pick it in /model\n"));
@@ -5625,6 +5627,7 @@ async function removeCodingProvider(ps, rows, p) {
5625
5627
  sessionCodingLabel = "";
5626
5628
  sessionCodingUrl = "";
5627
5629
  await saveSessionCoding(resolve(process.cwd()));
5630
+ await syncActiveCoderWhere();
5628
5631
  console.log(GUTTER + dim("this folder was using it — cleared the pick.\n"));
5629
5632
  }
5630
5633
  console.log(
@@ -5979,6 +5982,9 @@ async function useCoderInThisFolder({ ps, rows, beforeChoices, url, host, locali
5979
5982
  sessionCodingModel = offered.id;
5980
5983
  sessionCodingLabel = model;
5981
5984
  sessionCodingUrl = url;
5985
+ // …and the host the bar puts in front of it (#263): left alone, the PREVIOUS coder's
5986
+ // "ollama.com/" stayed in front of a newly added Claude model until the next /model pick.
5987
+ activeCoderWhere = whereOfActiveCoder(after?.codingChoices, sessionCodingModel);
5982
5988
  // The folder follows the SERVER's key (task #245) — its default, which a first key already is.
5983
5989
  sessionCodingKeyId = "";
5984
5990
  sessionApiKeyTail = key ? key.slice(-4) : "";
@@ -6072,9 +6078,9 @@ async function chooseModel() {
6072
6078
  // OpenAI-compatible providers are both "OpenAI-compatible", so the kind names neither.
6073
6079
  const { tags, show: showTag } = codingChoiceTags(choices);
6074
6080
  const labels = choices.map((c, i) => {
6075
- // An Ollama row says WHERE it runs in front of the name ("ollama.com/kimi-k3"), so the kind tag
6076
- // beside it would only repeat "Ollama". Other rows keep the tag rule.
6077
- const where = ollamaWhere(c.kind, c.url);
6081
+ // Every row says WHERE it runs in front of the name ("ollama.com/kimi-k3",
6082
+ // "api.anthropic.com/claude-opus-5" — #263), so the kind tag is only for a row with no server.
6083
+ const where = coderWhere(c.kind, c.url);
6078
6084
  const tag = showTag && !where ? ` ${dim(`· ${tags[i]}`)}` : "";
6079
6085
  return `${coderDisplayName(c.model, where)}${tag}${c.id === def ? " (default)" : ""}`;
6080
6086
  });
@@ -6122,7 +6128,7 @@ async function chooseModel() {
6122
6128
  // WRITTEN NOW, not at the end of the next turn: `/model` then `/exit` is how the choice is
6123
6129
  // made and then tested, and that path never reaches saveSession.
6124
6130
  await saveSessionCoding(resolve(process.cwd()));
6125
- activeCoderWhere = ollamaWhere(chosen.kind, chosen.url);
6131
+ activeCoderWhere = coderWhere(chosen.kind, chosen.url);
6126
6132
  const where = showTag && !activeCoderWhere ? ` (${tags[idx]})` : "";
6127
6133
  console.log(
6128
6134
  accent(
@@ -8644,6 +8650,16 @@ let defaultCodingModel = "";
8644
8650
  let sessionCodingUrl = "";
8645
8651
  /** "ollama.com" / "127.0.0.1" / a host — where the ACTIVE coder runs, when it is an Ollama one. */
8646
8652
  let activeCoderWhere = "";
8653
+ /**
8654
+ * Re-read where the coder in use runs (#263) — for the paths that change it without holding the
8655
+ * API's choice list (they clear the folder's pick back to the account default). Best-effort: an
8656
+ * unreachable API leaves no prefix rather than a stale one.
8657
+ */
8658
+ async function syncActiveCoderWhere() {
8659
+ const payload = await fetchAgentPayload([{ role: "user", content: "ping" }]).catch(() => null);
8660
+ activeCoderWhere = whereOfActiveCoder(payload?.codingChoices, sessionCodingModel);
8661
+ if (barPinned()) paintBar();
8662
+ }
8647
8663
  // The account-default backend's URL (from the startup probe's codingChoices), used when no
8648
8664
  // /model override is active. Set once at startup alongside defaultCodingModel.
8649
8665
  let defaultCodingUrl = "";
@@ -10527,14 +10543,9 @@ async function main() {
10527
10543
  }
10528
10544
  {
10529
10545
  // WHERE THE CODER RUNS, for the banner and the bar: the folder's pick, else the default.
10530
- const choices = Array.isArray(probe?.codingChoices) ? probe.codingChoices : [];
10531
- const active =
10532
- (sessionCodingModel && choices.find((c) => c.id === sessionCodingModel || c.model === sessionCodingModel)) ||
10533
- choices.find((c) => c.isDefault) ||
10534
- null;
10535
10546
  // No choice list (an older API): no prefix, rather than guess from the ACCOUNT default's raw
10536
10547
  // payload fields, which key-choice-regression.test.mjs forbids for exactly that reason.
10537
- activeCoderWhere = active ? ollamaWhere(active.kind, active.url) : "";
10548
+ activeCoderWhere = whereOfActiveCoder(probe?.codingChoices, sessionCodingModel);
10538
10549
  }
10539
10550
  // Task #133: no RAG storage question any more — the terminal ALWAYS keeps the index on
10540
10551
  // this machine. `/rag-cloud` opts into additionally mirroring it to the user's S3 bucket.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.734",
3
+ "version": "1.0.735",
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",