Coverage for agentos/models/backends/openai.py: 29%
92 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 12:20 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 12:20 +0800
1"""OpenAI backend for AgentOS.
3Supports OpenAI, Azure OpenAI, and any OpenAI-compatible API (DeepSeek, Groq, etc.).
4"""
6import os
7from collections.abc import AsyncIterator
8from dataclasses import dataclass, field
9from typing import Any
12@dataclass
13class OpenAIConfig:
14 """Configuration for OpenAI backend."""
16 api_key: str = field(default_factory=lambda: os.environ.get("OPENAI_API_KEY", ""))
17 base_url: str = field(
18 default_factory=lambda: os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1")
19 )
20 model: str = "gpt-4o"
21 temperature: float = 0.7
22 max_tokens: int = 4096
23 top_p: float = 1.0
24 frequency_penalty: float = 0.0
25 presence_penalty: float = 0.0
26 timeout: int = 60
27 max_retries: int = 3
28 organization: str = ""
31class OpenAIClient:
32 """OpenAI-compatible LLM client.
34 Works with:
35 - OpenAI (GPT-4o, GPT-4, GPT-3.5)
36 - Azure OpenAI
37 - DeepSeek
38 - Groq
39 - Together AI
40 - Any OpenAI-compatible endpoint
41 """
43 def __init__(self, config: OpenAIConfig | None = None):
44 self.config = config or OpenAIConfig()
45 self._client = None
46 self._async_client = None
48 @property
49 def headers(self) -> dict[str, str]:
50 h = {
51 "Authorization": f"Bearer {self.config.api_key}",
52 "Content-Type": "application/json",
53 }
54 if self.config.organization:
55 h["OpenAI-Organization"] = self.config.organization
56 return h
58 @property
59 def _chat_url(self) -> str:
60 return f"{self.config.base_url}/chat/completions"
62 async def _async_request(self, messages: list[dict], **kwargs) -> dict:
63 import httpx
65 timeout = httpx.Timeout(self.config.timeout)
66 payload = {
67 "model": self.config.model,
68 "messages": messages,
69 "temperature": kwargs.get("temperature", self.config.temperature),
70 "max_tokens": kwargs.get("max_tokens", self.config.max_tokens),
71 "top_p": kwargs.get("top_p", self.config.top_p),
72 "frequency_penalty": self.config.frequency_penalty,
73 "presence_penalty": self.config.presence_penalty,
74 }
76 if kwargs.get("tools"):
77 payload["tools"] = kwargs["tools"]
78 payload["tool_choice"] = kwargs.get("tool_choice", "auto")
80 if kwargs.get("response_format"):
81 payload["response_format"] = kwargs["response_format"]
83 for attempt in range(self.config.max_retries):
84 try:
85 async with httpx.AsyncClient() as client:
86 resp = await client.post(
87 self._chat_url,
88 json=payload,
89 headers=self.headers,
90 timeout=timeout,
91 )
92 resp.raise_for_status()
93 return resp.json()
94 except httpx.HTTPStatusError as e:
95 if attempt == self.config.max_retries - 1:
96 raise
97 if e.response.status_code >= 500:
98 import asyncio
100 await asyncio.sleep(2**attempt)
101 continue
102 raise
104 async def chat(
105 self,
106 messages: list[dict[str, str]],
107 system: str | None = None,
108 **kwargs,
109 ) -> dict[str, Any]:
110 """Send a chat completion request.
112 Args:
113 messages: List of message dicts with 'role' and 'content'.
114 system: Optional system prompt.
115 **kwargs: Override config parameters.
117 Returns:
118 Response dict with 'content', 'role', 'usage', 'model'.
119 """
120 msgs = messages.copy()
121 if system:
122 msgs.insert(0, {"role": "system", "content": system})
124 result = await self._async_request(msgs, **kwargs)
125 choice = result["choices"][0]
126 message = choice.get("message", {})
128 tool_calls = message.get("tool_calls", [])
130 return {
131 "content": message.get("content", ""),
132 "role": message.get("role", "assistant"),
133 "usage": result.get("usage", {}),
134 "model": result.get("model", self.config.model),
135 "finish_reason": choice.get("finish_reason", ""),
136 "tool_calls": [
137 {
138 "id": tc.get("id", ""),
139 "name": tc.get("function", {}).get("name", ""),
140 "arguments": tc.get("function", {}).get("arguments", "{}"),
141 }
142 for tc in tool_calls
143 ],
144 }
146 async def chat_stream(
147 self,
148 messages: list[dict[str, str]],
149 system: str | None = None,
150 **kwargs,
151 ) -> AsyncIterator[dict[str, Any]]:
152 """Stream chat completion tokens.
154 Yields dicts with 'delta', 'finish_reason', 'tool_call_delta'.
155 """
156 import httpx
158 msgs = messages.copy()
159 if system:
160 msgs.insert(0, {"role": "system", "content": system})
162 payload = {
163 "model": self.config.model,
164 "messages": msgs,
165 "temperature": kwargs.get("temperature", self.config.temperature),
166 "max_tokens": kwargs.get("max_tokens", self.config.max_tokens),
167 "top_p": kwargs.get("top_p", self.config.top_p),
168 "stream": True,
169 }
171 timeout = httpx.Timeout(self.config.timeout * 2)
172 async with httpx.AsyncClient() as client:
173 async with client.stream(
174 "POST",
175 self._chat_url,
176 json=payload,
177 headers=self.headers,
178 timeout=timeout,
179 ) as response:
180 response.raise_for_status()
181 async for line in response.aiter_lines():
182 if line.startswith("data: "):
183 data = line[6:].strip()
184 if data == "[DONE]":
185 break
186 import json
188 try:
189 chunk = json.loads(data)
190 choice = chunk["choices"][0]
191 delta = choice.get("delta", {})
192 yield {
193 "delta": delta.get("content", ""),
194 "finish_reason": choice.get("finish_reason"),
195 "tool_call_delta": delta.get("tool_calls"),
196 }
197 except (json.JSONDecodeError, KeyError):
198 continue
200 def sync_chat(
201 self,
202 messages: list[dict[str, str]],
203 system: str | None = None,
204 **kwargs,
205 ) -> dict[str, Any]:
206 """Synchronous chat completion."""
207 import asyncio
209 loop = asyncio.new_event_loop()
210 try:
211 return loop.run_until_complete(self.chat(messages, system, **kwargs))
212 finally:
213 loop.close()