- call_log._default_write: swallow any exception from the default file/ stdout sink and report to stderr, so a log-write failure (disk full, bad permissions, misconfigured CALL_LOG_PATH) never turns a successful narration into a 500 (charter §13). Injected write= sinks (used by tests) are left to surface their own errors. - ollama_client.chat: catch ValueError alongside httpx.HTTPError in the retry loop so a 200 response with a non-JSON body (JSONDecodeError is a ValueError subclass) counts as a failed attempt and falls through to ModelError after the one retry, instead of escaping chat() uncaught (charter §12 — one retry, then ModelError, nothing else escapes). - main.py: update stale docstrings — /dm/narrate is now fully wired (routing + model call + logging); the other four roles remain stubs. Regression tests added for both fixes (TDD: watched RED, then GREEN). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
65 lines
2.0 KiB
Python
65 lines
2.0 KiB
Python
"""Structured JSON-lines logging of every model call (charter §10 — the seed and
|
|
the full prompt logged with every call; §4 — free eval/replay infrastructure).
|
|
|
|
One JSON object per line: everything needed to replay the call (canon_log +
|
|
messages + model + seed) against a candidate model. No secrets to redact — the
|
|
client never sends keys, the canon log carries no PII, and the prompt is exactly
|
|
what we want on record.
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
from typing import Callable
|
|
|
|
from . import config
|
|
|
|
|
|
def _default_write(line: str) -> None:
|
|
"""Best-effort (charter §13): a good model call must never become a 500
|
|
because the log write failed (disk full, bad permissions, a misconfigured
|
|
CALL_LOG_PATH). Any failure here is swallowed and reported to stderr —
|
|
never stdout, since stdout may itself be the default sink and must stay
|
|
clean JSON-lines for downstream tooling.
|
|
"""
|
|
try:
|
|
path = config.call_log_path()
|
|
if path:
|
|
with open(path, "a", encoding="utf-8") as handle:
|
|
handle.write(line + "\n")
|
|
else:
|
|
sys.stdout.write(line + "\n")
|
|
except Exception as exc: # noqa: BLE001 - logging must never break a request
|
|
print(f"call_log: failed to write log line: {exc}", file=sys.stderr)
|
|
|
|
|
|
def record(
|
|
*,
|
|
role: str,
|
|
model: str,
|
|
options: dict,
|
|
messages: list[dict],
|
|
canon_log: dict,
|
|
ok: bool,
|
|
latency_ms: int,
|
|
response: str | None = None,
|
|
error: str | None = None,
|
|
write: Callable[[str], None] = _default_write,
|
|
) -> dict:
|
|
rec: dict = {
|
|
"ts": datetime.now(timezone.utc).isoformat(),
|
|
"role": role,
|
|
"model": model,
|
|
"options": options,
|
|
"messages": messages,
|
|
"canon_log": canon_log,
|
|
"ok": ok,
|
|
"latency_ms": latency_ms,
|
|
}
|
|
if ok:
|
|
rec["response"] = response
|
|
else:
|
|
rec["error"] = error
|
|
write(json.dumps(rec, ensure_ascii=False))
|
|
return rec
|