indoxrouter 0.1.0__py3-none-any.whl → 0.1.2__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.
- indoxRouter/__init__.py +83 -0
- indoxRouter/client.py +564 -218
- indoxRouter/client_resourses/__init__.py +20 -0
- indoxRouter/client_resourses/base.py +67 -0
- indoxRouter/client_resourses/chat.py +144 -0
- indoxRouter/client_resourses/completion.py +138 -0
- indoxRouter/client_resourses/embedding.py +83 -0
- indoxRouter/client_resourses/image.py +116 -0
- indoxRouter/client_resourses/models.py +114 -0
- indoxRouter/config.py +151 -0
- indoxRouter/constants/__init__.py +81 -0
- indoxRouter/exceptions/__init__.py +70 -0
- indoxRouter/models/__init__.py +111 -0
- indoxRouter/providers/__init__.py +50 -50
- indoxRouter/providers/ai21labs.json +128 -0
- indoxRouter/providers/base_provider.py +62 -30
- indoxRouter/providers/claude.json +164 -0
- indoxRouter/providers/cohere.json +116 -0
- indoxRouter/providers/databricks.json +110 -0
- indoxRouter/providers/deepseek.json +110 -0
- indoxRouter/providers/google.json +128 -0
- indoxRouter/providers/meta.json +128 -0
- indoxRouter/providers/mistral.json +146 -0
- indoxRouter/providers/nvidia.json +110 -0
- indoxRouter/providers/openai.json +308 -0
- indoxRouter/providers/openai.py +471 -72
- indoxRouter/providers/qwen.json +110 -0
- indoxRouter/utils/__init__.py +240 -0
- indoxrouter-0.1.2.dist-info/LICENSE +21 -0
- indoxrouter-0.1.2.dist-info/METADATA +259 -0
- indoxrouter-0.1.2.dist-info/RECORD +33 -0
- indoxRouter/api_endpoints.py +0 -336
- indoxRouter/client_package.py +0 -138
- indoxRouter/init_db.py +0 -71
- indoxRouter/main.py +0 -711
- indoxRouter/migrations/__init__.py +0 -1
- indoxRouter/migrations/env.py +0 -98
- indoxRouter/migrations/versions/__init__.py +0 -1
- indoxRouter/migrations/versions/initial_schema.py +0 -84
- indoxRouter/providers/ai21.py +0 -268
- indoxRouter/providers/claude.py +0 -177
- indoxRouter/providers/cohere.py +0 -171
- indoxRouter/providers/databricks.py +0 -166
- indoxRouter/providers/deepseek.py +0 -166
- indoxRouter/providers/google.py +0 -216
- indoxRouter/providers/llama.py +0 -164
- indoxRouter/providers/meta.py +0 -227
- indoxRouter/providers/mistral.py +0 -182
- indoxRouter/providers/nvidia.py +0 -164
- indoxrouter-0.1.0.dist-info/METADATA +0 -179
- indoxrouter-0.1.0.dist-info/RECORD +0 -27
- {indoxrouter-0.1.0.dist-info → indoxrouter-0.1.2.dist-info}/WHEEL +0 -0
- {indoxrouter-0.1.0.dist-info → indoxrouter-0.1.2.dist-info}/top_level.txt +0 -0
indoxRouter/client.py
CHANGED
@@ -1,286 +1,632 @@
|
|
1
|
-
|
1
|
+
"""
|
2
|
+
IndoxRouter Client Module
|
3
|
+
|
4
|
+
This module provides a client for interacting with the IndoxRouter API, which serves as a unified
|
5
|
+
interface to multiple AI providers and models. The client handles authentication, rate limiting,
|
6
|
+
error handling, and provides a standardized response format across different AI services.
|
7
|
+
|
8
|
+
The Client class offers methods for:
|
9
|
+
- Authentication and session management
|
10
|
+
- Making API requests with automatic token refresh
|
11
|
+
- Accessing AI capabilities: chat completions, text completions, embeddings, and image generation
|
12
|
+
- Retrieving information about available providers and models
|
13
|
+
- Monitoring usage statistics
|
14
|
+
|
15
|
+
Usage example:
|
16
|
+
```python
|
17
|
+
from indoxRouter import Client
|
18
|
+
|
19
|
+
# Initialize client with API key
|
20
|
+
client = Client(api_key="your_api_key")
|
21
|
+
|
22
|
+
# Get available models
|
23
|
+
models = client.models()
|
24
|
+
|
25
|
+
# Generate a chat completion
|
26
|
+
response = client.chat([
|
27
|
+
{"role": "system", "content": "You are a helpful assistant."},
|
28
|
+
{"role": "user", "content": "Tell me a joke."}
|
29
|
+
], provider="openai", model="gpt-3.5-turbo")
|
30
|
+
|
31
|
+
# Generate text embeddings
|
32
|
+
embeddings = client.embeddings("This is a sample text")
|
33
|
+
|
34
|
+
# Clean up resources when done
|
35
|
+
client.close()
|
36
|
+
```
|
37
|
+
|
38
|
+
The client can also be used as a context manager:
|
39
|
+
```python
|
40
|
+
with Client(api_key="your_api_key") as client:
|
41
|
+
response = client.chat([{"role": "user", "content": "Hello!"}])
|
42
|
+
```
|
43
|
+
"""
|
44
|
+
|
2
45
|
import os
|
3
|
-
import
|
4
|
-
import
|
46
|
+
import logging
|
47
|
+
from datetime import datetime, timedelta
|
48
|
+
from typing import Dict, List, Any, Optional, Union
|
5
49
|
import requests
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
50
|
+
import jwt
|
51
|
+
from uuid import uuid4
|
52
|
+
|
53
|
+
from .exceptions import (
|
54
|
+
AuthenticationError,
|
55
|
+
NetworkError,
|
56
|
+
RateLimitError,
|
57
|
+
ProviderError,
|
58
|
+
ModelNotFoundError,
|
59
|
+
ProviderNotFoundError,
|
60
|
+
InvalidParametersError
|
61
|
+
)
|
62
|
+
from .models import (
|
63
|
+
ChatMessage,
|
64
|
+
ChatResponse,
|
65
|
+
CompletionResponse,
|
66
|
+
EmbeddingResponse,
|
67
|
+
ImageResponse,
|
68
|
+
ModelInfo,
|
69
|
+
)
|
70
|
+
from .config import get_config
|
71
|
+
from .client_resourses import (
|
72
|
+
Chat,
|
73
|
+
Completions,
|
74
|
+
Embeddings,
|
75
|
+
Images,
|
76
|
+
Models,
|
77
|
+
)
|
78
|
+
from .constants import (
|
79
|
+
DEFAULT_API_VERSION,
|
80
|
+
DEFAULT_TIMEOUT,
|
81
|
+
DEFAULT_BASE_URL,
|
82
|
+
ERROR_INVALID_API_KEY,
|
83
|
+
ERROR_MODEL_NOT_FOUND,
|
84
|
+
ERROR_PROVIDER_NOT_FOUND,
|
85
|
+
ERROR_INVALID_PARAMETERS,
|
86
|
+
ERROR_RATE_LIMIT,
|
87
|
+
ERROR_REQUEST_FAILED,
|
88
|
+
)
|
89
|
+
|
90
|
+
logger = logging.getLogger(__name__)
|
16
91
|
|
17
92
|
class Client:
|
18
93
|
"""
|
19
|
-
Client for
|
94
|
+
Client for the IndoxRouter API that provides a unified interface to multiple AI providers.
|
95
|
+
|
96
|
+
The Client class handles:
|
97
|
+
- Authentication and token management with automatic refresh
|
98
|
+
- Rate limiting and quota tracking
|
99
|
+
- Standardized error handling across providers
|
100
|
+
- Consistent response formatting
|
101
|
+
|
102
|
+
This client provides access to various AI capabilities including:
|
103
|
+
- Chat completions (chat)
|
104
|
+
- Text completions (completion)
|
105
|
+
- Text embeddings (embeddings)
|
106
|
+
- Image generation (image)
|
107
|
+
|
108
|
+
It also offers methods to retrieve information about available providers and models,
|
109
|
+
as well as usage statistics for the authenticated user.
|
110
|
+
|
111
|
+
The client can be used directly or as a context manager with the 'with' statement.
|
20
112
|
"""
|
21
113
|
|
22
|
-
def __init__(
|
23
|
-
"""
|
24
|
-
Initialize the client.
|
25
|
-
|
26
|
-
Args:
|
27
|
-
api_key: API key for authentication
|
28
|
-
base_url: Base URL for the API (default: http://localhost:8000)
|
29
|
-
"""
|
30
|
-
self.api_key = api_key
|
31
|
-
self.base_url = base_url or "http://localhost:8000"
|
32
|
-
self.auth_manager = AuthManager()
|
33
|
-
|
34
|
-
# Verify the API key
|
35
|
-
self.user_data = self.auth_manager.verify_api_key(api_key)
|
36
|
-
if not self.user_data:
|
37
|
-
raise ValueError("Invalid API key")
|
38
|
-
|
39
|
-
def generate(
|
114
|
+
def __init__(
|
40
115
|
self,
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
**kwargs,
|
47
|
-
) -> str:
|
116
|
+
api_key: Optional[str] = None,
|
117
|
+
base_url: str = f"{DEFAULT_BASE_URL}/{DEFAULT_API_VERSION}",
|
118
|
+
timeout: int = DEFAULT_TIMEOUT,
|
119
|
+
auto_refresh: bool = True,
|
120
|
+
):
|
48
121
|
"""
|
49
|
-
|
122
|
+
Initialize the client with authentication and session management
|
50
123
|
|
51
124
|
Args:
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
max_tokens: Maximum tokens
|
57
|
-
**kwargs: Additional parameters
|
58
|
-
|
59
|
-
Returns:
|
60
|
-
Model response
|
125
|
+
api_key: User's API key (default: INDOX_ROUTER_API_KEY env var)
|
126
|
+
base_url: Base URL for the API server
|
127
|
+
timeout: Request timeout in seconds
|
128
|
+
auto_refresh: Enable automatic token refresh
|
61
129
|
"""
|
62
|
-
|
130
|
+
# Authentication setup
|
131
|
+
self.api_key = api_key or os.getenv("INDOX_ROUTER_API_KEY")
|
132
|
+
if not self.api_key:
|
133
|
+
raise AuthenticationError(ERROR_INVALID_API_KEY)
|
134
|
+
|
135
|
+
self.base_url = base_url
|
136
|
+
self.timeout = timeout
|
137
|
+
self.auto_refresh = auto_refresh
|
138
|
+
self.config = get_config()
|
139
|
+
|
140
|
+
# Session state management
|
141
|
+
self.session = requests.Session()
|
142
|
+
self._auth_token = None
|
143
|
+
self._token_expiry = None
|
144
|
+
self.user_info = None
|
145
|
+
self.rate_limits = {}
|
146
|
+
|
147
|
+
# Initialize resources
|
148
|
+
self._init_resources()
|
149
|
+
self._authenticate()
|
150
|
+
|
151
|
+
def _init_resources(self):
|
152
|
+
"""Initialize resource controllers"""
|
153
|
+
self._chat = Chat(self)
|
154
|
+
self._completions = Completions(self)
|
155
|
+
self._embeddings = Embeddings(self)
|
156
|
+
self._images = Images(self)
|
157
|
+
self._models = Models(self)
|
158
|
+
|
159
|
+
# Backward compatibility
|
160
|
+
self.chat = self._chat
|
161
|
+
self.completions = self._completions
|
162
|
+
self.embeddings = self._embeddings
|
163
|
+
self.images = self._images
|
164
|
+
self.models = self._models
|
165
|
+
|
166
|
+
def _authenticate(self):
|
167
|
+
"""Full authentication flow with the API server"""
|
168
|
+
try:
|
169
|
+
# Get authentication token
|
170
|
+
auth_response = self.session.post(
|
171
|
+
f"{self.base_url}/auth/token",
|
172
|
+
json={"api_key": self.api_key},
|
173
|
+
timeout=self.timeout
|
174
|
+
)
|
175
|
+
auth_response.raise_for_status()
|
176
|
+
|
177
|
+
auth_data = auth_response.json()
|
178
|
+
self._process_auth_data(auth_data)
|
179
|
+
|
180
|
+
logger.info("Authenticated as user: %s", self.user_info['email'])
|
181
|
+
|
182
|
+
except requests.exceptions.RequestException as e:
|
183
|
+
raise NetworkError(f"Authentication failed: {str(e)}") from e
|
184
|
+
except jwt.PyJWTError as e:
|
185
|
+
raise AuthenticationError(f"Token validation failed: {str(e)}") from e
|
186
|
+
except KeyError as e:
|
187
|
+
raise AuthenticationError(f"Invalid auth response: {str(e)}") from e
|
188
|
+
|
189
|
+
def _process_auth_data(self, auth_data: dict):
|
190
|
+
"""Process authentication response data"""
|
191
|
+
# Validate and decode JWT
|
192
|
+
decoded_token = jwt.decode(
|
193
|
+
auth_data['access_token'],
|
194
|
+
self.config.JWT_PUBLIC_KEY,
|
195
|
+
algorithms=["RS256"],
|
196
|
+
audience="indox-router-api"
|
197
|
+
)
|
63
198
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
199
|
+
# Update session state
|
200
|
+
self._auth_token = auth_data['access_token']
|
201
|
+
self._token_expiry = datetime.fromtimestamp(decoded_token['exp'])
|
202
|
+
self.user_info = decoded_token['user']
|
203
|
+
self.rate_limits = decoded_token.get('rate_limits', {})
|
204
|
+
|
205
|
+
# Set default headers
|
206
|
+
self.session.headers.update({
|
207
|
+
"Authorization": f"Bearer {self._auth_token}",
|
208
|
+
"Content-Type": "application/json"
|
209
|
+
})
|
210
|
+
|
211
|
+
def _check_auth(self):
|
212
|
+
"""Validate authentication state"""
|
213
|
+
if datetime.now() >= self._token_expiry:
|
214
|
+
if self.auto_refresh:
|
215
|
+
self._refresh_token()
|
216
|
+
else:
|
217
|
+
raise AuthenticationError("Session expired")
|
218
|
+
|
219
|
+
def _refresh_token(self):
|
220
|
+
"""Refresh access token using refresh token"""
|
221
|
+
try:
|
222
|
+
refresh_response = self.session.post(
|
223
|
+
f"{self.base_url}/auth/refresh",
|
224
|
+
timeout=self.timeout
|
225
|
+
)
|
226
|
+
refresh_response.raise_for_status()
|
68
227
|
|
69
|
-
|
70
|
-
|
71
|
-
"model": model,
|
72
|
-
"prompt": prompt,
|
73
|
-
"temperature": temperature,
|
74
|
-
"max_tokens": max_tokens,
|
75
|
-
**kwargs,
|
76
|
-
}
|
228
|
+
refresh_data = refresh_response.json()
|
229
|
+
self._process_auth_data(refresh_data)
|
77
230
|
|
78
|
-
|
231
|
+
logger.debug("Successfully refreshed authentication token")
|
79
232
|
|
80
|
-
|
81
|
-
|
82
|
-
response.json().get("error", {}).get("message", "Unknown error")
|
83
|
-
)
|
84
|
-
raise Exception(f"Error: {error_message}")
|
233
|
+
except requests.exceptions.RequestException as e:
|
234
|
+
raise AuthenticationError(f"Token refresh failed: {str(e)}") from e
|
85
235
|
|
86
|
-
|
236
|
+
def _check_rate_limit(self, endpoint: str):
|
237
|
+
"""Check rate limits for specific endpoint"""
|
238
|
+
limits = self.rate_limits.get(endpoint, {})
|
239
|
+
if limits.get('remaining', 1) <= 0:
|
240
|
+
reset_time = datetime.fromtimestamp(limits.get('reset', datetime.now().timestamp()))
|
241
|
+
raise RateLimitError(
|
242
|
+
f"{ERROR_RATE_LIMIT} for {endpoint}. Resets at {reset_time}",
|
243
|
+
reset_time=reset_time
|
244
|
+
)
|
87
245
|
|
88
|
-
def
|
246
|
+
def _make_request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
|
89
247
|
"""
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
Returns:
|
96
|
-
List of models
|
248
|
+
Unified request handler with:
|
249
|
+
- Authentication checks
|
250
|
+
- Rate limiting
|
251
|
+
- Error handling
|
252
|
+
- Response standardization
|
97
253
|
"""
|
98
|
-
|
254
|
+
self._check_auth()
|
255
|
+
self._check_rate_limit(endpoint)
|
99
256
|
|
100
|
-
|
101
|
-
|
257
|
+
request_id = uuid4().hex
|
258
|
+
url = f"{self.base_url}/{endpoint}"
|
259
|
+
start_time = datetime.now()
|
102
260
|
|
103
|
-
|
104
|
-
|
261
|
+
try:
|
262
|
+
response = self.session.request(
|
263
|
+
method=method,
|
264
|
+
url=url,
|
265
|
+
timeout=self.timeout,
|
266
|
+
**kwargs
|
267
|
+
)
|
268
|
+
duration = (datetime.now() - start_time).total_seconds()
|
269
|
+
|
270
|
+
# Update rate limits from headers
|
271
|
+
self._update_rate_limits(response.headers)
|
272
|
+
|
273
|
+
response.raise_for_status()
|
274
|
+
return self._format_success(response.json(), request_id, duration, endpoint)
|
275
|
+
|
276
|
+
except requests.exceptions.RequestException as e:
|
277
|
+
error_response = self._format_error(e, request_id, duration, endpoint)
|
278
|
+
|
279
|
+
# Map HTTP errors to appropriate exception types
|
280
|
+
if hasattr(e, 'response') and e.response is not None:
|
281
|
+
status_code = e.response.status_code
|
282
|
+
error_data = {}
|
283
|
+
|
284
|
+
try:
|
285
|
+
error_data = e.response.json().get('error', {})
|
286
|
+
except ValueError:
|
287
|
+
pass
|
288
|
+
|
289
|
+
error_type = error_data.get('type', '')
|
290
|
+
|
291
|
+
if status_code == 404:
|
292
|
+
if 'provider' in error_type.lower():
|
293
|
+
raise ProviderNotFoundError(f"{ERROR_PROVIDER_NOT_FOUND}: {error_data.get('message', str(e))}")
|
294
|
+
elif 'model' in error_type.lower():
|
295
|
+
raise ModelNotFoundError(f"{ERROR_MODEL_NOT_FOUND}: {error_data.get('message', str(e))}")
|
296
|
+
elif status_code == 400:
|
297
|
+
raise InvalidParametersError(f"{ERROR_INVALID_PARAMETERS}: {error_data.get('message', str(e))}")
|
298
|
+
elif status_code == 429:
|
299
|
+
reset_time = datetime.fromtimestamp(error_data.get('reset', datetime.now().timestamp() + 60))
|
300
|
+
raise RateLimitError(f"{ERROR_RATE_LIMIT}: {error_data.get('message', str(e))}", reset_time=reset_time)
|
301
|
+
elif status_code >= 500:
|
302
|
+
raise ProviderError(f"{ERROR_REQUEST_FAILED}: {error_data.get('message', str(e))}")
|
303
|
+
|
304
|
+
raise NetworkError(f"Request failed: {str(e)}")
|
305
|
+
|
306
|
+
def _update_rate_limits(self, headers: Dict[str, str]):
|
307
|
+
"""Update rate limits from response headers"""
|
308
|
+
for key, value in headers.items():
|
309
|
+
if key.startswith('X-Ratelimit-'):
|
310
|
+
endpoint = key.split('-')[-1]
|
311
|
+
self.rate_limits[endpoint] = {
|
312
|
+
'limit': int(headers[f'X-Ratelimit-Limit-{endpoint}']),
|
313
|
+
'remaining': int(headers[f'X-Ratelimit-Remaining-{endpoint}']),
|
314
|
+
'reset': int(headers[f'X-Ratelimit-Reset-{endpoint}'])
|
315
|
+
}
|
316
|
+
|
317
|
+
def _format_success(self, data: dict, request_id: str, duration: float, endpoint: str) -> Dict[str, Any]:
|
318
|
+
"""Standard success response format"""
|
319
|
+
return {
|
320
|
+
"request_id": request_id,
|
321
|
+
"data": data.get('result'),
|
322
|
+
"metadata": {
|
323
|
+
"endpoint": endpoint,
|
324
|
+
"duration": duration,
|
325
|
+
"timestamp": datetime.now().isoformat(),
|
326
|
+
**data.get('metadata', {})
|
327
|
+
},
|
328
|
+
"error": None
|
105
329
|
}
|
106
330
|
|
107
|
-
|
331
|
+
def _format_error(self, error: Exception, request_id: str, duration: float, endpoint: str) -> Dict[str, Any]:
|
332
|
+
"""Standard error response format"""
|
333
|
+
error_info = {
|
334
|
+
"request_id": request_id,
|
335
|
+
"data": None,
|
336
|
+
"metadata": {
|
337
|
+
"endpoint": endpoint,
|
338
|
+
"duration": duration,
|
339
|
+
"timestamp": datetime.now().isoformat()
|
340
|
+
},
|
341
|
+
"error": {
|
342
|
+
"type": error.__class__.__name__,
|
343
|
+
"message": str(error),
|
344
|
+
"details": {}
|
345
|
+
}
|
346
|
+
}
|
108
347
|
|
109
|
-
if
|
110
|
-
|
111
|
-
|
112
|
-
)
|
113
|
-
raise Exception(f"Error: {error_message}")
|
348
|
+
if isinstance(error, requests.exceptions.HTTPError):
|
349
|
+
error_info["error"]["code"] = error.response.status_code
|
350
|
+
error_info["error"]["details"] = error.response.json().get('error', {})
|
114
351
|
|
115
|
-
|
352
|
+
logger.error("Request %s failed: %s", request_id, error)
|
353
|
+
return error_info
|
116
354
|
|
117
|
-
|
355
|
+
# Resource proxy methods
|
356
|
+
def providers(self) -> List[Dict[str, Any]]:
|
118
357
|
"""
|
119
|
-
|
120
|
-
|
358
|
+
Get a list of all available AI providers.
|
359
|
+
|
121
360
|
Returns:
|
122
|
-
List of
|
361
|
+
List[Dict[str, Any]]: A list of provider information dictionaries, each containing
|
362
|
+
details such as provider ID, name, description, and supported features.
|
363
|
+
|
364
|
+
Example:
|
365
|
+
```python
|
366
|
+
providers = client.providers()
|
367
|
+
for provider in providers:
|
368
|
+
print(f"{provider['id']}: {provider['name']}")
|
369
|
+
```
|
123
370
|
"""
|
124
|
-
|
371
|
+
return self._make_request('GET', 'providers')['data']
|
125
372
|
|
126
|
-
|
127
|
-
"Authorization": f"Bearer {self.api_key}",
|
128
|
-
}
|
129
|
-
|
130
|
-
response = requests.get(url, headers=headers)
|
131
|
-
|
132
|
-
if response.status_code != 200:
|
133
|
-
error_message = (
|
134
|
-
response.json().get("error", {}).get("message", "Unknown error")
|
135
|
-
)
|
136
|
-
raise Exception(f"Error: {error_message}")
|
137
|
-
|
138
|
-
return response.json().get("data", [])
|
139
|
-
|
140
|
-
def _parse_model_name(self, model_name: str) -> tuple:
|
373
|
+
def models(self, provider: Optional[str] = None) -> Dict[str, List[Dict[str, Any]]]:
|
141
374
|
"""
|
142
|
-
|
143
|
-
|
375
|
+
Get available models, optionally filtered by provider.
|
376
|
+
|
144
377
|
Args:
|
145
|
-
|
146
|
-
|
378
|
+
provider (Optional[str]): Provider ID to filter models by. If None, returns models from all providers.
|
379
|
+
|
147
380
|
Returns:
|
148
|
-
|
381
|
+
Dict[str, List[Dict[str, Any]]]: A dictionary mapping provider IDs to lists of model information.
|
382
|
+
Each model information dictionary contains details such as model ID, name, capabilities, and pricing.
|
383
|
+
|
384
|
+
Example:
|
385
|
+
```python
|
386
|
+
# Get all models
|
387
|
+
all_models = client.models()
|
388
|
+
|
389
|
+
# Get models from a specific provider
|
390
|
+
openai_models = client.models(provider="openai")
|
391
|
+
```
|
149
392
|
"""
|
150
|
-
|
151
|
-
|
152
|
-
f"Invalid model name format: {model_name}. Expected format: 'provider/model'"
|
153
|
-
)
|
154
|
-
|
155
|
-
provider_name, model_part = model_name.split("/", 1)
|
156
|
-
return provider_name, model_part
|
393
|
+
params = {"provider": provider} if provider else None
|
394
|
+
return self._make_request('GET', 'models', params=params)['data']
|
157
395
|
|
158
|
-
def
|
396
|
+
def model_info(self, provider: str, model: str) -> ModelInfo:
|
159
397
|
"""
|
160
|
-
|
161
|
-
|
398
|
+
Get detailed information about a specific model.
|
399
|
+
|
162
400
|
Args:
|
163
|
-
|
164
|
-
|
401
|
+
provider (str): Provider ID (e.g., "openai", "anthropic")
|
402
|
+
model (str): Model ID (e.g., "gpt-4", "claude-2")
|
403
|
+
|
165
404
|
Returns:
|
166
|
-
|
405
|
+
ModelInfo: Detailed information about the model, including capabilities,
|
406
|
+
pricing, rate limits, and other provider-specific details.
|
407
|
+
|
408
|
+
Raises:
|
409
|
+
ModelNotFoundError: If the specified model doesn't exist or isn't available
|
410
|
+
ProviderNotFoundError: If the specified provider doesn't exist
|
411
|
+
|
412
|
+
Example:
|
413
|
+
```python
|
414
|
+
model_details = client.model_info(provider="openai", model="gpt-4")
|
415
|
+
print(f"Context window: {model_details.context_window} tokens")
|
416
|
+
```
|
167
417
|
"""
|
168
418
|
try:
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
return provider_module.Provider
|
175
|
-
except (ImportError, AttributeError) as e:
|
176
|
-
raise ValueError(f"Provider not supported: {provider_name}") from e
|
419
|
+
return self._make_request('GET', f'models/{provider}/{model}')['data']
|
420
|
+
except requests.exceptions.HTTPError as e:
|
421
|
+
if e.response.status_code == 404:
|
422
|
+
raise ModelNotFoundError(f"{ERROR_MODEL_NOT_FOUND}: '{model}' for provider '{provider}'")
|
423
|
+
raise
|
177
424
|
|
178
|
-
def
|
425
|
+
def chat(self, messages: List[Union[Dict[str, str], ChatMessage]], **kwargs) -> ChatResponse:
|
179
426
|
"""
|
180
|
-
|
181
|
-
|
427
|
+
Generate a chat completion from a series of messages.
|
428
|
+
|
182
429
|
Args:
|
183
|
-
|
184
|
-
|
430
|
+
messages (List[Union[Dict[str, str], ChatMessage]]): A list of messages, where each message
|
431
|
+
is either a dictionary with 'role' and 'content' keys, or a ChatMessage object.
|
432
|
+
**kwargs: Additional parameters to pass to the provider, such as:
|
433
|
+
- provider (str): Provider ID (e.g., "openai", "anthropic")
|
434
|
+
- model (str): Model ID (e.g., "gpt-4", "claude-2")
|
435
|
+
- temperature (float): Controls randomness (0.0-1.0)
|
436
|
+
- max_tokens (int): Maximum number of tokens to generate
|
437
|
+
- stream (bool): Whether to stream the response
|
438
|
+
|
185
439
|
Returns:
|
186
|
-
|
440
|
+
ChatResponse: The generated chat completion response, containing the assistant's message,
|
441
|
+
usage statistics, and other metadata.
|
442
|
+
|
443
|
+
Example:
|
444
|
+
```python
|
445
|
+
response = client.chat([
|
446
|
+
{"role": "system", "content": "You are a helpful assistant."},
|
447
|
+
{"role": "user", "content": "Tell me about AI."}
|
448
|
+
], provider="openai", model="gpt-4")
|
449
|
+
|
450
|
+
print(response.message.content)
|
451
|
+
```
|
187
452
|
"""
|
188
|
-
|
189
|
-
return self.provider_cache[model_name]
|
190
|
-
|
191
|
-
provider_name, model_part = self._parse_model_name(model_name)
|
192
|
-
provider_class = self._load_provider_class(provider_name)
|
453
|
+
return self._chat(messages, **kwargs)
|
193
454
|
|
194
|
-
|
195
|
-
provider_api_key = self._get_provider_credentials(provider_name)
|
196
|
-
|
197
|
-
instance = provider_class(api_key=provider_api_key, model_name=model_part)
|
198
|
-
self.provider_cache[model_name] = instance
|
199
|
-
return instance
|
200
|
-
|
201
|
-
def _get_provider_credentials(self, provider_name: str) -> str:
|
455
|
+
def completion(self, prompt: str, **kwargs) -> CompletionResponse:
|
202
456
|
"""
|
203
|
-
|
204
|
-
|
457
|
+
Generate a text completion from a prompt.
|
458
|
+
|
205
459
|
Args:
|
206
|
-
|
207
|
-
|
460
|
+
prompt (str): The text prompt to complete
|
461
|
+
**kwargs: Additional parameters to pass to the provider, such as:
|
462
|
+
- provider (str): Provider ID (e.g., "openai", "cohere")
|
463
|
+
- model (str): Model ID (e.g., "text-davinci-003", "command")
|
464
|
+
- temperature (float): Controls randomness (0.0-1.0)
|
465
|
+
- max_tokens (int): Maximum number of tokens to generate
|
466
|
+
- stream (bool): Whether to stream the response
|
467
|
+
|
208
468
|
Returns:
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
469
|
+
CompletionResponse: The generated completion response, containing the completed text,
|
470
|
+
usage statistics, and other metadata.
|
471
|
+
|
472
|
+
Example:
|
473
|
+
```python
|
474
|
+
response = client.completion(
|
475
|
+
"Once upon a time,",
|
476
|
+
provider="openai",
|
477
|
+
model="text-davinci-003"
|
217
478
|
)
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
def generate(self, model_name: str, prompt: str, **kwargs) -> Dict[str, Any]:
|
479
|
+
|
480
|
+
print(response.text)
|
481
|
+
```
|
222
482
|
"""
|
223
|
-
|
483
|
+
return self._completions(prompt, **kwargs)
|
224
484
|
|
485
|
+
def embeddings(self, text: Union[str, List[str]], **kwargs) -> EmbeddingResponse:
|
486
|
+
"""
|
487
|
+
Generate embeddings for text.
|
488
|
+
|
225
489
|
Args:
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
490
|
+
text (Union[str, List[str]]): Text or list of texts to generate embeddings for
|
491
|
+
**kwargs: Additional parameters to pass to the provider, such as:
|
492
|
+
- provider (str): Provider ID (e.g., "openai", "cohere")
|
493
|
+
- model (str): Model ID (e.g., "text-embedding-ada-002")
|
494
|
+
- dimensions (int): Desired dimensionality of the embeddings (if supported)
|
495
|
+
|
230
496
|
Returns:
|
231
|
-
|
497
|
+
EmbeddingResponse: The generated embeddings response, containing the vector representations,
|
498
|
+
usage statistics, and other metadata.
|
499
|
+
|
500
|
+
Example:
|
501
|
+
```python
|
502
|
+
response = client.embeddings(
|
503
|
+
["Hello world", "AI is amazing"],
|
504
|
+
provider="openai",
|
505
|
+
model="text-embedding-ada-002"
|
506
|
+
)
|
507
|
+
|
508
|
+
for i, embedding in enumerate(response.embeddings):
|
509
|
+
print(f"Embedding {i} has {len(embedding)} dimensions")
|
510
|
+
```
|
232
511
|
"""
|
233
|
-
|
512
|
+
return self._embeddings(text, **kwargs)
|
234
513
|
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
514
|
+
def image(self, prompt: str, **kwargs) -> ImageResponse:
|
515
|
+
"""
|
516
|
+
Generate an image from a text prompt.
|
517
|
+
|
518
|
+
Args:
|
519
|
+
prompt (str): The text description of the image to generate
|
520
|
+
**kwargs: Additional parameters to pass to the provider, such as:
|
521
|
+
- provider (str): Provider ID (e.g., "openai", "stability")
|
522
|
+
- model (str): Model ID (e.g., "dall-e-3", "stable-diffusion-xl")
|
523
|
+
- size (str): Image size (e.g., "1024x1024")
|
524
|
+
- quality (str): Image quality (e.g., "standard", "hd")
|
525
|
+
- style (str): Image style (e.g., "vivid", "natural")
|
526
|
+
- response_format (str): Format of the response (e.g., "url", "b64_json")
|
527
|
+
|
528
|
+
Returns:
|
529
|
+
ImageResponse: The generated image response, containing the image data or URLs,
|
530
|
+
usage statistics, and other metadata.
|
531
|
+
|
532
|
+
Example:
|
533
|
+
```python
|
534
|
+
response = client.image(
|
535
|
+
"A serene landscape with mountains and a lake",
|
536
|
+
provider="openai",
|
537
|
+
model="dall-e-3",
|
538
|
+
size="1024x1024"
|
243
539
|
)
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
# Deduct actual cost
|
249
|
-
success = self.auth_manager.deduct_credits(
|
250
|
-
self.user_data["id"], response["cost"]
|
251
|
-
)
|
252
|
-
|
253
|
-
if not success:
|
254
|
-
raise RuntimeError("Credit deduction failed")
|
255
|
-
|
256
|
-
# Get updated user data
|
257
|
-
self.user_data = self.auth_manager.get_user_by_id(self.user_data["id"])
|
258
|
-
|
259
|
-
return {
|
260
|
-
"text": response["text"],
|
261
|
-
"cost": response["cost"],
|
262
|
-
"remaining_credits": self.user_data["balance"],
|
263
|
-
"model": model_name,
|
264
|
-
}
|
265
|
-
|
266
|
-
def get_balance(self) -> float:
|
540
|
+
|
541
|
+
print(f"Image URL: {response.url}")
|
542
|
+
```
|
267
543
|
"""
|
268
|
-
|
544
|
+
return self._images(prompt, **kwargs)
|
269
545
|
|
546
|
+
# Additional features
|
547
|
+
def get_usage(self) -> Dict[str, Any]:
|
548
|
+
"""
|
549
|
+
Get current usage statistics for the authenticated user.
|
550
|
+
|
270
551
|
Returns:
|
271
|
-
|
552
|
+
Dict[str, Any]: Usage statistics including token counts, request counts,
|
553
|
+
billing information, and quota limits.
|
554
|
+
|
555
|
+
Example:
|
556
|
+
```python
|
557
|
+
usage = client.get_usage()
|
558
|
+
print(f"Total tokens used: {usage['total_tokens']}")
|
559
|
+
print(f"Remaining quota: {usage['remaining_quota']}")
|
560
|
+
```
|
272
561
|
"""
|
273
|
-
|
274
|
-
self.user_data = self.auth_manager.get_user_by_id(self.user_data["id"])
|
275
|
-
return self.user_data["balance"]
|
562
|
+
return self._make_request('GET', 'usage')['data']
|
276
563
|
|
277
564
|
def get_user_info(self) -> Dict[str, Any]:
|
278
565
|
"""
|
279
|
-
Get
|
566
|
+
Get information about the authenticated user.
|
567
|
+
|
568
|
+
Returns:
|
569
|
+
Dict[str, Any]: User information including ID, name, email, account type,
|
570
|
+
subscription details, and other user-specific data.
|
571
|
+
|
572
|
+
Example:
|
573
|
+
```python
|
574
|
+
user = client.get_user_info()
|
575
|
+
print(f"User ID: {user['id']}")
|
576
|
+
print(f"Account type: {user['account_type']}")
|
577
|
+
```
|
578
|
+
"""
|
579
|
+
return self.user_info.copy()
|
280
580
|
|
581
|
+
def close(self):
|
582
|
+
"""
|
583
|
+
Clean up client resources and close the session.
|
584
|
+
|
585
|
+
This method should be called when the client is no longer needed to ensure
|
586
|
+
proper cleanup of resources, particularly the HTTP session.
|
587
|
+
|
588
|
+
Example:
|
589
|
+
```python
|
590
|
+
client = Client(api_key="your_api_key")
|
591
|
+
# Use the client...
|
592
|
+
client.close() # Clean up when done
|
593
|
+
```
|
594
|
+
"""
|
595
|
+
self.session.close()
|
596
|
+
logger.info("Client session closed")
|
597
|
+
|
598
|
+
def __enter__(self):
|
599
|
+
"""
|
600
|
+
Enter the context manager.
|
601
|
+
|
602
|
+
This method enables the client to be used as a context manager with the 'with' statement.
|
603
|
+
|
281
604
|
Returns:
|
282
|
-
|
605
|
+
Client: The client instance.
|
606
|
+
|
607
|
+
Example:
|
608
|
+
```python
|
609
|
+
with Client(api_key="your_api_key") as client:
|
610
|
+
# Use the client within this block
|
611
|
+
response = client.chat([{"role": "user", "content": "Hello!"}])
|
612
|
+
# Client is automatically closed when exiting the block
|
613
|
+
```
|
283
614
|
"""
|
284
|
-
|
285
|
-
|
286
|
-
|
615
|
+
return self
|
616
|
+
|
617
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
618
|
+
"""
|
619
|
+
Exit the context manager.
|
620
|
+
|
621
|
+
This method is called when exiting a 'with' block. It ensures the client
|
622
|
+
is properly closed, even if an exception occurs within the block.
|
623
|
+
|
624
|
+
Args:
|
625
|
+
exc_type: The exception type, if an exception was raised in the with block, otherwise None
|
626
|
+
exc_val: The exception value, if an exception was raised in the with block, otherwise None
|
627
|
+
exc_tb: The traceback, if an exception was raised in the with block, otherwise None
|
628
|
+
"""
|
629
|
+
self.close()
|
630
|
+
|
631
|
+
# Backward compatibility
|
632
|
+
IndoxRouter = Client
|