From 42b3720721d708a10f81a244ad5b57edde3764f3 Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Fri, 10 Jul 2026 08:08:56 -0500 Subject: [PATCH] =?UTF-8?q?feat(api):=20config=20+=20role=E2=86=92model=20?= =?UTF-8?q?routing=20(qwen3.5=20narrator=20default,=20think=20off)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- api/app/config.py | 22 ++++++++++++++++++++++ api/app/routing.py | 25 +++++++++++++++++++++++++ api/tests/test_routing.py | 21 +++++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 api/app/config.py create mode 100644 api/app/routing.py create mode 100644 api/tests/test_routing.py diff --git a/api/app/config.py b/api/app/config.py new file mode 100644 index 0000000..4981f57 --- /dev/null +++ b/api/app/config.py @@ -0,0 +1,22 @@ +"""Server config, read from the environment at call time so tests can override +without reimport. Charter §4 — all of this lives server-side; the client never +sees it. +""" + +import os + + +def ollama_base_url() -> str: + return os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434") + + +def ollama_timeout_seconds() -> float: + return float(os.environ.get("OLLAMA_TIMEOUT_SECONDS", "30")) + + +def narrator_model() -> str: + return os.environ.get("OLLAMA_NARRATOR_MODEL", "qwen3.5:latest") + + +def call_log_path() -> str | None: + return os.environ.get("CALL_LOG_PATH") or None diff --git a/api/app/routing.py b/api/app/routing.py new file mode 100644 index 0000000..98d9dba --- /dev/null +++ b/api/app/routing.py @@ -0,0 +1,25 @@ +"""Role → model routing (charter §4/§5). Model choice is config, not law — a +deploy, not a client patch. Read at call time so an env override needs no reimport. +The provider abstraction (Ollama → Replicate) slots in here later (YAGNI now). +""" + +from dataclasses import dataclass + +from . import config + + +@dataclass(frozen=True) +class RoleConfig: + model: str + options: dict + think: bool | None = None # Ollama top-level `think`; None omits it + + +def for_role(role: str) -> RoleConfig: + if role == "narrator": + return RoleConfig( + model=config.narrator_model(), + options={"temperature": 0.8, "top_p": 0.9, "num_predict": 300}, + think=False, # qwen3.x thinking OFF; harmless on non-thinking models + ) + raise KeyError(f"no routing for role: {role}") diff --git a/api/tests/test_routing.py b/api/tests/test_routing.py new file mode 100644 index 0000000..d455598 --- /dev/null +++ b/api/tests/test_routing.py @@ -0,0 +1,21 @@ +import pytest + +from app.routing import for_role + + +def test_narrator_defaults(): + cfg = for_role("narrator") + assert cfg.model == "qwen3.5:latest" + assert cfg.think is False + assert cfg.options["num_predict"] == 300 + assert cfg.options["temperature"] == 0.8 + + +def test_env_overrides_model(monkeypatch): + monkeypatch.setenv("OLLAMA_NARRATOR_MODEL", "llama3.1:latest") + assert for_role("narrator").model == "llama3.1:latest" + + +def test_unknown_role_raises(): + with pytest.raises(KeyError): + for_role("wizard")