Coverage for agentos/llm/tests/test_providers.py: 0%
170 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 10:59 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 10:59 +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 create_provider,
12 OpenAIProvider,
13 DeepSeekProvider,
14 AnthropicProvider,
15 LLMProvider,
16 CompletionUsage,
17 TokenUsage,
18 Message,
19 MessageRole,
20 StreamChunk,
21 Tool,
22 ToolCall,
23 ToolParameter,
24)
27# ── 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 ───────────────────────────────────────
70class TestToolParameter:
71 def test_basic_schema(self):
72 p = ToolParameter(type="string", description="City name", required=True)
73 s = p.as_schema()
74 assert s["type"] == "string"
75 assert s["description"] == "City name"
77 def test_with_enum(self):
78 p = ToolParameter(type="string", enum=["celsius", "fahrenheit"])
79 s = p.as_schema()
80 assert s["enum"] == ["celsius", "fahrenheit"]
83class TestTool:
84 def test_from_function(self):
85 t = Tool.from_function(
86 "get_weather",
87 "Get weather for a city",
88 {
89 "city": ToolParameter(type="string", description="City", required=True),
90 "unit": ToolParameter(type="string", enum=["celsius", "fahrenheit"]),
91 },
92 required=["city"],
93 )
94 schema = t.as_schema()
95 assert schema["type"] == "function"
96 fn = schema["function"]
97 assert fn["name"] == "get_weather"
98 assert fn["parameters"]["required"] == ["city"]
99 assert "city" in fn["parameters"]["properties"]
101 def test_to_openai_format(self):
102 t = Tool.from_function("search", "Web search", {
103 "query": ToolParameter(type="string", description="Query", required=True),
104 })
105 schema = t.as_schema()
106 assert schema["function"]["name"] == "search"
107 props = schema["function"]["parameters"]["properties"]
108 assert props["query"]["type"] == "string"
111class TestToolCall:
112 def test_create_and_parse(self):
113 tc = ToolCall(id="call_1", name="add", arguments='{"a":1,"b":2}')
114 assert tc.id == "call_1"
115 assert tc.name == "add"
116 assert tc.parsed_arguments == {"a": 1, "b": 2}
118 def test_empty_arguments(self):
119 tc = ToolCall(id="x", name="ping", arguments="{}")
120 assert tc.parsed_arguments == {}
123# ── StreamChunk ────────────────────────────────────────────────────
125class TestStreamChunk:
126 def test_defaults(self):
127 c = StreamChunk()
128 assert c.content == ""
129 assert c.finish_reason is None
131 def test_with_content(self):
132 c = StreamChunk(content="hello")
133 assert c.content == "hello"
136# ── Factory ────────────────────────────────────────────────────────
138class TestCreateProvider:
139 def test_openai_default(self):
140 with patch.dict(os.environ, {"OPENAI_API_KEY": "sk-test"}, clear=True):
141 p = create_provider("openai")
142 assert p.provider_name == "openai"
143 assert p.model == "gpt-4o-mini"
145 def test_deepseek_default(self):
146 with patch.dict(os.environ, {"DEEPSEEK_API_KEY": "sk-ds"}, clear=True):
147 p = create_provider("deepseek")
148 assert p.provider_name == "deepseek"
149 assert p.model == "deepseek-chat"
150 assert "deepseek.com" in p.base_url
152 def test_anthropic_default(self):
153 with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-ant"}, clear=True):
154 p = create_provider("anthropic")
155 assert p.provider_name == "anthropic"
156 assert "sonnet" in p.model.lower()
158 def test_unknown_provider(self):
159 with pytest.raises(ValueError, match="Unknown provider"):
160 create_provider("nonexistent")
162 def test_api_key_env_openai(self):
163 with patch.dict(os.environ, {"OPENAI_API_KEY": "sk-env-test"}, clear=True):
164 p = create_provider("openai")
165 assert p.api_key == "sk-env-test"
167 def test_custom_model(self):
168 with patch.dict(os.environ, {"OPENAI_API_KEY": "sk-test"}, clear=True):
169 p = create_provider("openai", model="gpt-4o")
170 assert p.model == "gpt-4o"
173# ── DeepSeek Provider ──────────────────────────────────────────────
175class TestDeepSeekProvider:
176 def test_is_openai_subclass(self):
177 p = DeepSeekProvider(api_key="sk-ds")
178 assert isinstance(p, OpenAIProvider)
179 assert isinstance(p, LLMProvider)
181 def test_provider_name(self):
182 p = DeepSeekProvider(api_key="sk-ds")
183 assert p.provider_name == "deepseek"
185 def test_default_base_url(self):
186 p = DeepSeekProvider(api_key="sk-ds")
187 assert p.base_url == "https://api.deepseek.com/v1"
189 def test_custom_base_url(self):
190 p = DeepSeekProvider(api_key="sk-ds", base_url="http://localhost:8080/v1")
191 assert p.base_url == "http://localhost:8080/v1"
193 def test_default_model(self):
194 p = DeepSeekProvider(api_key="sk-ds")
195 assert p.model == "deepseek-chat"
197 def test_factory_creates(self):
198 with patch.dict(os.environ, {"DEEPSEEK_API_KEY": "sk-ds"}, clear=True):
199 p = create_provider("deepseek")
200 assert isinstance(p, DeepSeekProvider)
201 assert p.provider_name == "deepseek"
204# ── Anthropic Provider ─────────────────────────────────────────────
206class TestAnthropicProvider:
207 def test_provider_name(self):
208 p = AnthropicProvider(api_key="sk-ant")
209 assert p.provider_name == "anthropic"
211 def test_default_model(self):
212 p = AnthropicProvider(api_key="sk-ant")
213 assert p.model == "claude-sonnet-4-20250514"
215 def test_default_base_url(self):
216 p = AnthropicProvider(api_key="sk-ant")
217 assert "api.anthropic.com" in p.base_url
219 def test_custom_base_url(self):
220 p = AnthropicProvider(api_key="sk-ant", base_url="http://localhost:9999")
221 assert p.base_url == "http://localhost:9999"
223 def test_headers(self):
224 p = AnthropicProvider(api_key="sk-ant-test")
225 h = p._headers()
226 assert h["x-api-key"] == "sk-ant-test"
227 assert h["anthropic-version"] == "2023-06-01"
229 def test_tools_conversion(self):
230 p = AnthropicProvider(api_key="sk-ant")
231 tools = [
232 Tool.from_function("get_weather", "Get weather", {
233 "city": ToolParameter(type="string", description="City", required=True),
234 }),
235 ]
236 from agentos.llm.anthropic_provider import _tools_to_anthropic
237 result = _tools_to_anthropic(tools)
238 assert len(result) == 1
239 assert result[0]["name"] == "get_weather"
240 assert result[0]["input_schema"]["type"] == "object"
242 def test_message_conversion_simple(self):
243 from agentos.llm.anthropic_provider import _messages_to_anthropic
244 msgs = [Message(role=MessageRole.USER, content="Hello")]
245 system, api_msgs = _messages_to_anthropic(msgs)
246 assert system is None
247 assert len(api_msgs) == 1
248 assert api_msgs[0]["role"] == "user"
249 assert api_msgs[0]["content"] == "Hello"
251 def test_message_conversion_with_system(self):
252 from agentos.llm.anthropic_provider import _messages_to_anthropic
253 msgs = [
254 Message(role=MessageRole.SYSTEM, content="You are helpful."),
255 Message(role=MessageRole.USER, content="Hi"),
256 ]
257 system, api_msgs = _messages_to_anthropic(msgs)
258 assert system == "You are helpful."
259 assert len(api_msgs) == 1
260 assert api_msgs[0]["role"] == "user"
262 def test_build_body_includes_tools(self):
263 p = AnthropicProvider(api_key="sk-ant")
264 tools = [Tool.from_function("search", "search the web", {
265 "q": ToolParameter(type="string", description="query", required=True),
266 })]
267 body = p._build_body(
268 [Message(role=MessageRole.USER, content="test")],
269 temperature=0.5, max_tokens=100, top_p=0.9,
270 stop=None, tools=tools, tool_choice="auto",
271 )
272 assert body["model"] == "claude-sonnet-4-20250514"
273 assert "tools" in body
274 assert body["tools"][0]["name"] == "search"