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