feat(backend): adapt every completion to the detected backend (workspace T-137)
One choke point in the sanitized client: the flavor is probed once (the boilerroom wrapper names itself on /health, a bare llama-server serves /props, Ollama answers neither) and every completion adapts. The agents' tool_choice "required" survives only on Ollama — advisory there, enforced by llama-server, an unbreakable tool loop through the wrapper. Through the wrapper every completion carries webber's session identity: session webber, eviction_order 20 in the decided ranking, configurable via settings. The wrapper's balancing and compaction signals are read from the response body's extra fields — an httpx event-hook variant was tried and never fires under the openai SDK. The enabling fix: the sanitized client was never in the request path. The provider assigned self._openai_client, an attribute nobody reads — OllamaProvider.client serves self._client — so every completion has bypassed the null-content sanitizer since the class was introduced. Exposed when the wrapper 503'd a session-less request the choke point should have named; the client now goes through the constructor's official openai_client parameter, and a wiring test pins provider.client to the sanitized type. The same bug exists in tatlock (its T-6, filed). Verified against the live wrapper from the dev server: flavor boilerroom detected, a tool-using explore run answered in 4.8 s with no tool loop, webber resident at rank 20, and the wrapper parked librarian and tatlock-experts to seat it — the ranking doing exactly its job. Six new tests (227 green), five mutation-checked: the rank default, the strip condition, the session-add condition, the no-cache-on-failure rule, and the client wiring. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
Backend adaptation at the provider choke point (workspace T-137).
|
||||
|
||||
The flavor globals are set directly so the tests are deterministic
|
||||
regardless of which backend is reachable; the wire facts the probe
|
||||
relies on are the wrapper's contract, pinned in its own repo.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
from src.ollama import provider
|
||||
from src.shared.config import get_settings
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_flavor(monkeypatch):
|
||||
"""Each test states its flavor; nothing leaks between them."""
|
||||
monkeypatch.setattr(provider, "_local_flavor", None)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_ollama_keeps_the_advisory_nudge(monkeypatch):
|
||||
monkeypatch.setattr(provider, "_local_flavor", "ollama")
|
||||
kwargs = await provider._adapt_to_backend(
|
||||
{"extra_body": {"tool_choice": "required"}}
|
||||
)
|
||||
assert kwargs["extra_body"] == {"tool_choice": "required"}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_wrapper_strips_the_nudge_and_names_the_session(monkeypatch):
|
||||
# tool_choice "required" is enforced by llama-server — through the
|
||||
# wrapper it is the unbreakable tool loop, so it must not pass.
|
||||
monkeypatch.setattr(provider, "_local_flavor", "boilerroom")
|
||||
kwargs = await provider._adapt_to_backend(
|
||||
{"extra_body": {"tool_choice": "required"}}
|
||||
)
|
||||
extra = kwargs["extra_body"]
|
||||
assert "tool_choice" not in extra
|
||||
assert extra["session"] == "webber"
|
||||
assert extra["eviction_order"] == 20
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_bare_llama_server_strips_without_naming(monkeypatch):
|
||||
monkeypatch.setattr(provider, "_local_flavor", "llama-server")
|
||||
kwargs = await provider._adapt_to_backend(
|
||||
{"extra_body": {"tool_choice": "required"}}
|
||||
)
|
||||
assert "extra_body" not in kwargs
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_rank_and_name_come_from_settings(monkeypatch):
|
||||
monkeypatch.setattr(provider, "_local_flavor", "boilerroom")
|
||||
settings = get_settings()
|
||||
monkeypatch.setattr(settings, "backend_session_name", "webber-test")
|
||||
monkeypatch.setattr(settings, "backend_session_rank", 7)
|
||||
kwargs = await provider._adapt_to_backend({})
|
||||
assert kwargs["extra_body"] == {"session": "webber-test", "eviction_order": 7}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_other_extension_fields_survive_adaptation(monkeypatch):
|
||||
monkeypatch.setattr(provider, "_local_flavor", "boilerroom")
|
||||
kwargs = await provider._adapt_to_backend({"extra_body": {"marker": 1}})
|
||||
assert kwargs["extra_body"]["marker"] == 1
|
||||
assert kwargs["extra_body"]["session"] == "webber"
|
||||
|
||||
|
||||
def test_body_signals_are_logged(monkeypatch):
|
||||
# The httpx event-hook variant never fired under the SDK; the body
|
||||
# read must demonstrably log, or the signal passes silently again.
|
||||
calls = []
|
||||
|
||||
class StubLogger:
|
||||
def info(self, msg):
|
||||
calls.append(("info", msg))
|
||||
|
||||
def warning(self, msg):
|
||||
calls.append(("warning", msg))
|
||||
|
||||
monkeypatch.setattr(provider, "logger", StubLogger())
|
||||
|
||||
class Busy:
|
||||
model_extra = {"balancing": [{"parked": "librarian"}], "compaction_due": True}
|
||||
|
||||
provider._log_wrapper_signals(Busy())
|
||||
assert any(kind == "info" and "parked" in msg for kind, msg in calls)
|
||||
assert any(kind == "warning" for kind, msg in calls)
|
||||
|
||||
calls.clear()
|
||||
|
||||
class Quiet:
|
||||
model_extra = {"balancing": [], "compaction_due": False}
|
||||
|
||||
provider._log_wrapper_signals(Quiet())
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_provider_serves_the_sanitized_client():
|
||||
# The whole mechanism rides on `.client` returning OUR instance:
|
||||
# assigning a lookalike attribute after super().__init__ built its
|
||||
# own client is how the sanitizer silently died until 2026-09-13.
|
||||
p = provider.WebberOllamaProvider(base_url="http://127.0.0.1:9/v1")
|
||||
assert isinstance(p.client, provider._SanitizedAsyncOpenAI)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_probe_failure_answers_ollama_without_caching(monkeypatch):
|
||||
# A backend that is down at first call must be re-probed later —
|
||||
# caching "ollama" forever would nudge llama-server into the tool
|
||||
# loop the moment the wrapper came back.
|
||||
settings = get_settings()
|
||||
monkeypatch.setattr(settings, "ollama_url", "http://127.0.0.1:9")
|
||||
flavor = await provider._detect_flavor()
|
||||
assert flavor == "ollama"
|
||||
assert provider._local_flavor is None
|
||||
Reference in New Issue
Block a user