aboutsummaryrefslogtreecommitdiffstats
path: root/test_llamachat.py
diff options
context:
space:
mode:
Diffstat (limited to 'test_llamachat.py')
-rwxr-xr-xtest_llamachat.py122
1 files changed, 97 insertions, 25 deletions
diff --git a/test_llamachat.py b/test_llamachat.py
index 0251c78..1217176 100755
--- a/test_llamachat.py
+++ b/test_llamachat.py
@@ -14,6 +14,7 @@
# GNU General Public License for more details.
"""Self-checks for the non-GUI logic. Run: ./test_llamachat.py"""
+import contextlib
import json
import os
import subprocess
@@ -1849,10 +1850,7 @@ def test_client_auth_header():
return _HttpxResponse({"data": [{"id": "m1"}]})
import httpx
- original = httpx.get
- try:
- httpx.get = _recorder
-
+ with _patched(httpx, "get", _recorder):
assert backend.Client("http://x.example.org").models() == ["m1"]
assert "Authorization" not in sent["headers"]
@@ -1860,8 +1858,6 @@ def test_client_auth_header():
"http://x.example.org", api_key="sk-test-not-a-real-key"
).models()
assert sent["headers"]["Authorization"] == "Bearer sk-test-not-a-real-key"
- finally:
- httpx.get = original
# The streaming path is the one that carries every real turn, and no
# other test reaches it: the search tests all stub _stream_once out.
@@ -1881,10 +1877,7 @@ def test_client_auth_header():
sent["headers"] = headers or {}
return _Stream()
- original_stream = httpx.stream
- try:
- httpx.stream = _stream_recorder
-
+ with _patched(httpx, "stream", _stream_recorder):
client = backend.Client("http://x.example.org")
assert list(client._stream_once("m", [], tools=None)) == [("content", "hi")]
assert "Authorization" not in sent["headers"]
@@ -1894,14 +1887,91 @@ def test_client_auth_header():
)
list(keyed._stream_once("m", [], tools=None))
assert sent["headers"]["Authorization"] == "Bearer sk-test-not-a-real-key"
- finally:
- httpx.stream = original_stream
# The key is not on the client's repr, which reaches logs and tracebacks.
assert "sk-test-not-a-real-key" not in repr(keyed)
print("ok client authorization header")
+def test_multi_client():
+ """Models fan out across providers; requests route by model id."""
+ from llamachat import providers
+
+ table = providers.parse(
+ {
+ "providers": {
+ "local": {"base_url": "http://localhost:8181"},
+ "together": {
+ "base_url": "https://api.example.org",
+ "api_key": "env:MULTI_TEST_KEY",
+ "filter": ["qwen"],
+ },
+ "down": {"base_url": "https://dead.example.org"},
+ }
+ }
+ )
+ os.environ["MULTI_TEST_KEY"] = "sk-test-not-a-real-key"
+
+ listings = {
+ "http://localhost:8181": ["gemma4", "qwen3.5-9b"],
+ "https://api.example.org": [
+ "Qwen/Qwen2.5-72B",
+ "meta-llama/Llama-3.3-70B",
+ ],
+ }
+ built = []
+
+ class _StubClient:
+ def __init__(self, base_url, timeout=300, api_key=""):
+ self.base_url = base_url
+ self.api_key = api_key
+ built.append(self)
+
+ def models(self, list_timeout=30):
+ if self.base_url not in listings:
+ raise backend.BackendError(f"cannot reach {self.base_url}")
+ return listings[self.base_url]
+
+ multi = backend.MultiClient(
+ table, timeout=300, resolver=providers.KeyResolver(),
+ client_factory=_StubClient,
+ )
+ listed, problems = multi.models()
+
+ # Local models stay bare, cloud ones are prefixed, and the filter cut
+ # the Llama model out of together's listing.
+ assert listed == ["gemma4", "qwen3.5-9b", "together:Qwen/Qwen2.5-72B"]
+
+ # The unreachable provider is reported, and did not break the rest.
+ assert any("down" in p for p in problems)
+
+ # Routing: the client for a cloud model carries that provider's key.
+ client = multi.client_for("together:Qwen/Qwen2.5-72B")
+ assert client.base_url == "https://api.example.org"
+ assert client.api_key == "sk-test-not-a-real-key"
+
+ # And a local model gets the local client with no key at all.
+ local = multi.client_for("gemma4")
+ assert local.base_url == "http://localhost:8181"
+ assert local.api_key == ""
+
+ # The bare model name is what goes on the wire, not the prefixed id.
+ assert multi.wire_name("together:Qwen/Qwen2.5-72B") == "Qwen/Qwen2.5-72B"
+ assert multi.wire_name("gemma4") == "gemma4"
+
+ # A filter that matches nothing is reported by name with counts.
+ table["together"].filter = ["zzz"]
+ empty = backend.MultiClient(
+ table, timeout=300, resolver=providers.KeyResolver(),
+ client_factory=_StubClient,
+ )
+ _, notes = empty.models()
+ assert any("together: 0 of 2" in n for n in notes)
+
+ del os.environ["MULTI_TEST_KEY"]
+ print("ok multi-provider client")
+
+
class _FakeResponse:
"""Enough of an http.client response for urlopen's context manager."""
@@ -1940,6 +2010,17 @@ def _with_urlopen(body, capture=None):
return original
+@contextlib.contextmanager
+def _patched(obj, name, value):
+ """Swap an attribute for the duration of the block, restored even on error."""
+ original = getattr(obj, name)
+ setattr(obj, name, value)
+ try:
+ yield
+ finally:
+ setattr(obj, name, original)
+
+
def test_search_tool_schema():
from llamachat import search
@@ -2545,13 +2626,9 @@ def test_title_request():
import httpx
- original = httpx.post
- httpx.post = fake_post
- try:
+ with _patched(httpx, "post", fake_post):
client = backend.Client("http://router:8181")
out = client.complete("m", [{"role": "user", "content": "hi"}], max_tokens=32)
- finally:
- httpx.post = original
assert out == " A Title ", out
assert seen["url"] == "http://router:8181/v1/chat/completions"
@@ -2567,12 +2644,9 @@ def test_title_request():
# A keyed provider authenticates on this path too, not only on models().
# It runs after the body assertions above because it overwrites `seen`.
- httpx.post = fake_post
- try:
+ with _patched(httpx, "post", fake_post):
keyed = backend.Client("http://router:8181", api_key="sk-test-not-a-real-key")
assert keyed.complete("m", []) == " A Title "
- finally:
- httpx.post = original
assert seen["headers"]["Authorization"] == "Bearer sk-test-not-a-real-key"
# A reply with no choices is empty rather than an exception.
@@ -2580,11 +2654,8 @@ def test_title_request():
def json(self):
return {}
- httpx.post = lambda url, json=None, timeout=None, headers=None: Empty()
- try:
+ with _patched(httpx, "post", lambda url, json=None, timeout=None, headers=None: Empty()):
assert backend.Client("http://x").complete("m", []) == ""
- finally:
- httpx.post = original
print("ok title request")
@@ -2662,6 +2733,7 @@ if __name__ == "__main__":
test_metadata_and_cost()
test_token_column_migration()
test_client_auth_header()
+ test_multi_client()
test_search_tool_schema()
test_search_results_sanitising()
test_tool_call_accumulation()