fischer-agentkit/tests/unit/test_llm_provider.py

217 lines
7.9 KiB
Python

"""LLM Provider (OpenAI Compatible) 测试"""
import json
import pytest
from pytest_httpx import HTTPXMock
from agentkit.core.exceptions import LLMProviderError
from agentkit.llm.protocol import LLMRequest, LLMResponse, TokenUsage
from agentkit.llm.providers.openai import OpenAICompatibleProvider
class TestOpenAICompatibleProviderBasic:
"""基本 chat 功能测试"""
async def test_chat_returns_llm_response(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="https://api.openai.com/v1/chat/completions",
json={
"id": "chatcmpl-123",
"model": "gpt-4o-mini",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "Hello! How can I help?"},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 10, "completion_tokens": 6, "total_tokens": 16},
},
)
provider = OpenAICompatibleProvider(api_key="test-key")
request = LLMRequest(
messages=[{"role": "user", "content": "Hi"}],
model="gpt-4o-mini",
)
response = await provider.chat(request)
assert isinstance(response, LLMResponse)
assert response.content == "Hello! How can I help?"
assert response.model == "gpt-4o-mini"
assert response.usage.prompt_tokens == 10
assert response.usage.completion_tokens == 6
assert response.usage.total_tokens == 16
async def test_chat_with_custom_base_url(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="https://api.deepseek.com/v1/chat/completions",
json={
"id": "chatcmpl-456",
"model": "deepseek-chat",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "DeepSeek response"},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 5, "completion_tokens": 3, "total_tokens": 8},
},
)
provider = OpenAICompatibleProvider(
api_key="test-key",
base_url="https://api.deepseek.com/v1",
default_model="deepseek-chat",
)
request = LLMRequest(
messages=[{"role": "user", "content": "Hi"}],
model="deepseek-chat",
)
response = await provider.chat(request)
assert response.content == "DeepSeek response"
assert response.model == "deepseek-chat"
async def test_timeout_parameter_passed_to_httpx_client(self):
"""Verify that the timeout parameter is passed to the httpx client."""
provider = OpenAICompatibleProvider(
api_key="test-key",
base_url="https://api.openai.com/v1",
timeout=180.0,
)
# httpx stores timeout config on the client
assert provider._client.timeout.read == 180.0
await provider.close()
async def test_default_timeout_is_120s(self):
"""Verify that the default timeout is 120s (not the old hardcoded 60s)."""
provider = OpenAICompatibleProvider(api_key="test-key", base_url="https://api.openai.com/v1")
assert provider._client.timeout.read == 120.0
await provider.close()
class TestOpenAICompatibleProviderToolCalls:
"""Function Calling (tool_calls) 测试"""
async def test_response_contains_tool_calls(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="https://api.openai.com/v1/chat/completions",
json={
"id": "chatcmpl-789",
"model": "gpt-4o",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_abc",
"type": "function",
"function": {
"name": "get_weather",
"arguments": '{"city": "Beijing"}',
},
}
],
},
"finish_reason": "tool_calls",
}
],
"usage": {"prompt_tokens": 20, "completion_tokens": 15, "total_tokens": 35},
},
)
provider = OpenAICompatibleProvider(api_key="test-key")
request = LLMRequest(
messages=[{"role": "user", "content": "What's the weather in Beijing?"}],
model="gpt-4o",
tools=[
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
},
},
}
],
)
response = await provider.chat(request)
assert response.has_tool_calls is True
assert len(response.tool_calls) == 1
assert response.tool_calls[0].id == "call_abc"
assert response.tool_calls[0].name == "get_weather"
assert response.tool_calls[0].arguments == {"city": "Beijing"}
async def test_response_without_tool_calls(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="https://api.openai.com/v1/chat/completions",
json={
"id": "chatcmpl-101",
"model": "gpt-4o-mini",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "Just a text response"},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 5, "completion_tokens": 5, "total_tokens": 10},
},
)
provider = OpenAICompatibleProvider(api_key="test-key")
request = LLMRequest(
messages=[{"role": "user", "content": "Hello"}],
model="gpt-4o-mini",
)
response = await provider.chat(request)
assert response.has_tool_calls is False
assert response.content == "Just a text response"
class TestOpenAICompatibleProviderErrors:
"""API 错误处理测试"""
async def test_api_error_raises_provider_error(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="https://api.openai.com/v1/chat/completions",
status_code=401,
json={"error": {"message": "Invalid API key", "type": "invalid_request_error"}},
)
provider = OpenAICompatibleProvider(api_key="bad-key")
request = LLMRequest(
messages=[{"role": "user", "content": "Hi"}],
model="gpt-4o-mini",
)
with pytest.raises(LLMProviderError):
await provider.chat(request)
async def test_api_rate_limit_raises_provider_error(self, httpx_mock: HTTPXMock):
httpx_mock.add_response(
url="https://api.openai.com/v1/chat/completions",
status_code=429,
json={"error": {"message": "Rate limit exceeded", "type": "rate_limit_error"}},
)
provider = OpenAICompatibleProvider(api_key="test-key")
request = LLMRequest(
messages=[{"role": "user", "content": "Hi"}],
model="gpt-4o-mini",
)
with pytest.raises(LLMProviderError):
await provider.chat(request)