The mojibake in8518114survived a green suite, and not by omission. There is a test called `test_a_stream_is_proxied_verbatim_including_the_telemetry _comments` which passed for months against a proxy that was decoding the stream as ISO-8859-1 and re-encoding it as UTF-8. It asserted three ASCII substrings, and a test that only ever sees ASCII cannot observe a charset bug. Two separate lies, both now fixed. The fakes. `iter_lines` yielded `self._lines` unchanged in both modes, so a test holding str lines got those str back even under decode_unicode -- a model of a stream that had ALREADY been decoded correctly. A fake that hands back the right answer cannot reproduce a decode bug. They now treat the wire as UTF-8 bytes whatever the test wrote, derive `encoding` from Content-Type using requests' own `get_encoding_from_headers`, and default to the charset-less `text/event-stream` OpenRouter really sends. The hostile case is the default, so a broken passthrough can no longer look correct. The assertions. The verbatim test now compares BYTES against every line the provider emitted, and the shared STREAM_LINES sample carries a raw UTF-8 em dash, so the whole streaming surface fails if the proxy ever goes back to decoding and re-encoding. Measured, by reverting8518114and running the suite: before this commit 1 test caught it (the one written for it) after 2, incl. the verbatim test that had been lying The dedicated regression test also gets simpler -- it no longer needs a bespoke response subclass, because the shared fake is now faithful enough. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRQXz5SYZYVWscxS1QqF6U
235 lines
8.5 KiB
Python
235 lines
8.5 KiB
Python
"""Unit tests for the new availability/relevance helpers in dispatcher.py.
|
|
|
|
Offline. Mocks ``requests.post`` so no model/network is reached. Covers the
|
|
fail-safe ``_embed_for_relevance``, the min_candidates/disabled gates in
|
|
``_relevance_order_for``, the circuit-breaker exclusion set ``_open_circuits``,
|
|
and the connection-opening helper ``_open_upstream``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import dispatcher
|
|
import circuit_breaker
|
|
|
|
|
|
class _FakeResp:
|
|
def __init__(self, status_code=200, text="", json_payload=None, lines=None):
|
|
self.status_code = status_code
|
|
self._text = text
|
|
self._json = json_payload
|
|
self._lines = lines or []
|
|
self.closed = False
|
|
|
|
@property
|
|
def text(self):
|
|
return self._text
|
|
|
|
def json(self):
|
|
if self._json is None:
|
|
raise ValueError("no json")
|
|
return self._json
|
|
|
|
def close(self):
|
|
self.closed = True
|
|
|
|
@property
|
|
def encoding(self):
|
|
"""The charset requests would pick, by requests' own rule.
|
|
|
|
Derived rather than asserted, so this fake tracks requests instead of
|
|
restating a belief about it. The default is a charset-less
|
|
``text/event-stream`` on purpose: that is what OpenRouter actually
|
|
sends, and `get_encoding_from_headers` answers ISO-8859-1 for any
|
|
``text/*`` without a charset. Decoding a UTF-8 stream with that and
|
|
re-encoding it produced mojibake on every streamed non-ASCII
|
|
character -- so the hostile case is the DEFAULT here, and a fake can
|
|
no longer make a broken passthrough look correct.
|
|
"""
|
|
from requests.structures import CaseInsensitiveDict
|
|
from requests.utils import get_encoding_from_headers
|
|
|
|
headers = CaseInsensitiveDict(
|
|
getattr(self, "headers", None) or {"Content-Type": "text/event-stream"}
|
|
)
|
|
return get_encoding_from_headers(headers) or "utf-8"
|
|
|
|
def iter_lines(self, decode_unicode=False):
|
|
# Faithful to requests in both modes: BYTES unless decode_unicode is
|
|
# set, and when it is set the charset comes from Content-Type.
|
|
#
|
|
# The `wire` line is the load-bearing one. Whatever a test wrote into
|
|
# `lines`, what a provider actually puts on the wire is UTF-8 bytes,
|
|
# so that is what gets decoded. An earlier version of this fake passed
|
|
# str lines straight through under decode_unicode, which modelled a
|
|
# stream that had ALREADY been decoded correctly -- and a fake that
|
|
# hands back the right answer cannot reproduce a charset bug. It let
|
|
# the whole streaming suite pass against a proxy that was mangling
|
|
# every non-ASCII character.
|
|
for line in self._lines:
|
|
wire = line.encode("utf-8") if isinstance(line, str) else line
|
|
yield wire.decode(self.encoding) if decode_unicode else wire
|
|
|
|
|
|
def _cfg_with_relevance(enabled=True, min_candidates=2):
|
|
class _Rel:
|
|
enabled = True
|
|
model = "nomic-embed-text"
|
|
base_url = "http://localhost:11434/v1"
|
|
timeout_seconds = 10
|
|
min_candidates = 2
|
|
|
|
if not enabled:
|
|
_Rel.enabled = False
|
|
if min_candidates != 2:
|
|
_Rel.min_candidates = min_candidates
|
|
|
|
class _Pinch:
|
|
enabled = True
|
|
keep_last_turns = 1
|
|
budget_tokens = 100
|
|
max_summarize_chars = 4000
|
|
relevance = _Rel()
|
|
|
|
class _CB:
|
|
enabled = False
|
|
|
|
class _Cfg:
|
|
pinch = _Pinch()
|
|
circuit_breaker = _CB()
|
|
|
|
return _Cfg()
|
|
|
|
|
|
def _embedding_data(n_vectors, dim=4):
|
|
data = []
|
|
for i in range(n_vectors):
|
|
vec = [0.0] * dim
|
|
vec[i % dim] = 1.0
|
|
data.append({"embedding": vec, "index": i})
|
|
return data
|
|
|
|
|
|
def test_embed_for_relevance_happy_path_returns_order(monkeypatch):
|
|
captured = {}
|
|
|
|
def fake_post(url, headers=None, json=None, timeout=None):
|
|
captured["json"] = json
|
|
# Return vectors: query (idx0), candidate0 = same as query (most
|
|
# relevant), candidate1 = orthogonal (least relevant).
|
|
payload = {
|
|
"data": [
|
|
{"embedding": [1.0, 0.0, 0.0, 0.0]},
|
|
{"embedding": [1.0, 0.0, 0.0, 0.0]},
|
|
{"embedding": [0.0, 1.0, 0.0, 0.0]},
|
|
]
|
|
}
|
|
return _FakeResp(status_code=200, json_payload=payload)
|
|
|
|
monkeypatch.setattr(dispatcher.requests, "post", fake_post)
|
|
order = dispatcher._embed_for_relevance("hi", ["a", "b"], _cfg_with_relevance())
|
|
# candidate1 (b) is least relevant, so it is compressed first -> index 1.
|
|
assert order == [1, 0]
|
|
assert captured["json"]["model"] == "nomic-embed-text"
|
|
|
|
|
|
def test_embed_for_relevance_failure_paths_return_none(monkeypatch):
|
|
cases = [
|
|
_FakeResp(status_code=500, text="err"), # non-200
|
|
_FakeResp(status_code=200), # unparseable (no json)
|
|
_FakeResp(status_code=200, json_payload={"data": None}), # malformed
|
|
_FakeResp(status_code=200, json_payload={"data": [{"embedding": []}]}),
|
|
]
|
|
for resp in cases:
|
|
monkeypatch.setattr(dispatcher.requests, "post", lambda *a, _r=resp, **k: _r)
|
|
assert dispatcher._embed_for_relevance("q", ["a"], _cfg_with_relevance()) is None
|
|
|
|
|
|
def test_embed_for_relevance_request_exception_returns_none(monkeypatch):
|
|
def boom(*a, **k):
|
|
raise dispatcher.requests.RequestException("timeout")
|
|
|
|
monkeypatch.setattr(dispatcher.requests, "post", boom)
|
|
assert dispatcher._embed_for_relevance("q", ["a"], _cfg_with_relevance()) is None
|
|
|
|
|
|
def test_relevance_order_for_returns_none_when_disabled():
|
|
cfg = _cfg_with_relevance(enabled=False)
|
|
assert dispatcher._relevance_order_for([], cfg, budget_tokens=100) is None
|
|
|
|
|
|
def test_relevance_order_for_below_min_candidates_skips_embedding(monkeypatch):
|
|
called = {"n": 0}
|
|
|
|
def fake_post(*a, **k):
|
|
called["n"] += 1
|
|
return _FakeResp(status_code=200, json_payload={"data": None})
|
|
|
|
monkeypatch.setattr(dispatcher.requests, "post", fake_post)
|
|
cfg = _cfg_with_relevance(enabled=True, min_candidates=2)
|
|
# One candidate only -> below min_candidates -> no embed call.
|
|
messages = [
|
|
{"role": "user", "content": "q"},
|
|
{"role": "tool", "name": "read", "content": "x" * 6000},
|
|
{"role": "assistant", "content": "a"},
|
|
]
|
|
assert dispatcher._relevance_order_for(messages, cfg, budget_tokens=100) is None
|
|
assert called["n"] == 0
|
|
|
|
|
|
def test_relevance_order_for_skips_embedding_when_under_budget(monkeypatch):
|
|
called = {"n": 0}
|
|
|
|
def fake_post(*a, **k):
|
|
called["n"] += 1
|
|
return _FakeResp(status_code=200, json_payload={"data": None})
|
|
|
|
monkeypatch.setattr(dispatcher.requests, "post", fake_post)
|
|
cfg = _cfg_with_relevance(enabled=True, min_candidates=2)
|
|
# Two short tool-results -> token estimate under budget -> no embed call.
|
|
messages = [
|
|
{"role": "user", "content": "q"},
|
|
{"role": "tool", "name": "read", "content": "short"},
|
|
{"role": "tool", "name": "read", "content": "also short"},
|
|
{"role": "assistant", "content": "a"},
|
|
]
|
|
assert dispatcher._relevance_order_for(messages, cfg, budget_tokens=50000) is None
|
|
assert called["n"] == 0
|
|
|
|
|
|
def test_open_circuits_empty_when_disabled():
|
|
cfg = _cfg_with_relevance()
|
|
cfg.circuit_breaker.enabled = False
|
|
rows = [{"model_id": "m1", "provider": "p1"}]
|
|
assert dispatcher._open_circuits(rows, cfg) == set()
|
|
|
|
|
|
def test_open_circuits_lists_down_models(monkeypatch):
|
|
cfg = _cfg_with_relevance()
|
|
cfg.circuit_breaker.enabled = True
|
|
circuit_breaker.clear()
|
|
try:
|
|
monkeypatch.setattr(circuit_breaker.time, "time", lambda: 1000.0)
|
|
circuit_breaker.record_failure("m1", "p1", 30, 600, 2.0)
|
|
rows = [
|
|
{"model_id": "m1", "provider": "p1"}, # down
|
|
{"model_id": "m2", "provider": "p1"}, # fine
|
|
]
|
|
assert dispatcher._open_circuits(rows, cfg) == {"m1"}
|
|
finally:
|
|
circuit_breaker.clear()
|
|
|
|
|
|
def test_open_upstream_posts_stream_with_candidate_model(monkeypatch):
|
|
captured = {}
|
|
|
|
def fake_post(url, headers=None, json=None, stream=None, timeout=None):
|
|
captured["model"] = json["model"]
|
|
captured["stream"] = stream
|
|
return _FakeResp(status_code=200)
|
|
|
|
monkeypatch.setattr(dispatcher.requests, "post", fake_post)
|
|
dispatcher._open_upstream("candidate-model", "http://x", {"authorization": "Bearer k"},
|
|
{"model": "ignore", "messages": []})
|
|
assert captured["model"] == "candidate-model"
|
|
assert captured["stream"] is True
|