@tiens.nguyen/gu-cli 1.0.821 → 1.0.822

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/gu-repl.mjs CHANGED
@@ -154,7 +154,7 @@ import {
154
154
  parseModelFallback,
155
155
  } from "./model-fallback.mjs";
156
156
  import { codingChoiceTags, coderDisplayName, coderWhere, whereOfActiveCoder } from "./coding-choice-tags.mjs";
157
- import { TITLE_POP, TITLE_PUSH, idleTitle, needsSummary, summariseForTitle, titleSeq, trimTitle } from "./terminal-title.mjs";
157
+ import { TITLE_POP, TITLE_PUSH, idleTitle, needsSummary, summariseForTitle, titleModel, titleSeq, trimTitle } from "./terminal-title.mjs";
158
158
  import { IMAGE_NEEDS_CLAUDE, coderTakesImagesFor, clipboardImageToFile, fitImageNotice, imageMarkersForDisplay, createImageStore, imagePasteKey, imagePathFromPaste, isClaudeCoderUrl, isImagePasteKey, storeImageFile } from "./image-paste.mjs";
159
159
  import {
160
160
  isDoubleClick,
@@ -1633,7 +1633,17 @@ function showQuestionInTitle(question) {
1633
1633
  void (async () => {
1634
1634
  const settings = await fetchWorkerSettings();
1635
1635
  if (!settings || mine !== _titleSeq) return;
1636
- const title = await summariseForTitle(question, settings, { fetchImpl: doctorFetch });
1636
+ // #358: the coder this turn runs on, with the key bound to its server — not the agent-model
1637
+ // setting, which can name a model the turns never use, called with no key at all.
1638
+ const target = titleModel(settings, { url: activeCodingUrl(), model: activeCodingModelId() });
1639
+ if (!target) return;
1640
+ const key = target.backendId ? await fetchKeyForBackend(target.backendId) : null;
1641
+ if (mine !== _titleSeq) return;
1642
+ const title = await summariseForTitle(question, settings, {
1643
+ fetchImpl: doctorFetch,
1644
+ target,
1645
+ apiKey: key?.apiKey ?? "",
1646
+ });
1637
1647
  if (title && mine === _titleSeq) writeTitle(title);
1638
1648
  })().catch(() => {});
1639
1649
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.821",
3
+ "version": "1.0.822",
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",
@@ -7,8 +7,8 @@
7
7
  * shortened, and keep it there until the next question.
8
8
  *
9
9
  * TWO STEPS, AND THE FIRST NEVER WAITS FOR THE SECOND. The trimmed question goes up the moment
10
- * the turn starts. Only a question too long to fit is then sent to the agent model for a
11
- * few-word summary, which replaces the trimmed one if — and only if — it arrives usable and the
10
+ * the turn starts. Only a question too long to fit is then sent to the coder the turn runs on
11
+ * (with its server's key — see titleModel) for a few-word summary, which replaces the trimmed one if — and only if — it arrives usable and the
12
12
  * user has not asked something else in the meantime. Every way that call can fail (no model
13
13
  * configured, a provider that wants a key this side does not hold, a timeout, a model that
14
14
  * answers with an essay) ends the same way: the trimmed question stays.
@@ -98,6 +98,30 @@ export function agentModelFromSettings(settings) {
98
98
  return { url, model: str(settings?.agentModelName) || "default_model" };
99
99
  }
100
100
 
101
+ /** A server URL reduced to what identifies it: no case, no trailing slash, no trailing /v1. */
102
+ function serverOf(url) {
103
+ return String(url ?? "").trim().toLowerCase().replace(/\/+$/, "").replace(/\/v1$/, "");
104
+ }
105
+
106
+ /**
107
+ * WHICH MODEL WRITES THE TITLE (#358): the coder this turn runs on, and the server row its key
108
+ * is bound to. → { url, model, backendId } or null.
109
+ *
110
+ * It used to be the account's agent-model setting, which on a real account was OpenAI o1 —
111
+ * not what the turns ran on (kimi-k3 on ollama.com), and called without a key, so every long
112
+ * question stayed trimmed. The coder is the model the user has already chosen and keyed; the
113
+ * agent-model setting is only the fallback for a session whose coder is not known yet.
114
+ */
115
+ export function titleModel(settings, coder = {}) {
116
+ const rows = Array.isArray(settings?.agentCodingBackends) ? settings.agentCodingBackends : [];
117
+ const rowFor = (url) => rows.find((r) => r?.id && serverOf(r.url) === serverOf(url))?.id ?? "";
118
+ const url = String(coder?.url ?? "").trim();
119
+ const model = String(coder?.model ?? "").trim();
120
+ if (url && model) return { url, model, backendId: rowFor(url) };
121
+ const agent = agentModelFromSettings(settings);
122
+ return agent ? { ...agent, backendId: rowFor(agent.url) } : null;
123
+ }
124
+
101
125
  export function summaryRequest(model, question) {
102
126
  const q = Array.from(cleanTitleText(question)).slice(0, SUMMARY_INPUT_MAX).join("");
103
127
  return {
@@ -140,20 +164,22 @@ export function pickSummary(body, max = TITLE_MAX) {
140
164
  }
141
165
 
142
166
  /**
143
- * Ask the agent model for a title. → the title, or "" for every reason there is not one.
167
+ * Ask a model for a title. → the title, or "" for every reason there is not one.
168
+ * `target` ({ url, model }, see titleModel) picks the model — the agent-model setting when absent —
169
+ * and `apiKey` is sent as a Bearer token when the server needs one.
144
170
  * Never throws: this runs beside a turn and must not be able to disturb it.
145
171
  */
146
172
  export async function summariseForTitle(
147
173
  question,
148
174
  settings,
149
- { fetchImpl = fetch, timeoutMs = 20_000 } = {},
175
+ { fetchImpl = fetch, timeoutMs = 20_000, target = null, apiKey = "" } = {},
150
176
  ) {
151
- const agent = agentModelFromSettings(settings);
177
+ const agent = target?.url && target?.model ? target : agentModelFromSettings(settings);
152
178
  if (!agent || !needsSummary(question)) return "";
153
179
  try {
154
180
  const res = await fetchImpl(chatEndpoint(agent.url), {
155
181
  method: "POST",
156
- headers: { "Content-Type": "application/json" },
182
+ headers: { "Content-Type": "application/json", ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}) },
157
183
  body: JSON.stringify(summaryRequest(agent.model, question)),
158
184
  signal: AbortSignal.timeout(timeoutMs),
159
185
  });