omniai-kit 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omniai/__init__.py +117 -0
- omniai/_base.py +386 -0
- omniai/_constants.py +39 -0
- omniai/_version.py +4 -0
- omniai/client.py +316 -0
- omniai/exceptions.py +113 -0
- omniai/py.typed +1 -0
- omniai/resources/__init__.py +26 -0
- omniai/resources/chat.py +403 -0
- omniai/resources/livekit.py +71 -0
- omniai/resources/models.py +63 -0
- omniai/resources/router.py +1040 -0
- omniai/resources/tasks.py +171 -0
- omniai/types/__init__.py +27 -0
- omniai/types/chat.py +48 -0
- omniai/types/livekit.py +17 -0
- omniai/types/models.py +30 -0
- omniai/types/tasks.py +208 -0
- omniai_kit-1.0.0.dist-info/METADATA +270 -0
- omniai_kit-1.0.0.dist-info/RECORD +23 -0
- omniai_kit-1.0.0.dist-info/WHEEL +5 -0
- omniai_kit-1.0.0.dist-info/licenses/LICENSE +21 -0
- omniai_kit-1.0.0.dist-info/top_level.txt +1 -0
omniai/__init__.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""OmniAI: Official Python Client SDK for OmniAI Gateway & Multi-Provider LLM Router."""
|
|
2
|
+
|
|
3
|
+
from omniai._version import __version__, VERSION
|
|
4
|
+
from omniai._constants import DEFAULT_BASE_URL, SUPPORTED_PROVIDERS
|
|
5
|
+
from omniai.client import (
|
|
6
|
+
OmniaiClient,
|
|
7
|
+
AsyncOmniaiClient,
|
|
8
|
+
OmniAIClient,
|
|
9
|
+
AsyncOmniAIClient,
|
|
10
|
+
OmniClient,
|
|
11
|
+
AsyncOmniClient,
|
|
12
|
+
)
|
|
13
|
+
from omniai.resources.router import RouterEngine
|
|
14
|
+
from omniai._base import RetryPolicy
|
|
15
|
+
from omniai.exceptions import (
|
|
16
|
+
OmniaiError,
|
|
17
|
+
OmniaiAuthError,
|
|
18
|
+
OmniaiAuthenticationError,
|
|
19
|
+
OmniaiPermissionDeniedError,
|
|
20
|
+
OmniaiAPIError,
|
|
21
|
+
OmniaiBadRequestError,
|
|
22
|
+
OmniaiNotFoundError,
|
|
23
|
+
OmniaiRateLimitError,
|
|
24
|
+
OmniaiTimeoutError,
|
|
25
|
+
OmniaiConnectionError,
|
|
26
|
+
OmniaiExecutionError,
|
|
27
|
+
OmniaiValidationError,
|
|
28
|
+
OmniaiPaymentRequiredError,
|
|
29
|
+
OmniaiUnsupportedProviderError,
|
|
30
|
+
OmniAIError,
|
|
31
|
+
OmniAIAuthError,
|
|
32
|
+
OmniAIAuthenticationError,
|
|
33
|
+
OmniAIAPIError,
|
|
34
|
+
OmniAIRateLimitError,
|
|
35
|
+
OmniAITimeoutError,
|
|
36
|
+
OmniAIConnectionError,
|
|
37
|
+
OmniAIValidationError,
|
|
38
|
+
OmniError,
|
|
39
|
+
OmniAuthError,
|
|
40
|
+
OmniExecutionError,
|
|
41
|
+
OmniTimeoutError,
|
|
42
|
+
OmniRateLimitError,
|
|
43
|
+
)
|
|
44
|
+
from omniai.types import (
|
|
45
|
+
ChatMessage,
|
|
46
|
+
ChatCompletionChoice,
|
|
47
|
+
ChatCompletionUsage,
|
|
48
|
+
ChatCompletionResponse,
|
|
49
|
+
ChatCompletionChunk,
|
|
50
|
+
ModelInfo,
|
|
51
|
+
ModelList,
|
|
52
|
+
LiveSessionInfo,
|
|
53
|
+
TaskStatus,
|
|
54
|
+
TaskJob,
|
|
55
|
+
AsyncTaskJob,
|
|
56
|
+
)
|
|
57
|
+
from omniai import resources, types
|
|
58
|
+
|
|
59
|
+
__all__ = [
|
|
60
|
+
"__version__",
|
|
61
|
+
"VERSION",
|
|
62
|
+
"DEFAULT_BASE_URL",
|
|
63
|
+
"SUPPORTED_PROVIDERS",
|
|
64
|
+
# Clients
|
|
65
|
+
"OmniaiClient",
|
|
66
|
+
"AsyncOmniaiClient",
|
|
67
|
+
"OmniAIClient",
|
|
68
|
+
"AsyncOmniAIClient",
|
|
69
|
+
"OmniClient",
|
|
70
|
+
"AsyncOmniClient",
|
|
71
|
+
# Engine & Policies
|
|
72
|
+
"RouterEngine",
|
|
73
|
+
"RetryPolicy",
|
|
74
|
+
# Exceptions
|
|
75
|
+
"OmniaiError",
|
|
76
|
+
"OmniaiAuthError",
|
|
77
|
+
"OmniaiAuthenticationError",
|
|
78
|
+
"OmniaiPermissionDeniedError",
|
|
79
|
+
"OmniaiAPIError",
|
|
80
|
+
"OmniaiBadRequestError",
|
|
81
|
+
"OmniaiNotFoundError",
|
|
82
|
+
"OmniaiRateLimitError",
|
|
83
|
+
"OmniaiTimeoutError",
|
|
84
|
+
"OmniaiConnectionError",
|
|
85
|
+
"OmniaiExecutionError",
|
|
86
|
+
"OmniaiValidationError",
|
|
87
|
+
"OmniaiPaymentRequiredError",
|
|
88
|
+
"OmniaiUnsupportedProviderError",
|
|
89
|
+
"OmniAIError",
|
|
90
|
+
"OmniAIAuthError",
|
|
91
|
+
"OmniAIAuthenticationError",
|
|
92
|
+
"OmniAIAPIError",
|
|
93
|
+
"OmniAIRateLimitError",
|
|
94
|
+
"OmniAITimeoutError",
|
|
95
|
+
"OmniAIConnectionError",
|
|
96
|
+
"OmniAIValidationError",
|
|
97
|
+
"OmniError",
|
|
98
|
+
"OmniAuthError",
|
|
99
|
+
"OmniExecutionError",
|
|
100
|
+
"OmniTimeoutError",
|
|
101
|
+
"OmniRateLimitError",
|
|
102
|
+
# Types
|
|
103
|
+
"ChatMessage",
|
|
104
|
+
"ChatCompletionChoice",
|
|
105
|
+
"ChatCompletionUsage",
|
|
106
|
+
"ChatCompletionResponse",
|
|
107
|
+
"ChatCompletionChunk",
|
|
108
|
+
"ModelInfo",
|
|
109
|
+
"ModelList",
|
|
110
|
+
"LiveSessionInfo",
|
|
111
|
+
"TaskStatus",
|
|
112
|
+
"TaskJob",
|
|
113
|
+
"AsyncTaskJob",
|
|
114
|
+
# Modules
|
|
115
|
+
"resources",
|
|
116
|
+
"types",
|
|
117
|
+
]
|
omniai/_base.py
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""Base HTTP clients for the OmniAI Python SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
from typing import Any, Dict, Mapping, Optional, Union
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from omniai._constants import (
|
|
11
|
+
DEFAULT_BASE_URL,
|
|
12
|
+
DEFAULT_MAX_RETRIES,
|
|
13
|
+
DEFAULT_TIMEOUT,
|
|
14
|
+
ENV_API_KEY_NAMES,
|
|
15
|
+
ENV_BASE_URL_NAMES,
|
|
16
|
+
)
|
|
17
|
+
from omniai._version import __version__
|
|
18
|
+
from omniai.exceptions import (
|
|
19
|
+
OmniaiAuthError,
|
|
20
|
+
OmniaiConnectionError,
|
|
21
|
+
OmniaiExecutionError,
|
|
22
|
+
OmniaiNotFoundError,
|
|
23
|
+
OmniaiPaymentRequiredError,
|
|
24
|
+
OmniaiRateLimitError,
|
|
25
|
+
OmniaiTimeoutError,
|
|
26
|
+
)
|
|
27
|
+
import email.utils
|
|
28
|
+
import logging
|
|
29
|
+
import random
|
|
30
|
+
import time
|
|
31
|
+
from typing import Callable, Coroutine, Set, Tuple, Type, TypeVar
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger("omniai._base")
|
|
34
|
+
|
|
35
|
+
T = TypeVar("T")
|
|
36
|
+
|
|
37
|
+
DEFAULT_RETRYABLE_EXCEPTIONS: Tuple[Type[Exception], ...] = (
|
|
38
|
+
httpx.ConnectError,
|
|
39
|
+
httpx.ConnectTimeout,
|
|
40
|
+
httpx.ReadTimeout,
|
|
41
|
+
httpx.WriteTimeout,
|
|
42
|
+
httpx.PoolTimeout,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
DEFAULT_RETRYABLE_STATUSES: Set[int] = {429, 500, 502, 503, 504}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class RetryPolicy:
|
|
49
|
+
"""
|
|
50
|
+
Enterprise-grade exponential backoff retry policy with full jitter
|
|
51
|
+
and Retry-After header parsing for resilient API interactions.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
max_retries: int = 3,
|
|
57
|
+
backoff_factor: float = 0.5,
|
|
58
|
+
max_backoff: float = 30.0,
|
|
59
|
+
retryable_statuses: Optional[Set[int]] = None,
|
|
60
|
+
retryable_exceptions: Optional[Tuple[Type[Exception], ...]] = None,
|
|
61
|
+
):
|
|
62
|
+
self.max_retries = max(0, max_retries)
|
|
63
|
+
self.backoff_factor = max(0.01, backoff_factor)
|
|
64
|
+
self.max_backoff = max_backoff
|
|
65
|
+
self.retryable_statuses = retryable_statuses or DEFAULT_RETRYABLE_STATUSES
|
|
66
|
+
self.retryable_exceptions = retryable_exceptions or DEFAULT_RETRYABLE_EXCEPTIONS
|
|
67
|
+
|
|
68
|
+
def parse_retry_after(self, response: httpx.Response) -> Optional[float]:
|
|
69
|
+
"""Parses Retry-After header as either seconds or HTTP-date."""
|
|
70
|
+
raw = response.headers.get("retry-after")
|
|
71
|
+
if not raw:
|
|
72
|
+
return None
|
|
73
|
+
try:
|
|
74
|
+
return float(raw)
|
|
75
|
+
except ValueError:
|
|
76
|
+
try:
|
|
77
|
+
date_tuple = email.utils.parsedate_tz(raw)
|
|
78
|
+
if date_tuple:
|
|
79
|
+
timestamp = email.utils.mktime_tz(date_tuple)
|
|
80
|
+
delta = timestamp - time.time()
|
|
81
|
+
return max(0.0, delta)
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
def calculate_delay(
|
|
87
|
+
self,
|
|
88
|
+
attempt: int,
|
|
89
|
+
response: Optional[httpx.Response] = None,
|
|
90
|
+
) -> float:
|
|
91
|
+
"""Calculates retry delay in seconds using exponential backoff with full jitter."""
|
|
92
|
+
if response is not None:
|
|
93
|
+
retry_after = self.parse_retry_after(response)
|
|
94
|
+
if retry_after is not None:
|
|
95
|
+
return min(self.max_backoff, retry_after + random.uniform(0.05, 0.5))
|
|
96
|
+
|
|
97
|
+
base_delay = min(self.max_backoff, self.backoff_factor * (2 ** (attempt - 1)))
|
|
98
|
+
jittered = base_delay * random.uniform(0.5, 1.5)
|
|
99
|
+
return min(self.max_backoff, max(0.05, jittered))
|
|
100
|
+
|
|
101
|
+
def should_retry(
|
|
102
|
+
self,
|
|
103
|
+
attempt: int,
|
|
104
|
+
response: Optional[httpx.Response] = None,
|
|
105
|
+
exception: Optional[Exception] = None,
|
|
106
|
+
) -> bool:
|
|
107
|
+
"""Determines if a failed request attempt should be retried."""
|
|
108
|
+
if attempt >= self.max_retries:
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
if exception is not None:
|
|
112
|
+
return isinstance(exception, self.retryable_exceptions)
|
|
113
|
+
|
|
114
|
+
if response is not None:
|
|
115
|
+
return response.status_code in self.retryable_statuses
|
|
116
|
+
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
def execute_sync(
|
|
120
|
+
self,
|
|
121
|
+
func: Callable[[], httpx.Response],
|
|
122
|
+
operation_name: str = "request",
|
|
123
|
+
) -> httpx.Response:
|
|
124
|
+
"""Executes a synchronous HTTP call with retry logic."""
|
|
125
|
+
attempt = 0
|
|
126
|
+
while True:
|
|
127
|
+
attempt += 1
|
|
128
|
+
try:
|
|
129
|
+
resp = func()
|
|
130
|
+
if self.should_retry(attempt, response=resp):
|
|
131
|
+
delay = self.calculate_delay(attempt, response=resp)
|
|
132
|
+
logger.warning(
|
|
133
|
+
f"OmniAI {operation_name} returned status {resp.status_code}. "
|
|
134
|
+
f"Retrying (attempt {attempt}/{self.max_retries}) in {delay:.2f}s..."
|
|
135
|
+
)
|
|
136
|
+
time.sleep(delay)
|
|
137
|
+
continue
|
|
138
|
+
return resp
|
|
139
|
+
except Exception as exc:
|
|
140
|
+
if self.should_retry(attempt, exception=exc):
|
|
141
|
+
delay = self.calculate_delay(attempt)
|
|
142
|
+
logger.warning(
|
|
143
|
+
f"OmniAI {operation_name} failed with {exc.__class__.__name__}: {exc}. "
|
|
144
|
+
f"Retrying (attempt {attempt}/{self.max_retries}) in {delay:.2f}s..."
|
|
145
|
+
)
|
|
146
|
+
time.sleep(delay)
|
|
147
|
+
continue
|
|
148
|
+
raise exc
|
|
149
|
+
|
|
150
|
+
async def execute_async(
|
|
151
|
+
self,
|
|
152
|
+
func: Callable[[], Coroutine[Any, Any, httpx.Response]],
|
|
153
|
+
operation_name: str = "request",
|
|
154
|
+
) -> httpx.Response:
|
|
155
|
+
"""Executes an asynchronous HTTP call with retry logic."""
|
|
156
|
+
attempt = 0
|
|
157
|
+
while True:
|
|
158
|
+
attempt += 1
|
|
159
|
+
try:
|
|
160
|
+
resp = await func()
|
|
161
|
+
if self.should_retry(attempt, response=resp):
|
|
162
|
+
delay = self.calculate_delay(attempt, response=resp)
|
|
163
|
+
logger.warning(
|
|
164
|
+
f"OmniAI {operation_name} returned status {resp.status_code}. "
|
|
165
|
+
f"Retrying async (attempt {attempt}/{self.max_retries}) in {delay:.2f}s..."
|
|
166
|
+
)
|
|
167
|
+
await asyncio.sleep(delay)
|
|
168
|
+
continue
|
|
169
|
+
return resp
|
|
170
|
+
except Exception as exc:
|
|
171
|
+
if self.should_retry(attempt, exception=exc):
|
|
172
|
+
delay = self.calculate_delay(attempt)
|
|
173
|
+
logger.warning(
|
|
174
|
+
f"OmniAI {operation_name} failed with {exc.__class__.__name__}: {exc}. "
|
|
175
|
+
f"Retrying async (attempt {attempt}/{self.max_retries}) in {delay:.2f}s..."
|
|
176
|
+
)
|
|
177
|
+
await asyncio.sleep(delay)
|
|
178
|
+
continue
|
|
179
|
+
raise exc
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _resolve_api_key(api_key: Optional[str]) -> str:
|
|
183
|
+
"""Discovers API key from parameters or environment variables."""
|
|
184
|
+
if api_key:
|
|
185
|
+
return api_key.strip()
|
|
186
|
+
for env_var in ENV_API_KEY_NAMES:
|
|
187
|
+
val = os.getenv(env_var)
|
|
188
|
+
if val:
|
|
189
|
+
return val.strip()
|
|
190
|
+
return ""
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _resolve_base_url(base_url: Optional[str]) -> str:
|
|
194
|
+
"""Discovers base URL from parameters or environment variables, falling back to default."""
|
|
195
|
+
if base_url:
|
|
196
|
+
return base_url.strip().rstrip("/")
|
|
197
|
+
for env_var in ENV_BASE_URL_NAMES:
|
|
198
|
+
val = os.getenv(env_var)
|
|
199
|
+
if val:
|
|
200
|
+
return val.strip().rstrip("/")
|
|
201
|
+
return DEFAULT_BASE_URL.rstrip("/")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _mask_key(key: str) -> str:
|
|
205
|
+
"""Masks secret API key for safe representation."""
|
|
206
|
+
if not key:
|
|
207
|
+
return "None"
|
|
208
|
+
if len(key) <= 12:
|
|
209
|
+
return "***"
|
|
210
|
+
return f"{key[:7]}...{key[-4:]}"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class BaseClient:
|
|
214
|
+
"""Synchronous base HTTP client handling authentication, sessions, and retries."""
|
|
215
|
+
|
|
216
|
+
def __init__(
|
|
217
|
+
self,
|
|
218
|
+
api_key: Optional[str] = None,
|
|
219
|
+
base_url: Optional[str] = None,
|
|
220
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
221
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
222
|
+
headers: Optional[Mapping[str, str]] = None,
|
|
223
|
+
http_client: Optional[httpx.Client] = None,
|
|
224
|
+
):
|
|
225
|
+
self.api_key: str = _resolve_api_key(api_key)
|
|
226
|
+
self.base_url: str = _resolve_base_url(base_url)
|
|
227
|
+
self.timeout: float = timeout
|
|
228
|
+
self.retry_policy = RetryPolicy(max_retries=max_retries)
|
|
229
|
+
|
|
230
|
+
default_headers: Dict[str, str] = {
|
|
231
|
+
"Accept": "application/json",
|
|
232
|
+
"User-Agent": f"OmniAI-Python-SDK/{__version__}",
|
|
233
|
+
}
|
|
234
|
+
if self.api_key:
|
|
235
|
+
default_headers["Authorization"] = f"Bearer {self.api_key}"
|
|
236
|
+
default_headers["X-API-KEY"] = self.api_key
|
|
237
|
+
|
|
238
|
+
if headers:
|
|
239
|
+
default_headers.update(headers)
|
|
240
|
+
|
|
241
|
+
self._headers = default_headers
|
|
242
|
+
self._custom_client = http_client is not None
|
|
243
|
+
self._http: httpx.Client = http_client or httpx.Client(
|
|
244
|
+
base_url=self.base_url,
|
|
245
|
+
headers=self._headers,
|
|
246
|
+
timeout=self.timeout,
|
|
247
|
+
follow_redirects=False,
|
|
248
|
+
)
|
|
249
|
+
self._client = self._http
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def client(self) -> httpx.Client:
|
|
253
|
+
return self._http
|
|
254
|
+
|
|
255
|
+
def _handle_error_response(self, response: httpx.Response) -> None:
|
|
256
|
+
"""Parses error responses and maps them to appropriate typed exceptions."""
|
|
257
|
+
status = response.status_code
|
|
258
|
+
try:
|
|
259
|
+
data = response.json()
|
|
260
|
+
message = (
|
|
261
|
+
data.get("error", {}).get("message")
|
|
262
|
+
or data.get("detail")
|
|
263
|
+
or data.get("message")
|
|
264
|
+
or response.text
|
|
265
|
+
)
|
|
266
|
+
except Exception:
|
|
267
|
+
message = response.text or f"HTTP {status}"
|
|
268
|
+
|
|
269
|
+
if status in (401, 403):
|
|
270
|
+
raise OmniaiAuthError(f"Authentication failed ({status}): {message}")
|
|
271
|
+
elif status == 402:
|
|
272
|
+
raise OmniaiPaymentRequiredError(f"Payment required ({status}): {message}")
|
|
273
|
+
elif status == 404:
|
|
274
|
+
raise OmniaiNotFoundError(f"Resource not found ({status}): {message}")
|
|
275
|
+
elif status == 429:
|
|
276
|
+
retry_after_raw = response.headers.get("Retry-After")
|
|
277
|
+
retry_after = float(retry_after_raw) if retry_after_raw and retry_after_raw.isdigit() else None
|
|
278
|
+
raise OmniaiRateLimitError(f"Rate limit exceeded: {message}", retry_after=retry_after)
|
|
279
|
+
elif status >= 500:
|
|
280
|
+
raise OmniaiExecutionError(f"Server error ({status}): {message}", status_code=status)
|
|
281
|
+
else:
|
|
282
|
+
raise OmniaiExecutionError(f"Request failed ({status}): {message}", status_code=status)
|
|
283
|
+
|
|
284
|
+
def close(self) -> None:
|
|
285
|
+
if not self._custom_client and hasattr(self, "_http") and not self._http.is_closed:
|
|
286
|
+
self._http.close()
|
|
287
|
+
|
|
288
|
+
def __enter__(self) -> BaseClient:
|
|
289
|
+
return self
|
|
290
|
+
|
|
291
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
292
|
+
self.close()
|
|
293
|
+
|
|
294
|
+
def __repr__(self) -> str:
|
|
295
|
+
masked = _mask_key(self.api_key)
|
|
296
|
+
return f"<{self.__class__.__name__} base_url={self.base_url!r} api_key={masked!r}>"
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class AsyncBaseClient:
|
|
300
|
+
"""Asynchronous base HTTP client handling authentication, sessions, and retries."""
|
|
301
|
+
|
|
302
|
+
def __init__(
|
|
303
|
+
self,
|
|
304
|
+
api_key: Optional[str] = None,
|
|
305
|
+
base_url: Optional[str] = None,
|
|
306
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
307
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
308
|
+
headers: Optional[Mapping[str, str]] = None,
|
|
309
|
+
http_client: Optional[httpx.AsyncClient] = None,
|
|
310
|
+
):
|
|
311
|
+
self.api_key: str = _resolve_api_key(api_key)
|
|
312
|
+
self.base_url: str = _resolve_base_url(base_url)
|
|
313
|
+
self.timeout: float = timeout
|
|
314
|
+
self.retry_policy = RetryPolicy(max_retries=max_retries)
|
|
315
|
+
|
|
316
|
+
default_headers: Dict[str, str] = {
|
|
317
|
+
"Accept": "application/json",
|
|
318
|
+
"User-Agent": f"OmniAI-Python-SDK/{__version__}",
|
|
319
|
+
}
|
|
320
|
+
if self.api_key:
|
|
321
|
+
default_headers["Authorization"] = f"Bearer {self.api_key}"
|
|
322
|
+
default_headers["X-API-KEY"] = self.api_key
|
|
323
|
+
|
|
324
|
+
if headers:
|
|
325
|
+
default_headers.update(headers)
|
|
326
|
+
|
|
327
|
+
self._headers = default_headers
|
|
328
|
+
self._custom_client = http_client is not None
|
|
329
|
+
self._http: httpx.AsyncClient = http_client or httpx.AsyncClient(
|
|
330
|
+
base_url=self.base_url,
|
|
331
|
+
headers=self._headers,
|
|
332
|
+
timeout=self.timeout,
|
|
333
|
+
follow_redirects=False,
|
|
334
|
+
)
|
|
335
|
+
self._client = self._http
|
|
336
|
+
|
|
337
|
+
@property
|
|
338
|
+
def client(self) -> httpx.AsyncClient:
|
|
339
|
+
return self._http
|
|
340
|
+
|
|
341
|
+
def _handle_error_response(self, response: httpx.Response) -> None:
|
|
342
|
+
"""Parses error responses and maps them to appropriate typed exceptions."""
|
|
343
|
+
status = response.status_code
|
|
344
|
+
try:
|
|
345
|
+
data = response.json()
|
|
346
|
+
message = (
|
|
347
|
+
data.get("error", {}).get("message")
|
|
348
|
+
or data.get("detail")
|
|
349
|
+
or data.get("message")
|
|
350
|
+
or response.text
|
|
351
|
+
)
|
|
352
|
+
except Exception:
|
|
353
|
+
message = response.text or f"HTTP {status}"
|
|
354
|
+
|
|
355
|
+
if status in (401, 403):
|
|
356
|
+
raise OmniaiAuthError(f"Authentication failed ({status}): {message}")
|
|
357
|
+
elif status == 402:
|
|
358
|
+
raise OmniaiPaymentRequiredError(f"Payment required ({status}): {message}")
|
|
359
|
+
elif status == 404:
|
|
360
|
+
raise OmniaiNotFoundError(f"Resource not found ({status}): {message}")
|
|
361
|
+
elif status == 429:
|
|
362
|
+
retry_after_raw = response.headers.get("Retry-After")
|
|
363
|
+
retry_after = float(retry_after_raw) if retry_after_raw and retry_after_raw.isdigit() else None
|
|
364
|
+
raise OmniaiRateLimitError(f"Rate limit exceeded: {message}", retry_after=retry_after)
|
|
365
|
+
elif status >= 500:
|
|
366
|
+
raise OmniaiExecutionError(f"Server error ({status}): {message}", status_code=status)
|
|
367
|
+
else:
|
|
368
|
+
raise OmniaiExecutionError(f"Request failed ({status}): {message}", status_code=status)
|
|
369
|
+
|
|
370
|
+
async def aclose(self) -> None:
|
|
371
|
+
if not self._custom_client and hasattr(self, "_http") and not self._http.is_closed:
|
|
372
|
+
await self._http.aclose()
|
|
373
|
+
|
|
374
|
+
async def close(self) -> None:
|
|
375
|
+
"""Alias for aclose() for interface consistency."""
|
|
376
|
+
await self.aclose()
|
|
377
|
+
|
|
378
|
+
async def __aenter__(self) -> AsyncBaseClient:
|
|
379
|
+
return self
|
|
380
|
+
|
|
381
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
382
|
+
await self.aclose()
|
|
383
|
+
|
|
384
|
+
def __repr__(self) -> str:
|
|
385
|
+
masked = _mask_key(self.api_key)
|
|
386
|
+
return f"<{self.__class__.__name__} base_url={self.base_url!r} api_key={masked!r}>"
|
omniai/_constants.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Constants used throughout the OmniAI Python SDK."""
|
|
2
|
+
|
|
3
|
+
from typing import Set, Tuple
|
|
4
|
+
|
|
5
|
+
DEFAULT_BASE_URL: str = "https://omniai.go-agents.app"
|
|
6
|
+
DEFAULT_TIMEOUT: float = 60.0
|
|
7
|
+
DEFAULT_MAX_RETRIES: int = 3
|
|
8
|
+
DEFAULT_BACKOFF_FACTOR: float = 0.5
|
|
9
|
+
DEFAULT_MAX_BACKOFF: float = 30.0
|
|
10
|
+
|
|
11
|
+
DEFAULT_RETRYABLE_STATUSES: Set[int] = {429, 500, 502, 503, 504}
|
|
12
|
+
|
|
13
|
+
SUPPORTED_PROVIDERS: Tuple[str, ...] = (
|
|
14
|
+
"openai",
|
|
15
|
+
"gemini",
|
|
16
|
+
"anthropic",
|
|
17
|
+
"vllm",
|
|
18
|
+
"openrouter",
|
|
19
|
+
"ollama",
|
|
20
|
+
"custom",
|
|
21
|
+
"local",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
ENV_API_KEY_NAMES: Tuple[str, ...] = (
|
|
25
|
+
"OMNIAI_API_KEY",
|
|
26
|
+
"OMNI_API_KEY",
|
|
27
|
+
"X_API_KEY",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
ENV_BASE_URL_NAMES: Tuple[str, ...] = (
|
|
31
|
+
"OMNIAI_BASE_URL",
|
|
32
|
+
"OMNI_BASE_URL",
|
|
33
|
+
"GATEWAY_URL",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
DEFAULT_HEADERS: dict[str, str] = {
|
|
37
|
+
"Accept": "application/json",
|
|
38
|
+
"Content-Type": "application/json",
|
|
39
|
+
}
|
omniai/_version.py
ADDED