refactor(backend): speak only OpenAI-compat /v1 to the local backend

Three native-API touchpoints converted — steward /api/generate to
/v1/chat/completions, embeddings /api/embeddings to /v1/embeddings,
health /api/tags to /v1/models — so the backend behind OLLAMA_HOST is
swappable by env alone. This makes the serving plan's "tatlock needs
zero changes" claim true for the llama-server cutover and for forge
after it. EMBEDDING_HOST (default: OLLAMA_HOST) lets gen and embed
point at different servers, which the boilerroom stack needs.

The dead OllamaClient goes with it: a native-API client nothing
imported, whose presence would make the post-cutover "no native
endpoints" grep lie.

Contract tests rewritten to mirror the new requests and extended with
the embeddings shape (dim must match the configured Qdrant dimension).
All pass against live Ollama's /v1 — deployable before any cutover.
Both new assertions mutation-checked via env overrides (bogus model,
wrong dim: each fails). The Anthropic contract now skips on 401: the
configured key is deliberately revoked per workspace D-11, which is
"fallback disabled", not a boundary break. Embedding continuity across
backends was measured separately: same nomic bytes, cosine 1.0000.

663 unit tests pass; ruff clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-11 21:35:48 +02:00
co-authored by Claude Fable 5
parent f578ebded8
commit 16b7edbc26
8 changed files with 110 additions and 278 deletions
+11 -9
View File
@@ -156,24 +156,26 @@ class StewardAgent:
return response.content[0].text.strip()
async def _call_ollama(self, prompt: str) -> str:
"""Call Ollama API directly for plain text generation."""
"""Call the local backend's OpenAI-compatible chat endpoint.
Plain /v1/chat/completions with sampling params, so any
OpenAI-compatible server (Ollama, llama-server) can sit behind
OLLAMA_HOST without this method knowing which.
"""
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.post(
f"{self.ollama_host}/api/generate",
f"{self.ollama_host}/v1/chat/completions",
json={
"model": self.ollama_model,
"prompt": prompt,
"stream": False,
"options": {
"temperature": 0.3, # Lower = more consistent
"top_p": 0.9,
},
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.3, # Lower = more consistent
"top_p": 0.9,
},
)
response.raise_for_status()
result = response.json()
return result["response"].strip()
return result["choices"][0]["message"]["content"].strip()
async def analyze(self, query: str, conversation_history: list[dict] | None = None) -> str:
"""