Files
2026-07-26 17:40:51 +08:00

204 lines
6.4 KiB
Python

from __future__ import annotations
import json
from collections.abc import Awaitable, Callable
import httpx
import pytest
from agent_platform.config import Settings
from agent_platform.runtime.provider import ModelProvider
@pytest.fixture(autouse=True)
def disable_retry_sleep(monkeypatch: pytest.MonkeyPatch) -> None:
async def no_sleep(_: float) -> None:
return None
monkeypatch.setattr("agent_platform.runtime.provider.asyncio.sleep", no_sleep)
async def _complete_with_handler(
settings: Settings,
handler: Callable[[httpx.Request], httpx.Response | Awaitable[httpx.Response]],
**kwargs,
) -> dict:
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
provider = ModelProvider(settings, client)
return await provider.complete(
model="test-model",
messages=[{"role": "user", "content": "hello"}],
**kwargs,
)
async def test_complete_retries_transient_gateway_response(settings: Settings) -> None:
calls = 0
def handler(request: httpx.Request) -> httpx.Response:
nonlocal calls
calls += 1
if calls == 1:
return httpx.Response(524, request=request, headers={"Retry-After": "0"})
return httpx.Response(
200,
request=request,
json={"choices": [{"message": {"role": "assistant", "content": "ok"}}]},
)
result = await _complete_with_handler(settings, handler)
assert calls == 2
assert result["choices"][0]["message"]["content"] == "ok"
async def test_complete_retries_transport_error(settings: Settings) -> None:
calls = 0
def handler(request: httpx.Request) -> httpx.Response:
nonlocal calls
calls += 1
if calls == 1:
raise httpx.ConnectError("temporary disconnect", request=request)
return httpx.Response(
200,
request=request,
json={"choices": [{"message": {"role": "assistant", "content": "recovered"}}]},
)
result = await _complete_with_handler(settings, handler)
assert calls == 2
assert result["choices"][0]["message"]["content"] == "recovered"
async def test_complete_does_not_retry_client_error(settings: Settings) -> None:
calls = 0
def handler(request: httpx.Request) -> httpx.Response:
nonlocal calls
calls += 1
return httpx.Response(400, request=request, json={"error": "bad request"})
with pytest.raises(httpx.HTTPStatusError):
await _complete_with_handler(settings, handler)
assert calls == 1
async def test_complete_aggregates_streamed_tool_call(settings: Settings) -> None:
chunks = [
{
"id": "completion-1",
"object": "chat.completion.chunk",
"created": 123,
"model": "test-model",
"choices": [
{
"index": 0,
"delta": {
"role": "assistant",
"tool_calls": [
{
"index": 0,
"id": "call-1",
"type": "function",
"function": {"name": "ping", "arguments": '{"val'},
}
],
},
"finish_reason": None,
}
],
},
{
"choices": [
{
"index": 0,
"delta": {
"tool_calls": [
{
"index": 0,
"function": {"arguments": 'ue":"ok"}'},
}
]
},
"finish_reason": None,
}
]
},
{
"choices": [
{
"index": 0,
"delta": {},
"finish_reason": "tool_calls",
}
],
"usage": {"prompt_tokens": 4, "completion_tokens": 3, "total_tokens": 7},
},
]
body = "".join(f"data: {json.dumps(chunk)}\n\n" for chunk in chunks) + "data: [DONE]\n\n"
def handler(request: httpx.Request) -> httpx.Response:
request_payload = json.loads(request.content)
assert request_payload["stream"] is True
assert request_payload["max_tokens"] == 4096
return httpx.Response(
200,
request=request,
headers={"Content-Type": "text/event-stream"},
content=body,
)
result = await _complete_with_handler(settings, handler)
choice = result["choices"][0]
assert choice["finish_reason"] == "tool_calls"
assert choice["message"]["tool_calls"] == [
{
"id": "call-1",
"type": "function",
"function": {"name": "ping", "arguments": '{"value":"ok"}'},
}
]
assert result["usage"]["total_tokens"] == 7
async def test_deepseek_route_enables_thinking_and_uses_separate_credentials(settings: Settings) -> None:
body = (
'data: {"choices":[{"index":0,"delta":{"role":"assistant",'
'"reasoning_content":"private state","tool_calls":[{"index":0,"id":"call-1",'
'"type":"function","function":{"name":"ping","arguments":"{}"}}]},'
'"finish_reason":"tool_calls"}]}\n\n'
"data: [DONE]\n\n"
)
def handler(request: httpx.Request) -> httpx.Response:
assert str(request.url) == "https://api.deepseek.example.test/v1/chat/completions"
assert request.headers["Authorization"] == "Bearer deepseek-secret"
payload = json.loads(request.content)
assert payload["model"] == "test-model"
assert payload["thinking"] == {"type": "enabled"}
assert payload["reasoning_effort"] == "max"
assert payload["max_tokens"] == 16_384
return httpx.Response(
200,
request=request,
headers={"Content-Type": "text/event-stream"},
content=body,
)
result = await _complete_with_handler(
settings,
handler,
provider="deepseek",
thinking_enabled=True,
reasoning_effort="max",
max_output_tokens=16_384,
)
message = result["choices"][0]["message"]
assert message["reasoning_content"] == "private state"
assert message["tool_calls"][0]["function"]["name"] == "ping"