Coverage for agentos/llm/providers/base_http.py: 0%
48 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 12:29 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 12:29 +0800
1"""Base HTTP-based LLM Provider with shared OpenAI-compatible API logic."""
3from __future__ import annotations
5import json
6import os
7import urllib.request
9from agentos.llm.base import (
10 CompletionChoice,
11 CompletionResult,
12 CompletionUsage,
13 LLMProvider,
14 Message,
15 MessageRole,
16 Tool,
17 ToolCall,
18)
21class BaseHttpProvider(LLMProvider):
22 """OpenAI-compatible HTTP API provider base class.
24 Subclasses override: provider_name, API_URL, _api_key_env, _default_model
25 """
27 API_URL: str = ""
28 _api_key_env: str = ""
29 _default_model: str = ""
31 def __init__(self, model: str | None = None, api_key: str | None = None):
32 super().__init__(model=model or self._default_model)
33 self._api_key = api_key or os.getenv(self._api_key_env, "")
35 # ── Message conversion ──
37 @staticmethod
38 def _messages_to_api(messages: list[Message]) -> list[dict]:
39 api_msgs = []
40 for m in messages:
41 entry: dict = {"role": m.role.value}
42 if m.content:
43 entry["content"] = m.content
44 if m.tool_calls:
45 entry["tool_calls"] = [
46 {
47 "id": tc.id,
48 "type": tc.type if hasattr(tc, "type") else "function",
49 "function": {
50 "name": tc.name,
51 "arguments": tc.arguments,
52 },
53 }
54 for tc in m.tool_calls
55 ]
56 if m.tool_call_id:
57 entry["tool_call_id"] = m.tool_call_id
58 entry["content"] = m.content or ""
59 api_msgs.append(entry)
60 return api_msgs
62 @staticmethod
63 def _tools_to_api(tools: list[Tool]) -> list[dict]:
64 return [t.as_schema() for t in tools]
66 # ── API call ──
68 def _call_api(
69 self, messages: list[Message], tools: list[Tool] | None = None, temperature: float = 0.7
70 ) -> CompletionResult:
71 body: dict = {
72 "model": self.model,
73 "messages": self._messages_to_api(messages),
74 "stream": False,
75 "temperature": temperature,
76 }
77 if tools:
78 body["tools"] = self._tools_to_api(tools)
80 req = urllib.request.Request(
81 self.API_URL,
82 data=json.dumps(body).encode("utf-8"),
83 headers={
84 "Content-Type": "application/json",
85 "Authorization": f"Bearer {self._api_key}",
86 },
87 method="POST",
88 )
90 with urllib.request.urlopen(req, timeout=120) as resp:
91 data = json.loads(resp.read().decode("utf-8"))
93 choice = data["choices"][0]
94 msg = choice["message"]
96 tool_calls = None
97 if msg.get("tool_calls"):
98 tool_calls = [
99 ToolCall(
100 id=tc["id"],
101 name=tc["function"]["name"],
102 arguments=tc["function"]["arguments"],
103 )
104 for tc in msg["tool_calls"]
105 ]
107 return CompletionResult(
108 id=data.get("id", ""),
109 model=data.get("model", self.model),
110 choices=[
111 CompletionChoice(
112 index=0,
113 message=Message(
114 role=MessageRole.ASSISTANT,
115 content=msg.get("content", ""),
116 tool_calls=tool_calls,
117 ),
118 finish_reason=choice.get("finish_reason", "stop"),
119 )
120 ],
121 usage=CompletionUsage(
122 prompt_tokens=data.get("usage", {}).get("prompt_tokens", 0),
123 completion_tokens=data.get("usage", {}).get("completion_tokens", 0),
124 total_tokens=data.get("usage", {}).get("total_tokens", 0),
125 ),
126 )
128 # ── Interface ──
130 def chat(self, messages: list[Message], **kwargs) -> CompletionResult:
131 tools = kwargs.get("tools")
132 temperature = kwargs.get("temperature", 0.7)
133 return self._call_api(messages, tools=tools, temperature=temperature)
135 async def achat(self, messages: list[Message], **kwargs) -> CompletionResult:
136 return self.chat(messages, **kwargs)