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.
Files changed (53) hide show
  1. indoxRouter/__init__.py +83 -0
  2. indoxRouter/client.py +564 -218
  3. indoxRouter/client_resourses/__init__.py +20 -0
  4. indoxRouter/client_resourses/base.py +67 -0
  5. indoxRouter/client_resourses/chat.py +144 -0
  6. indoxRouter/client_resourses/completion.py +138 -0
  7. indoxRouter/client_resourses/embedding.py +83 -0
  8. indoxRouter/client_resourses/image.py +116 -0
  9. indoxRouter/client_resourses/models.py +114 -0
  10. indoxRouter/config.py +151 -0
  11. indoxRouter/constants/__init__.py +81 -0
  12. indoxRouter/exceptions/__init__.py +70 -0
  13. indoxRouter/models/__init__.py +111 -0
  14. indoxRouter/providers/__init__.py +50 -50
  15. indoxRouter/providers/ai21labs.json +128 -0
  16. indoxRouter/providers/base_provider.py +62 -30
  17. indoxRouter/providers/claude.json +164 -0
  18. indoxRouter/providers/cohere.json +116 -0
  19. indoxRouter/providers/databricks.json +110 -0
  20. indoxRouter/providers/deepseek.json +110 -0
  21. indoxRouter/providers/google.json +128 -0
  22. indoxRouter/providers/meta.json +128 -0
  23. indoxRouter/providers/mistral.json +146 -0
  24. indoxRouter/providers/nvidia.json +110 -0
  25. indoxRouter/providers/openai.json +308 -0
  26. indoxRouter/providers/openai.py +471 -72
  27. indoxRouter/providers/qwen.json +110 -0
  28. indoxRouter/utils/__init__.py +240 -0
  29. indoxrouter-0.1.2.dist-info/LICENSE +21 -0
  30. indoxrouter-0.1.2.dist-info/METADATA +259 -0
  31. indoxrouter-0.1.2.dist-info/RECORD +33 -0
  32. indoxRouter/api_endpoints.py +0 -336
  33. indoxRouter/client_package.py +0 -138
  34. indoxRouter/init_db.py +0 -71
  35. indoxRouter/main.py +0 -711
  36. indoxRouter/migrations/__init__.py +0 -1
  37. indoxRouter/migrations/env.py +0 -98
  38. indoxRouter/migrations/versions/__init__.py +0 -1
  39. indoxRouter/migrations/versions/initial_schema.py +0 -84
  40. indoxRouter/providers/ai21.py +0 -268
  41. indoxRouter/providers/claude.py +0 -177
  42. indoxRouter/providers/cohere.py +0 -171
  43. indoxRouter/providers/databricks.py +0 -166
  44. indoxRouter/providers/deepseek.py +0 -166
  45. indoxRouter/providers/google.py +0 -216
  46. indoxRouter/providers/llama.py +0 -164
  47. indoxRouter/providers/meta.py +0 -227
  48. indoxRouter/providers/mistral.py +0 -182
  49. indoxRouter/providers/nvidia.py +0 -164
  50. indoxrouter-0.1.0.dist-info/METADATA +0 -179
  51. indoxrouter-0.1.0.dist-info/RECORD +0 -27
  52. {indoxrouter-0.1.0.dist-info → indoxrouter-0.1.2.dist-info}/WHEEL +0 -0
  53. {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
- from typing import Dict, Optional, Any, List
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 sys
4
- import json
46
+ import logging
47
+ from datetime import datetime, timedelta
48
+ from typing import Dict, List, Any, Optional, Union
5
49
  import requests
6
-
7
- # Add the parent directory to the path to make imports work correctly
8
- current_dir = os.path.dirname(os.path.abspath(__file__))
9
- parent_dir = os.path.dirname(current_dir)
10
- sys.path.append(parent_dir)
11
-
12
- # Use absolute imports
13
- from indoxRouter.utils.auth import AuthManager
14
- from indoxRouter.providers.base_provider import BaseProvider
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 making API requests to the IndoxRouter API.
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__(self, api_key: str, base_url: str = None):
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
- provider: str,
42
- model: str,
43
- prompt: str,
44
- temperature: float = 0.7,
45
- max_tokens: int = 1000,
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
- Generate a response from a model.
122
+ Initialize the client with authentication and session management
50
123
 
51
124
  Args:
52
- provider: Provider name
53
- model: Model name
54
- prompt: Prompt
55
- temperature: Temperature
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
- url = f"{self.base_url}/v1/completions"
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
- headers = {
65
- "Content-Type": "application/json",
66
- "Authorization": f"Bearer {self.api_key}",
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
- data = {
70
- "provider": provider,
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
- response = requests.post(url, headers=headers, json=data)
231
+ logger.debug("Successfully refreshed authentication token")
79
232
 
80
- if response.status_code != 200:
81
- error_message = (
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
- return response.json().get("choices", [{}])[0].get("text", "")
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 list_models(self, provider: Optional[str] = None) -> List[Dict[str, Any]]:
246
+ def _make_request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
89
247
  """
90
- List available models.
91
-
92
- Args:
93
- provider: Provider name (optional)
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
- url = f"{self.base_url}/v1/models"
254
+ self._check_auth()
255
+ self._check_rate_limit(endpoint)
99
256
 
100
- if provider:
101
- url += f"?provider={provider}"
257
+ request_id = uuid4().hex
258
+ url = f"{self.base_url}/{endpoint}"
259
+ start_time = datetime.now()
102
260
 
103
- headers = {
104
- "Authorization": f"Bearer {self.api_key}",
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
- response = requests.get(url, headers=headers)
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 response.status_code != 200:
110
- error_message = (
111
- response.json().get("error", {}).get("message", "Unknown error")
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
- return response.json().get("data", [])
352
+ logger.error("Request %s failed: %s", request_id, error)
353
+ return error_info
116
354
 
117
- def list_providers(self) -> List[str]:
355
+ # Resource proxy methods
356
+ def providers(self) -> List[Dict[str, Any]]:
118
357
  """
119
- List available providers.
120
-
358
+ Get a list of all available AI providers.
359
+
121
360
  Returns:
122
- List of providers
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
- url = f"{self.base_url}/v1/providers"
371
+ return self._make_request('GET', 'providers')['data']
125
372
 
126
- headers = {
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
- Parse model name into provider and model parts
143
-
375
+ Get available models, optionally filtered by provider.
376
+
144
377
  Args:
145
- model_name: Full model name (e.g., 'openai/gpt-4')
146
-
378
+ provider (Optional[str]): Provider ID to filter models by. If None, returns models from all providers.
379
+
147
380
  Returns:
148
- Tuple of (provider_name, model_part)
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
- if "/" not in model_name:
151
- raise ValueError(
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 _load_provider_class(self, provider_name: str):
396
+ def model_info(self, provider: str, model: str) -> ModelInfo:
159
397
  """
160
- Dynamically load provider class
161
-
398
+ Get detailed information about a specific model.
399
+
162
400
  Args:
163
- provider_name: Name of the provider
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
- Provider class
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
- # Import the provider module dynamically
170
- module_path = f".providers.{provider_name}"
171
- provider_module = __import__(
172
- module_path, fromlist=["Provider"], globals=globals()
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 _get_provider(self, model_name: str) -> BaseProvider:
425
+ def chat(self, messages: List[Union[Dict[str, str], ChatMessage]], **kwargs) -> ChatResponse:
179
426
  """
180
- Get provider instance with cached credentials
181
-
427
+ Generate a chat completion from a series of messages.
428
+
182
429
  Args:
183
- model_name: Full model name (e.g., 'openai/gpt-4')
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
- Provider instance
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
- if model_name in self.provider_cache:
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
- # Get provider API key from secure storage
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
- Retrieve provider API key from secure storage
204
-
457
+ Generate a text completion from a prompt.
458
+
205
459
  Args:
206
- provider_name: Name of the provider
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
- Provider API key
210
- """
211
- # Implement your secure credential storage (e.g., AWS Secrets Manager)
212
- # Example using environment variables:
213
- env_var = f"{provider_name.upper()}_API_KEY"
214
- if env_var not in os.environ:
215
- raise ValueError(
216
- f"Missing API key for provider: {provider_name}. Set {env_var} environment variable."
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
- return os.environ[env_var]
220
-
221
- def generate(self, model_name: str, prompt: str, **kwargs) -> Dict[str, Any]:
479
+
480
+ print(response.text)
481
+ ```
222
482
  """
223
- Generate completion with credit handling
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
- model_name: Provider/model name (e.g., 'openai/gpt-4')
227
- prompt: User input prompt
228
- **kwargs: Generation parameters
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
- Dictionary with response and credit information
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
- provider = self._get_provider(model_name)
512
+ return self._embeddings(text, **kwargs)
234
513
 
235
- # Estimate max possible cost
236
- max_tokens = kwargs.get("max_tokens", 2048)
237
- estimated_cost = provider.estimate_cost(prompt, max_tokens)
238
-
239
- # Check balance
240
- if self.user_data["balance"] < estimated_cost:
241
- raise ValueError(
242
- f"Insufficient credits. Required: {estimated_cost:.6f}, Available: {self.user_data['balance']:.6f}"
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
- # Make API call
246
- response = provider.generate(prompt, **kwargs)
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
- Get current user balance
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
- Current credit balance
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
- # Refresh user data to get the latest balance
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 current user information
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
- User information dictionary
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
- # Refresh user data
285
- self.user_data = self.auth_manager.get_user_by_id(self.user_data["id"])
286
- return self.user_data
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