diff options
| -rw-r--r-- | AGENTS.md | 3 | ||||
| -rw-r--r-- | assistant.py | 21 | ||||
| -rw-r--r-- | docs/superpowers/specs/2026-10-23-cloud-models-design.md | 4 |
3 files changed, 20 insertions, 8 deletions
@@ -297,7 +297,8 @@ and history were untouched. - **Hetzner sends `usage` on every chunk**, not once at the end: the daemon sent ~60 `ctx` events per reply. Now one per request. - Titles from the cloud took ~12 s: `enable_thinking` is not sent to - providers, so Qwen reasons over the title too. Harmless, after done. + providers, so Qwen reasoned over the title too. Titles now always come + from the local loaded model, thinking off. - Qwen's replies start with a blank line or two after its reasoning; Markdown drops them. - The key appeared in no log line. diff --git a/assistant.py b/assistant.py index fac6b9c..2bc91d5 100644 --- a/assistant.py +++ b/assistant.py @@ -1211,18 +1211,19 @@ def answer(mine, text, files=(), chosen=None): emit({"type": "state", "state": "idle"}) log("assistant:", said) if fresh: - retitle(mine, conv, text, said, model) + retitle(mine, conv, text, said) -def retitle(mine, conv, user, said, model=None): +def retitle(mine, conv, user, said): # The first six words stand until the model names the conversation. Asked # after done, under the reply's turn: the next ask, stop, new or open # hangs it up, so it never holds a reply back, and the six words stay. + # Always the local model, cloud chat or not: with thinking off it is + # quick, where a cloud model reasoned over the title for ~12 s. try: msgs = [{"role": "user", "content": f"{TITLE}\n\nUser: {user[:2000]}\n\nAssistant: {said[:2000]}"}] - text = "".join(p for k, p in ask(msgs, {}, None, mine, think=False, meter=False, - model=model) + text = "".join(p for k, p in ask(msgs, {}, None, mine, think=False, meter=False) if k == "token") line = next((l for l in text.splitlines() if l.strip()), "") title = line.strip().strip("\"'*#").strip()[:80] @@ -1515,7 +1516,8 @@ def test(): pass def do_GET(self): - script["auth"] = self.headers.get("Authorization") + if self.headers.get("Authorization"): # cloud_models; the router's has none + script["auth"] = self.headers["Authorization"] body = ({"data": [{"id": "m", "status": {"value": "loaded"}, "architecture": {"input_modalities": script["inputs"]}}]} if self.path.endswith("/models") @@ -1525,9 +1527,9 @@ def test(): self.wfile.write(json.dumps(body).encode()) def do_POST(self): - script["auth"] = self.headers.get("Authorization") body = json.loads(self.rfile.read(int(self.headers["Content-Length"]))) if str(body["messages"][0]["content"]).startswith(TITLE): + script["title_path"] = self.path # Answered apart, so a title never takes a reply meant for an ask. self.send_response(200) self.end_headers() @@ -1535,6 +1537,7 @@ def test(): "content": script.get("title", "")}}]}).encode() + b"\n\ndata: [DONE]\n\n") return script.get("paths", []).append(self.path) + script["auth"] = self.headers.get("Authorization") # not a title's script["body"] = body script.get("bodies", []).append(script["body"]) if script.get("hold"): @@ -1872,8 +1875,14 @@ def test(): {"tool_calls": [{"index": 0, "id": "k1", "function": {"name": "mail", "arguments": "{}"}}]}], [{"content": "No mail tool."}]] + script["title_path"] = None send(type="ask", text="any mail?", model="fake:vendor/x:free") seen = until("done") + for _ in range(40): # the title comes after done + if script["title_path"]: + break + time.sleep(0.05) + assert script["title_path"] == "/v1/chat/completions" # local, not the provider first, second = script["bodies"] assert script["paths"] == ["/cloud/v1/chat/completions"] * 2 assert script["auth"] == "Bearer sk-test" diff --git a/docs/superpowers/specs/2026-10-23-cloud-models-design.md b/docs/superpowers/specs/2026-10-23-cloud-models-design.md index 5cf2c56..d0e91b7 100644 --- a/docs/superpowers/specs/2026-10-23-cloud-models-design.md +++ b/docs/superpowers/specs/2026-10-23-cloud-models-design.md @@ -85,7 +85,9 @@ ids contain `/` and sometimes `:` (OpenRouter's `:free`). the chat's model. The audio never leaves; only the transcript does, as the text question. If the loaded model cannot hear, the ask is refused as today. -- **Titles.** Written by the chat's own model. +- **Titles.** Written by the local loaded model, thinking off, whatever + the chat's model (changed after the live check: a cloud model reasoned + over the title for ~12 s). - **To .md** (`write`) has no chat, so it always uses the local loaded model. - **`models` command.** Answers with the router's models (id, status) |
