110 lines
3.6 KiB
Python
110 lines
3.6 KiB
Python
"""Unit tests for the Anthropic LLM plugin that do not require a real API key or network."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from livekit.agents import APIConnectOptions, llm
|
|
from livekit.plugins.anthropic.llm import LLMStream
|
|
|
|
pytestmark = pytest.mark.plugin("anthropic")
|
|
|
|
|
|
def _make_llm(**kwargs):
|
|
from livekit.plugins.anthropic import LLM
|
|
|
|
return LLM(api_key="sk-ant-test", **kwargs)
|
|
|
|
|
|
class TestHttpxTimeoutDefaults:
|
|
def test_default_read_timeout_is_generous(self) -> None:
|
|
"""Default read timeout must accommodate adaptive-thinking pauses (≥30 s)."""
|
|
llm = _make_llm()
|
|
read = llm._client._client.timeout.read
|
|
assert read >= 30.0, f"read timeout {read}s is too short for adaptive thinking"
|
|
|
|
def test_default_connect_timeout_remains_tight(self) -> None:
|
|
"""Connect timeout should stay short so genuine connection failures surface fast."""
|
|
llm = _make_llm()
|
|
connect = llm._client._client.timeout.connect
|
|
assert connect <= 10.0, f"connect timeout {connect}s is unexpectedly long"
|
|
|
|
def test_default_timeout_is_split(self) -> None:
|
|
"""Default must be an httpx.Timeout object, not a flat scalar."""
|
|
llm = _make_llm()
|
|
t = llm._client._client.timeout
|
|
assert isinstance(t, httpx.Timeout)
|
|
assert t.read != t.connect, "read and connect timeouts should differ in the default"
|
|
|
|
|
|
class TestHttpxTimeoutCustom:
|
|
def test_custom_timeout_honored(self) -> None:
|
|
"""A caller-supplied httpx.Timeout is passed through to the httpx client."""
|
|
custom = httpx.Timeout(3.0, read=120.0)
|
|
llm = _make_llm(timeout=custom)
|
|
t = llm._client._client.timeout
|
|
assert t.read == 120.0
|
|
assert t.connect == 3.0
|
|
|
|
def test_none_timeout_uses_default(self) -> None:
|
|
"""Passing timeout=None must fall back to the built-in default."""
|
|
llm = _make_llm(timeout=None)
|
|
assert llm._client._client.timeout.read >= 30.0
|
|
|
|
def test_explicit_client_bypasses_timeout_param(self) -> None:
|
|
"""When a pre-built client= is supplied, timeout= is ignored (client wins)."""
|
|
import anthropic
|
|
|
|
tight_client = anthropic.AsyncClient(
|
|
api_key="sk-ant-test",
|
|
http_client=httpx.AsyncClient(timeout=httpx.Timeout(1.0)),
|
|
)
|
|
# timeout= argument should have no effect here
|
|
llm = _make_llm(client=tight_client, timeout=httpx.Timeout(5.0, read=999.0))
|
|
assert llm._client._client.timeout.read == 1.0
|
|
|
|
|
|
class _EmptyAnthropicStream:
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, exc_type, exc, tb):
|
|
return None
|
|
|
|
def __aiter__(self):
|
|
return self
|
|
|
|
async def __anext__(self):
|
|
raise StopAsyncIteration
|
|
|
|
|
|
class TestAnthropicStreamRetry:
|
|
@pytest.mark.asyncio
|
|
async def test_retry_creates_a_fresh_stream_awaitable(self) -> None:
|
|
calls = 0
|
|
|
|
async def failing_stream():
|
|
raise RuntimeError("transient connect failure")
|
|
|
|
async def empty_stream():
|
|
return _EmptyAnthropicStream()
|
|
|
|
def create_stream():
|
|
nonlocal calls
|
|
calls += 1
|
|
return failing_stream() if calls == 1 else empty_stream()
|
|
|
|
stream = LLMStream(
|
|
_make_llm(),
|
|
create_anthropic_stream=create_stream,
|
|
chat_ctx=llm.ChatContext.empty(),
|
|
tools=[],
|
|
conn_options=APIConnectOptions(max_retry=1, retry_interval=0),
|
|
)
|
|
|
|
response = await stream.collect()
|
|
|
|
assert calls == 2
|
|
assert response.usage is not None
|