Files
2026-07-13 13:12:33 +08:00

298 lines
9.6 KiB
Python

from __future__ import annotations
import asyncio
import json
from typing import Any
import httpx
from opensquilla.provider.ollama import _OLLAMA_DEFAULT_NUM_CTX, OllamaProvider
from opensquilla.provider.selector import ProviderConfig, _build_provider
from opensquilla.provider.types import (
ChatConfig,
ContentBlockImage,
ContentBlockText,
ContentBlockToolResult,
ContentBlockToolUse,
DoneEvent,
Message,
TextDeltaEvent,
ToolDefinition,
ToolInputSchema,
ToolUseEndEvent,
)
def _ndjson(*chunks: dict[str, Any]) -> bytes:
return b"".join((json.dumps(chunk) + "\n").encode() for chunk in chunks)
_DEFAULT_BODY = _ndjson(
{"model": "llama3", "message": {"role": "assistant", "content": "ok"}},
{
"model": "llama3",
"message": {"role": "assistant", "content": ""},
"done": True,
"prompt_eval_count": 3,
"eval_count": 2,
},
)
def _patch_stream(monkeypatch: Any, captured: dict[str, Any], body: bytes = _DEFAULT_BODY) -> None:
def handler(request: httpx.Request) -> httpx.Response:
captured["url"] = str(request.url)
captured["headers"] = request.headers
captured["payload"] = json.loads(request.content.decode("utf-8"))
return httpx.Response(200, content=body)
transport = httpx.MockTransport(handler)
real_async_client = httpx.AsyncClient
def patched_async_client(*args: Any, **kwargs: Any) -> httpx.AsyncClient:
kwargs["transport"] = transport
return real_async_client(*args, **kwargs)
monkeypatch.setattr("opensquilla.provider.ollama.httpx.AsyncClient", patched_async_client)
def _collect(
provider: OllamaProvider,
messages: list[Message],
cfg: ChatConfig | None = None,
tools: list[ToolDefinition] | None = None,
) -> list[Any]:
async def _run() -> list[Any]:
return [
event
async for event in provider.chat(messages, tools=tools, config=cfg or ChatConfig())
]
return asyncio.run(_run())
def test_tool_use_replayed_as_tool_calls_and_correlated_tool_message(monkeypatch: Any) -> None:
captured: dict[str, Any] = {}
_patch_stream(monkeypatch, captured)
provider = OllamaProvider(model="llama3")
messages = [
Message(role="user", content="look it up"),
Message(
role="assistant",
content=[ContentBlockToolUse(id="call_1", name="lookup", input={"q": "cache"})],
),
Message(
role="user",
content=[ContentBlockToolResult(tool_use_id="call_1", content="cache is warm")],
),
Message(role="user", content="continue"),
]
_collect(provider, messages)
msgs = captured["payload"]["messages"]
# Assistant turn keeps its tool_calls instead of being dropped.
assert msgs[1]["role"] == "assistant"
assert msgs[1]["tool_calls"] == [{"function": {"name": "lookup", "arguments": {"q": "cache"}}}]
# Tool result becomes its own tool-role message, correlated by tool_name.
assert msgs[2] == {"role": "tool", "content": "cache is warm", "tool_name": "lookup"}
assert msgs[3] == {"role": "user", "content": "continue"}
def test_parallel_tool_results_expand_to_separate_tool_messages(monkeypatch: Any) -> None:
captured: dict[str, Any] = {}
_patch_stream(monkeypatch, captured)
provider = OllamaProvider(model="llama3")
messages = [
Message(
role="assistant",
content=[
ContentBlockToolUse(id="call_a", name="foo", input={"x": 1}),
ContentBlockToolUse(id="call_b", name="bar", input={"y": 2}),
],
),
Message(
role="user",
content=[
ContentBlockToolResult(tool_use_id="call_a", content="ra"),
ContentBlockToolResult(tool_use_id="call_b", content="rb"),
],
),
]
_collect(provider, messages)
msgs = captured["payload"]["messages"]
assert msgs[0]["tool_calls"] == [
{"function": {"name": "foo", "arguments": {"x": 1}}},
{"function": {"name": "bar", "arguments": {"y": 2}}},
]
# Both results survive as distinct tool messages (the old code dropped all but one).
assert msgs[1] == {"role": "tool", "content": "ra", "tool_name": "foo"}
assert msgs[2] == {"role": "tool", "content": "rb", "tool_name": "bar"}
def test_tool_result_list_content_is_json_encoded(monkeypatch: Any) -> None:
captured: dict[str, Any] = {}
_patch_stream(monkeypatch, captured)
provider = OllamaProvider(model="llama3")
messages = [
Message(
role="user",
content=[ContentBlockToolResult(tool_use_id="call_x", content=[{"k": "v"}])],
),
]
_collect(provider, messages)
assert captured["payload"]["messages"][0]["content"] == json.dumps([{"k": "v"}])
def test_num_ctx_defaults_above_ollama_2048(monkeypatch: Any) -> None:
captured: dict[str, Any] = {}
_patch_stream(monkeypatch, captured)
provider = OllamaProvider(model="llama3")
_collect(provider, [Message(role="user", content="hi")], ChatConfig(max_tokens=512))
options = captured["payload"]["options"]
assert options["num_ctx"] == _OLLAMA_DEFAULT_NUM_CTX
assert _OLLAMA_DEFAULT_NUM_CTX > 2048
assert options["num_predict"] == 512
def test_num_ctx_is_configurable(monkeypatch: Any) -> None:
captured: dict[str, Any] = {}
_patch_stream(monkeypatch, captured)
provider = OllamaProvider(model="llama3", num_ctx=32768)
_collect(provider, [Message(role="user", content="hi")])
assert captured["payload"]["options"]["num_ctx"] == 32768
def test_bearer_header_sent_when_api_key_set(monkeypatch: Any) -> None:
captured: dict[str, Any] = {}
_patch_stream(monkeypatch, captured)
provider = OllamaProvider(
model="gpt-oss:120b-cloud", base_url="https://ollama.com", api_key="secret"
)
_collect(provider, [Message(role="user", content="hi")])
assert captured["url"] == "https://ollama.com/api/chat"
assert captured["headers"]["authorization"] == "Bearer secret"
def test_no_authorization_header_without_api_key(monkeypatch: Any) -> None:
captured: dict[str, Any] = {}
_patch_stream(monkeypatch, captured)
provider = OllamaProvider(model="llama3")
_collect(provider, [Message(role="user", content="hi")])
assert "authorization" not in captured["headers"]
def test_image_block_attached_to_message_images(monkeypatch: Any) -> None:
captured: dict[str, Any] = {}
_patch_stream(monkeypatch, captured)
provider = OllamaProvider(model="llava")
messages = [
Message(
role="user",
content=[
ContentBlockText(text="what is this"),
ContentBlockImage(source_type="base64", media_type="image/png", data="QkFTRTY0"),
],
),
]
_collect(provider, messages)
msg = captured["payload"]["messages"][0]
assert msg["content"] == "what is this"
assert msg["images"] == ["QkFTRTY0"]
def test_system_prompt_is_first_message(monkeypatch: Any) -> None:
captured: dict[str, Any] = {}
_patch_stream(monkeypatch, captured)
provider = OllamaProvider(model="llama3")
_collect(provider, [Message(role="user", content="hi")], ChatConfig(system="be brief"))
assert captured["payload"]["messages"][0] == {"role": "system", "content": "be brief"}
def test_stream_emits_text_and_done_with_usage(monkeypatch: Any) -> None:
captured: dict[str, Any] = {}
_patch_stream(monkeypatch, captured)
provider = OllamaProvider(model="llama3")
events = _collect(provider, [Message(role="user", content="hi")])
text = next(e for e in events if isinstance(e, TextDeltaEvent))
done = next(e for e in events if isinstance(e, DoneEvent))
assert text.text == "ok"
assert done.input_tokens == 3
assert done.output_tokens == 2
def test_stream_tool_call_emits_tool_events(monkeypatch: Any) -> None:
captured: dict[str, Any] = {}
body = _ndjson(
{
"model": "llama3",
"message": {
"role": "assistant",
"content": "",
"tool_calls": [{"function": {"name": "lookup", "arguments": {"q": "hi"}}}],
},
},
{
"model": "llama3",
"message": {"role": "assistant", "content": ""},
"done": True,
"prompt_eval_count": 4,
"eval_count": 1,
},
)
_patch_stream(monkeypatch, captured, body)
provider = OllamaProvider(model="llama3")
tool = ToolDefinition(
name="lookup",
description="Lookup a value.",
input_schema=ToolInputSchema(properties={"q": {"type": "string"}}, required=["q"]),
)
events = _collect(provider, [Message(role="user", content="hi")], tools=[tool])
tool_end = next(e for e in events if isinstance(e, ToolUseEndEvent))
assert tool_end.tool_name == "lookup"
assert tool_end.arguments == {"q": "hi"}
assert captured["payload"]["tools"][0]["function"]["name"] == "lookup"
def test_selector_passes_api_key_to_ollama_provider() -> None:
cfg = ProviderConfig(
provider="ollama",
model="gpt-oss:120b-cloud",
api_key="sk-test",
base_url="https://ollama.com",
)
provider = _build_provider(cfg)
assert isinstance(provider, OllamaProvider)
assert provider._headers() == {"Authorization": "Bearer sk-test"}
def test_selector_omits_api_key_for_local_ollama() -> None:
cfg = ProviderConfig(provider="ollama", model="llama3")
provider = _build_provider(cfg)
assert isinstance(provider, OllamaProvider)
assert provider._headers() == {}