@tiens.nguyen/gu-cli 1.0.733 → 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.
- package/coding-choice-tags.mjs +40 -1
- package/gonext_agent_chat.py +25 -5
- package/gu-repl.mjs +22 -11
- package/package.json +1 -1
package/coding-choice-tags.mjs
CHANGED
|
@@ -95,7 +95,46 @@ export function ollamaWhere(kind, url) {
|
|
|
95
95
|
return (local ? "127.0.0.1" : host) + port;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
/**
|
|
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;
|
package/gonext_agent_chat.py
CHANGED
|
@@ -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
|
|
8248
|
-
"
|
|
8249
|
-
"
|
|
8250
|
-
"
|
|
8251
|
-
"
|
|
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,
|
|
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
|
-
//
|
|
6076
|
-
//
|
|
6077
|
-
const where =
|
|
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 =
|
|
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 =
|
|
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.
|
|
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",
|