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 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
@@ -0,0 +1,4 @@
1
+ """OmniAI Python SDK Version."""
2
+
3
+ __version__ = "1.0.0"
4
+ VERSION = __version__