Coverage for agentos/llm/tests/test_providers.py: 0%
170 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-09 07:12 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-09 07:12 +0800
1"""LLM Provider 模块单元测试 — v1.3.36。
2测试范围: factory, base types, Function Calling, DeepSeek, Anthropic (unit/mock)。
3"""
5import os
6from unittest.mock import patch
8import pytest
10from agentos.llm import (
11 AnthropicProvider,
12 CompletionUsage,
13 DeepSeekProvider,
14 LLMProvider,
15 Message,
16 MessageRole,
17 OpenAIProvider,
18 StreamChunk,
19 TokenUsage,
20 Tool,
21 ToolCall,
22 ToolParameter,
23 create_provider,
24)
26# ── Base Types ───────────────────────────────────────────────────
29class TestTokenUsage:
30 def test_defaults(self):
31 u = TokenUsage()
32 assert u.prompt_tokens == 0
33 assert u.completion_tokens == 0
34 assert u.total_tokens == 0
36 def test_values(self):
37 u = TokenUsage(prompt_tokens=100, completion_tokens=50, total_tokens=150)
38 assert u.prompt_tokens == 100
41class TestCompletionUsage:
42 def test_cost_default(self):
43 u = CompletionUsage(prompt_tokens=500, completion_tokens=200, total_tokens=700)
44 assert u.cost_usd == 0.0
47class TestMessage:
48 def test_basic(self):
49 m = Message(role=MessageRole.USER, content="hi")
50 d = m.as_dict()
51 assert d["role"] == "user"
52 assert d["content"] == "hi"
54 def test_with_tool_call_id(self):
55 m = Message(role=MessageRole.TOOL, content="result", tool_call_id="call_123")
56 d = m.as_dict()
57 assert d["tool_call_id"] == "call_123"
59 def test_with_tool_calls(self):
60 m = Message(
61 role=MessageRole.ASSISTANT,
62 content="",
63 tool_calls=[ToolCall(id="tc1", name="get_weather", arguments='{"city":"NYC"}')],
64 )
65 assert m.tool_calls[0].name == "get_weather"
68# ── Tool / Function Calling ───────────────────────────────────────
71class TestToolParameter:
72 def test_basic_schema(self):
73 p = ToolParameter(type="string", description="City name", required=True)
74 s = p.as_schema()
75 assert s["type"] == "string"
76 assert s["description"] == "City name"
78 def test_with_enum(self):
79 p = ToolParameter(type="string", enum=["celsius", "fahrenheit"])
80 s = p.as_schema()
81 assert s["enum"] == ["celsius", "fahrenheit"]
84class TestTool:
85 def test_from_function(self):
86 t = Tool.from_function(
87 "get_weather",
88 "Get weather for a city",
89 {
90 "city": ToolParameter(type="string", description="City", required=True),
91 "unit": ToolParameter(type="string", enum=["celsius", "fahrenheit"]),
92 },
93 required=["city"],
94 )
95 schema = t.as_schema()
96 assert schema["type"] == "function"
97 fn = schema["function"]
98 assert fn["name"] == "get_weather"
99 assert fn["parameters"]["required"] == ["city"]
100 assert "city" in fn["parameters"]["properties"]
102 def test_to_openai_format(self):
103 t = Tool.from_function(
104 "search",
105 "Web search",
106 {
107 "query": ToolParameter(type="string", description="Query", required=True),
108 },
109 )
110 schema = t.as_schema()
111 assert schema["function"]["name"] == "search"
112 props = schema["function"]["parameters"]["properties"]
113 assert props["query"]["type"] == "string"
116class TestToolCall:
117 def test_create_and_parse(self):
118 tc = ToolCall(id="call_1", name="add", arguments='{"a":1,"b":2}')
119 assert tc.id == "call_1"
120 assert tc.name == "add"
121 assert tc.parsed_arguments == {"a": 1, "b": 2}
123 def test_empty_arguments(self):
124 tc = ToolCall(id="x", name="ping", arguments="{}")
125 assert tc.parsed_arguments == {}
128# ── StreamChunk ────────────────────────────────────────────────────
131class TestStreamChunk:
132 def test_defaults(self):
133 c = StreamChunk()
134 assert c.content == ""
135 assert c.finish_reason is None
137 def test_with_content(self):
138 c = StreamChunk(content="hello")
139 assert c.content == "hello"
142# ── Factory ────────────────────────────────────────────────────────
145class TestCreateProvider:
146 def test_openai_default(self):
147 with patch.dict(os.environ, {"OPENAI_API_KEY": "sk-test"}, clear=True):
148 p = create_provider("openai")
149 assert p.provider_name == "openai"
150 assert p.model == "gpt-4o-mini"
152 def test_deepseek_default(self):
153 with patch.dict(os.environ, {"DEEPSEEK_API_KEY": "sk-ds"}, clear=True):
154 p = create_provider("deepseek")
155 assert p.provider_name == "deepseek"
156 assert p.model == "deepseek-chat"
157 assert "deepseek.com" in p.base_url
159 def test_anthropic_default(self):
160 with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-ant"}, clear=True):
161 p = create_provider("anthropic")
162 assert p.provider_name == "anthropic"
163 assert "sonnet" in p.model.lower()
165 def test_unknown_provider(self):
166 with pytest.raises(ValueError, match="Unknown provider"):
167 create_provider("nonexistent")
169 def test_api_key_env_openai(self):
170 with patch.dict(os.environ, {"OPENAI_API_KEY": "sk-env-test"}, clear=True):
171 p = create_provider("openai")
172 assert p.api_key == "sk-env-test"
174 def test_custom_model(self):
175 with patch.dict(os.environ, {"OPENAI_API_KEY": "sk-test"}, clear=True):
176 p = create_provider("openai", model="gpt-4o")
177 assert p.model == "gpt-4o"
180# ── DeepSeek Provider ──────────────────────────────────────────────
183class TestDeepSeekProvider:
184 def test_is_openai_subclass(self):
185 p = DeepSeekProvider(api_key="sk-ds")
186 assert isinstance(p, OpenAIProvider)
187 assert isinstance(p, LLMProvider)
189 def test_provider_name(self):
190 p = DeepSeekProvider(api_key="sk-ds")
191 assert p.provider_name == "deepseek"
193 def test_default_base_url(self):
194 p = DeepSeekProvider(api_key="sk-ds")
195 assert p.base_url == "https://api.deepseek.com/v1"
197 def test_custom_base_url(self):
198 p = DeepSeekProvider(api_key="sk-ds", base_url="http://localhost:8080/v1")
199 assert p.base_url == "http://localhost:8080/v1"
201 def test_default_model(self):
202 p = DeepSeekProvider(api_key="sk-ds")
203 assert p.model == "deepseek-chat"
205 def test_factory_creates(self):
206 with patch.dict(os.environ, {"DEEPSEEK_API_KEY": "sk-ds"}, clear=True):
207 p = create_provider("deepseek")
208 assert isinstance(p, DeepSeekProvider)
209 assert p.provider_name == "deepseek"
212# ── Anthropic Provider ─────────────────────────────────────────────
215class TestAnthropicProvider:
216 def test_provider_name(self):
217 p = AnthropicProvider(api_key="sk-ant")
218 assert p.provider_name == "anthropic"
220 def test_default_model(self):
221 p = AnthropicProvider(api_key="sk-ant")
222 assert p.model == "claude-sonnet-4-20250514"
224 def test_default_base_url(self):
225 p = AnthropicProvider(api_key="sk-ant")
226 assert "api.anthropic.com" in p.base_url
228 def test_custom_base_url(self):
229 p = AnthropicProvider(api_key="sk-ant", base_url="http://localhost:9999")
230 assert p.base_url == "http://localhost:9999"
232 def test_headers(self):
233 p = AnthropicProvider(api_key="sk-ant-test")
234 h = p._headers()
235 assert h["x-api-key"] == "sk-ant-test"
236 assert h["anthropic-version"] == "2023-06-01"
238 def test_tools_conversion(self):
239 AnthropicProvider(api_key="sk-ant")
240 tools = [
241 Tool.from_function(
242 "get_weather",
243 "Get weather",
244 {
245 "city": ToolParameter(type="string", description="City", required=True),
246 },
247 ),
248 ]
249 from agentos.llm.anthropic_provider import _tools_to_anthropic
251 result = _tools_to_anthropic(tools)
252 assert len(result) == 1
253 assert result[0]["name"] == "get_weather"
254 assert result[0]["input_schema"]["type"] == "object"
256 def test_message_conversion_simple(self):
257 from agentos.llm.anthropic_provider import _messages_to_anthropic
259 msgs = [Message(role=MessageRole.USER, content="Hello")]
260 system, api_msgs = _messages_to_anthropic(msgs)
261 assert system is None
262 assert len(api_msgs) == 1
263 assert api_msgs[0]["role"] == "user"
264 assert api_msgs[0]["content"] == "Hello"
266 def test_message_conversion_with_system(self):
267 from agentos.llm.anthropic_provider import _messages_to_anthropic
269 msgs = [
270 Message(role=MessageRole.SYSTEM, content="You are helpful."),
271 Message(role=MessageRole.USER, content="Hi"),
272 ]
273 system, api_msgs = _messages_to_anthropic(msgs)
274 assert system == "You are helpful."
275 assert len(api_msgs) == 1
276 assert api_msgs[0]["role"] == "user"
278 def test_build_body_includes_tools(self):
279 p = AnthropicProvider(api_key="sk-ant")
280 tools = [
281 Tool.from_function(
282 "search",
283 "search the web",
284 {
285 "q": ToolParameter(type="string", description="query", required=True),
286 },
287 )
288 ]
289 body = p._build_body(
290 [Message(role=MessageRole.USER, content="test")],
291 temperature=0.5,
292 max_tokens=100,
293 top_p=0.9,
294 stop=None,
295 tools=tools,
296 tool_choice="auto",
297 )
298 assert body["model"] == "claude-sonnet-4-20250514"
299 assert "tools" in body
300 assert body["tools"][0]["name"] == "search"