fix(api): best-effort call logging + non-JSON 200 → ModelError (final-review)

- 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>
This commit is contained in:
2026-07-10 08:45:39 -05:00
parent 470ccb2935
commit f7c51b79da
5 changed files with 60 additions and 10 deletions

View File

@@ -81,3 +81,15 @@ def test_empty_content_retried_then_modelerror():
with pytest.raises(ModelError):
chat("m", [{"role": "user", "content": "x"}], {}, client=_client(handler))
def test_non_json_200_retried_then_modelerror():
"""A 200 response with a non-JSON body (e.g. a proxy error page) must
degrade to ModelError after the one retry, not escape as a bare
JSONDecodeError/ValueError (charter §12/§13).
"""
def handler(request):
return httpx.Response(200, text="<html>not json</html>")
with pytest.raises(ModelError):
chat("m", [{"role": "user", "content": "x"}], {}, client=_client(handler))