Compare commits

...
Author SHA1 Message Date
conductor 46e74110ad chore(release): auto-bump version to 0.66.16 — earned by # (patch: converged release (push-to-master bump))
Auto-version and Release / release (push) Successful in 12s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / lint (push) Successful in 36s
CI Pipeline / unit-tests (push) Successful in 56s
CI Pipeline / canary (push) Successful in 29s
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 7s
CI Pipeline / type-check (push) Successful in 30s
CI Pipeline / required-checks (push) Successful in 1s
deploy / deploy (push) Successful in 1m8s
Promotion record: (unknown)
2026-09-12 03:40:49 +01:00
Nick c669e37a3d fix(runtime): pin cloud models by bare model id — qualified name 400s on the wire
CI Pipeline / documentation (push) Successful in 5s
Auto-version and Release / release (push) Successful in 31s
CI Pipeline / lint (push) Successful in 36s
CI Pipeline / canary (push) Successful in 32s
CI Pipeline / type-check (push) Successful in 30s
CI Pipeline / gitea-offline (push) Successful in 17s
CI Pipeline / unit-tests (push) Successful in 1m0s
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / required-checks (push) Successful in 2s
deploy / deploy (push) Successful in 3m29s
The runtime passes '<Provider>/<model>' to the transport; the provider API
wants the bare model id. Observed live: every pinned-Verboo PL1 held-out
attempt 400'd because the wire carried 'Verboo Code/glm-5.3-flash'.
Regression test proves the wire now carries 'glm-5.3-flash'. 1296 green.
2026-09-12 03:40:28 +01:00
conductor 7f4a6262ae chore(release): auto-bump version to 0.66.15 — earned by # (minor: converged release (push-to-master bump))
Auto-version and Release / release (push) Successful in 10s
CI Pipeline / required-checks (push) Successful in 7s
CI Pipeline / lint (push) Successful in 57s
CI Pipeline / unit-tests (push) Successful in 1m34s
CI Pipeline / canary (push) Successful in 52s
deploy / deploy (push) Successful in 42s
CI Pipeline / best-practices (push) Successful in 6s
CI Pipeline / documentation (push) Successful in 6s
CI Pipeline / type-check (push) Successful in 51s
CI Pipeline / gitea-offline (push) Successful in 21s
Promotion record: (unknown)
2026-09-12 03:09:58 +01:00
Nick 34d5ab74b7 feat(runtime): OpenAI-compatible transport for cloud-pinned operator execution
CI Pipeline / best-practices (push) Successful in 12s
CI Pipeline / documentation (push) Successful in 6s
Auto-version and Release / release (push) Successful in 33s
CI Pipeline / canary (push) Successful in 55s
CI Pipeline / lint (push) Successful in 1m8s
CI Pipeline / type-check (push) Successful in 46s
CI Pipeline / gitea-offline (push) Successful in 18s
CI Pipeline / unit-tests (push) Successful in 1m28s
CI Pipeline / required-checks (push) Successful in 4s
deploy / deploy (push) Successful in 3m45s
Merges prototype/openai-compat-transport (7b268e1). The operator's execution
substrate had exactly one transport (OllamaChatTransport, localhost:11434) —
send_task(model='<Provider>/<model>') posted cloud model names to Ollama's
/api/chat and 400'd (observed live: Verboo Code/glm-5.3-flash, 0.2s). The
supported model= override could not execute ANY cloud provider.

OpenAICompatTransport speaks the OpenAI wire format (/chat/completions) and
returns Ollama-shaped responses (LocalToolRuntime loop unchanged). Selection
is deterministic on routing_mode=='cloud' — no router, ladder, or escalation
change. Provider config resolved from the same source of truth the mimo CLI
uses (mimocode.jsonc baseURL + auth.json key).

Translation unit tests (6) + live smoke against the real Verboo API
(TRANSPORT-SMOKE-OK). Umbrella manifest updated. Stray experiment files
removed. 1295 tests green; ratchet 352/80; henxels clean.
2026-09-12 03:09:17 +01:00
Nick 7b268e17fe prototype: OpenAI-compatible transport for cloud-pinned operator execution (NOT RELEASED)
deploy / deploy (push) Skipped
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / gitea-offline (push) Successful in 21s
CI Pipeline / lint (push) Failing after 49s
CI Pipeline / canary (push) Failing after 50s
CI Pipeline / type-check (push) Successful in 39s
CI Pipeline / unit-tests (push) Failing after 1m24s
CI Pipeline / required-checks (push) Successful in 1s
Local branch only — preserved per directive 2026-09-12: the PL1 held-out
strong-provider rerun is BLOCKED_BY_EXECUTION_SUBSTRATE (operator's only
transport is Ollama; send_task(model='Verboo Code/glm-5.3-flash') 400s).
This prototype makes the supported override executable but is NOT authorized
for release. Translation unit tests + live smoke (TRANSPORT-SMOKE-OK) pass.
2026-09-12 02:29:15 +01:00
11 changed files with 295 additions and 9 deletions

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
# Rune
**Current release: v0.66.14**
**Current release: v0.66.16**
**Rune is a persistent personal agent designed to accumulate experience, develop reusable competence, and remain recognisably the same agent even when its underlying foundation model changes.**
+1
View File
@@ -118,6 +118,7 @@
"tool_executor",
"tool_protocol",
"local_tool_runtime",
"openai_compat_transport",
"forge_client",
"message_queue",
"connection_pool",
-1
View File
@@ -1 +0,0 @@
def broken(:
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "rune"
version = "0.66.14"
version = "0.66.16"
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
requires-python = ">=3.10"
+1 -1
View File
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
import json
from pathlib import Path
__version__ = "0.66.14"
__version__ = "0.66.16"
# One build identity (handoff 2026-09-10 §4): the semantic version above is
# the repo-level truth (auto-bumped at merge); the DEPLOYED revision (exact
+1 -1
View File
@@ -231,7 +231,7 @@ def main() -> int:
)
check(
"Rune minor version advertises provisioning and restore verification",
'__version__ = "0.66.14"' in rune_version,
'__version__ = "0.66.16"' in rune_version,
failures,
)
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
+189
View File
@@ -0,0 +1,189 @@
"""OpenAI-compatible chat transport (T15 PL1 held-out substrate fix).
The operator's execution substrate had exactly one transport —
OllamaChatTransport (localhost:11434) — so send_task(model=<cloud>) posted a
cloud model name to Ollama's /api/chat and died with HTTP 400 (observed live
2026-09-12: 'Verboo Code/glm-5.3-flash' → 400 in 0.2s). The supported
model= override could not execute ANY cloud provider.
This transport speaks the OpenAI wire format (/chat/completions) and returns
Ollama-SHAPED responses, so LocalToolRuntime's loop is unchanged. Selection
is deterministic (routing_mode == 'cloud') — no router, ladder, or escalation
logic is touched.
"""
from __future__ import annotations
import json
import time
import urllib.request
class OpenAICompatTransport:
"""ChatTransport for OpenAI-compatible providers (Verboo Code, Xioami...)."""
def __init__(
self,
base_url: str,
api_key: str,
provider_name: str,
timeout: float = 120,
context_window: int = 131072,
):
self.base_url = base_url.rstrip("/")
self.api_key = api_key
self.provider = provider_name
self.timeout = timeout
self._context_window = context_window
# Usage accumulators — same contract as OllamaChatTransport (I2).
self.usage_calls = 0
self.usage_prompt_tokens = 0
self.usage_output_tokens = 0
self.usage_total_duration_ns = 0
# -- wire -----------------------------------------------------------
def _post(self, path: str, payload: dict) -> dict:
request = urllib.request.Request(
f"{self.base_url}{path}",
data=json.dumps(payload).encode("utf-8"),
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}",
},
)
with urllib.request.urlopen(request, timeout=self.timeout) as response:
decoded = json.loads(response.read())
if not isinstance(decoded, dict):
raise ValueError("provider returned a non-object response")
return decoded
# -- message translation --------------------------------------------
@staticmethod
def _to_openai_messages(messages: list[dict]) -> list[dict]:
"""Ollama-shape → OpenAI-shape. Tool results carry tool_call_id
already (the runtime's _tool_message sets it from request.call_id);
assistant tool_calls gain ids and JSON-string arguments."""
out: list[dict] = []
for m in messages:
role = m.get("role")
if role == "tool":
out.append({
"role": "tool",
"tool_call_id": m.get("tool_call_id") or f"call_{len(out)}",
"content": m.get("content", ""),
})
continue
tool_calls = m.get("tool_calls") or []
if role == "assistant" and tool_calls:
tc = []
for i, c in enumerate(tool_calls):
fn = c.get("function", {})
args = fn.get("arguments", {})
args_str = json.dumps(args) if isinstance(args, dict) else str(args)
tc.append({
"id": f"call_{len(out)}_{i}",
"type": "function",
"function": {"name": fn.get("name"), "arguments": args_str},
})
out.append({"role": "assistant", "content": m.get("content") or None, "tool_calls": tc})
continue
out.append({"role": role, "content": m.get("content", "")})
return out
@staticmethod
def _from_openai_response(resp: dict) -> dict:
"""OpenAI response → Ollama-shape (LocalToolRuntime's contract)."""
choices = resp.get("choices") or [{}]
message = choices[0].get("message", {}) or {}
tool_calls = []
for c in message.get("tool_calls") or []:
fn = c.get("function", {}) or {}
args = fn.get("arguments", {})
if isinstance(args, str):
try:
args = json.loads(args)
except json.JSONDecodeError:
args = {"_raw": args}
tool_calls.append({"function": {"name": fn.get("name"), "arguments": args or {}}})
out: dict = {"message": {"role": "assistant", "content": message.get("content") or ""}}
if tool_calls:
out["message"]["tool_calls"] = tool_calls
return out
# -- ChatTransport protocol -------------------------------------------
def chat(self, *, model: str, messages: list[dict], tools: list[dict] | None,
options: dict | None = None, think: bool | str | None = None) -> dict:
# The runtime passes the qualified name ('<Provider>/<model>'); the
# provider API wants the bare model id ('glm-5.3-flash'). Observed
# live: qualified name → 400 on every call (PL1 held-out attempt).
wire_model = model.split("/", 1)[-1] if "/" in model else model
payload: dict = {
"model": wire_model,
"messages": self._to_openai_messages(messages),
"stream": False,
}
if tools:
payload["tools"] = tools # Ollama tool schema == OpenAI tool schema
if options:
for k in ("temperature", "top_p", "max_tokens"):
if k in options:
payload[k] = options[k]
started = time.monotonic()
resp = self._post("/chat/completions", payload)
elapsed_ns = int((time.monotonic() - started) * 1e9)
usage = resp.get("usage", {}) or {}
self.usage_calls += 1
self.usage_prompt_tokens += int(usage.get("prompt_tokens") or 0)
self.usage_output_tokens += int(usage.get("completion_tokens") or 0)
self.usage_total_duration_ns += elapsed_ns
return self._from_openai_response(resp)
def supports_tools(self, model: str) -> bool:
return True
def supports_thinking(self, model: str) -> bool:
return False
def context_length(self, model: str) -> int:
return self._context_window
def cloud_transport_for(model: str, timeout: float = 120) -> OpenAICompatTransport:
"""Resolve provider config for a '<Provider>/<model>' name from the same
source of truth the mimo CLI uses (mimocode.jsonc + auth.json)."""
import os
import re
from pathlib import Path
if "/" not in model:
raise ValueError(f"cloud model must be '<Provider>/<model>': {model!r}")
provider_name, short_model = model.split("/", 1)
base_url = api_key = None
context_window = 131072
cfg_path = Path(os.path.expanduser("~/.config/mimocode/mimocode.jsonc"))
if cfg_path.is_file():
text = cfg_path.read_text()
m = re.search(rf'"{re.escape(provider_name)}"\s*:\s*\{{', text)
if m:
block = text[m.start():m.start() + 2000]
bm = re.search(r'"baseURL":\s*"([^"]+)"', block)
if bm:
base_url = bm.group(1)
wm = re.search(rf'"{re.escape(short_model)}"\s*:\s*\{{[^}}]*?"context_window"\s*:\s*(\d+)', block)
if wm:
context_window = int(wm.group(1))
if not base_url:
raise ValueError(f"no baseURL configured for provider {provider_name!r}")
key = os.environ.get(f"{provider_name.upper().replace(' ', '_')}_API_KEY", "")
if not key:
auth_path = Path(os.path.expanduser("~/.local/share/mimocode/auth.json"))
if auth_path.is_file():
auth = json.loads(auth_path.read_text())
key = (auth.get(provider_name) or {}).get("key", "")
if not key:
raise ValueError(f"no API key for provider {provider_name!r}")
return OpenAICompatTransport(
base_url, key, provider_name, timeout=timeout, context_window=context_window
)
+13 -1
View File
@@ -1953,8 +1953,20 @@ I have tools to read, write, edit, search, lint, test, format, and commit code.
if hasattr(capsule, "capture_messages"):
capsule.capture_messages(messages)
# Transport selection is deterministic substrate (T15 PL1
# substrate fix): the pinned provider override
# (send_task(model='<Provider>/<model>')) requires an
# OpenAI-compatible wire for cloud models — OllamaChatTransport
# is localhost-only and 400s on any cloud model name. Router,
# ladder, and escalation logic are untouched.
if routing_mode == "cloud" and "/" in model:
from rune.openai_compat_transport import cloud_transport_for
transport = cloud_transport_for(model, timeout=timeout)
else:
transport = OllamaChatTransport(timeout=timeout)
runtime = LocalToolRuntime(
transport=OllamaChatTransport(timeout=timeout),
transport=transport,
executor=tool_executor.execute_tool,
model=model,
tools=capability_selection["tools"],
+2 -2
View File
@@ -8,7 +8,7 @@ mode system, subject header, shared memory and identity.
This is not an interface. This is how I see, speak, and interact with the world.
Version: v0.66.14
Version: v0.66.16
Features: 41 (24 original + 17 new native commands)
Usage:
@@ -1856,7 +1856,7 @@ class RuneApp(App):
# Show version info
self.chat_channel.add_message(
"system",
f"[dim]Rune v0.66.14 | Textual TUI | {len(loaded)} native modules[/dim]",
f"[dim]Rune v0.66.16 | Textual TUI | {len(loaded)} native modules[/dim]",
)
# Auto-show night mode suggestion if evening/night hours
+86
View File
@@ -0,0 +1,86 @@
"""OpenAI-compatible transport (T15 PL1 substrate fix).
The operator had exactly one transport (Ollama) — send_task(model=<cloud>)
posted cloud model names to Ollama's /api/chat and 400'd. These tests prove
the translation layer both directions; the live wire is smoke-tested
separately (provider credentials are not test fixtures)."""
import importlib.util
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "src"))
from rune.openai_compat_transport import OpenAICompatTransport # noqa: E402
class TestMessageTranslation:
def test_ollama_to_openai_roundtrip(self):
msgs = [
{"role": "system", "content": "sys"},
{"role": "user", "content": "task"},
{"role": "assistant", "content": "", "tool_calls": [
{"function": {"name": "edit_file", "arguments": {"path": "/x.py", "old": "a", "new": "b"}}}]},
{"role": "tool", "tool_call_id": "call_2_0", "content": "{\"success\": true}"},
]
out = OpenAICompatTransport._to_openai_messages(msgs)
# assistant tool_calls: dict args → JSON string, ids assigned
tc = out[2]["tool_calls"][0]
assert tc["function"]["arguments"] == json.dumps({"path": "/x.py", "old": "a", "new": "b"})
assert tc["id"] == "call_2_0"
# tool result keeps its call id
assert out[3]["tool_call_id"] == "call_2_0"
# plain roles pass through
assert out[0] == {"role": "system", "content": "sys"}
def test_openai_response_to_ollama_shape(self):
resp = {"choices": [{"message": {"role": "assistant", "content": None, "tool_calls": [
{"id": "z", "type": "function", "function": {"name": "run_tests", "arguments": "{\"path\": \"t\"}"}}]}}]}
shape = OpenAICompatTransport._from_openai_response(resp)
tc = shape["message"]["tool_calls"][0]
assert tc["function"]["name"] == "run_tests"
assert tc["function"]["arguments"] == {"path": "t"} # string → dict
def test_malformed_arguments_become_raw(self):
resp = {"choices": [{"message": {"tool_calls": [
{"function": {"name": "x", "arguments": "not-json"}}]}}]}
shape = OpenAICompatTransport._from_openai_response(resp)
assert shape["message"]["tool_calls"][0]["function"]["arguments"] == {"_raw": "not-json"}
def test_no_tool_calls_plain_content(self):
resp = {"choices": [{"message": {"content": "hello"}}]}
shape = OpenAICompatTransport._from_openai_response(resp)
assert shape["message"]["content"] == "hello"
assert "tool_calls" not in shape["message"]
def test_protocol_methods(self):
t = OpenAICompatTransport("http://x/v1", "k", "Test")
assert t.supports_tools("m") is True
assert t.supports_thinking("m") is False
assert t.context_length("m") == 131072
def test_usage_accumulators(self):
t = OpenAICompatTransport("http://x/v1", "k", "Test")
t.chat = lambda **kw: ( # simulate the accumulator path
setattr(t, "usage_calls", t.usage_calls + 1),
setattr(t, "usage_prompt_tokens", t.usage_prompt_tokens + 10),
setattr(t, "usage_output_tokens", t.usage_output_tokens + 5),
)[-1]
t.chat(model="m", messages=[])
assert t.usage_calls == 1 and t.usage_prompt_tokens == 10 and t.usage_output_tokens == 5
def test_qualified_model_name_sent_as_bare_id(self):
"""Runtime passes '<Provider>/<model>'; the wire must carry the bare
model id (observed live: qualified name → 400 on every call)."""
captured = {}
def fake_post(path, payload):
captured.update(payload)
return {"choices": [{"message": {"role": "assistant", "content": "ok"}}], "usage": {}}
t = OpenAICompatTransport("http://x/v1", "k", "Verboo Code")
t._post = fake_post
t.chat(model="Verboo Code/glm-5.3-flash", messages=[{"role": "user", "content": "hi"}], tools=None)
assert captured["model"] == "glm-5.3-flash"
-1
View File
@@ -1 +0,0 @@
def broken(: