aisbf 0.1.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.
- aisbf/__init__.py +72 -0
- aisbf/config.py +144 -0
- aisbf/handlers.py +190 -0
- aisbf/models.py +67 -0
- aisbf/providers.py +301 -0
- aisbf-0.1.0.dist-info/METADATA +134 -0
- aisbf-0.1.0.dist-info/RECORD +10 -0
- aisbf-0.1.0.dist-info/WHEEL +5 -0
- aisbf-0.1.0.dist-info/licenses/LICENSE.txt +674 -0
- aisbf-0.1.0.dist-info/top_level.txt +1 -0
aisbf/__init__.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyleft (C) 2026 Stefy Lanza <stefy@nexlab.net>
|
|
3
|
+
|
|
4
|
+
AISBF - AI Service Broker Framework || AI Should Be Free
|
|
5
|
+
|
|
6
|
+
A modular proxy server for managing multiple AI provider integrations.
|
|
7
|
+
|
|
8
|
+
This program is free software: you can redistribute it and/or modify
|
|
9
|
+
it under the terms of the GNU General Public License as published by
|
|
10
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
11
|
+
(at your option) any later version.
|
|
12
|
+
|
|
13
|
+
This program is distributed in the hope that it will be useful,
|
|
14
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
GNU General Public License for more details.
|
|
17
|
+
|
|
18
|
+
You should have received a copy of the GNU General Public License
|
|
19
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
20
|
+
|
|
21
|
+
Why did the programmer quit his job? Because he didn't get arrays!
|
|
22
|
+
|
|
23
|
+
A modular proxy server for managing multiple AI provider integrations.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from .config import config, Config, ProviderConfig, RotationConfig, AppConfig
|
|
27
|
+
from .models import (
|
|
28
|
+
Message,
|
|
29
|
+
ChatCompletionRequest,
|
|
30
|
+
ChatCompletionResponse,
|
|
31
|
+
Model,
|
|
32
|
+
Provider,
|
|
33
|
+
ErrorTracking
|
|
34
|
+
)
|
|
35
|
+
from .providers import (
|
|
36
|
+
BaseProviderHandler,
|
|
37
|
+
GoogleProviderHandler,
|
|
38
|
+
OpenAIProviderHandler,
|
|
39
|
+
AnthropicProviderHandler,
|
|
40
|
+
OllamaProviderHandler,
|
|
41
|
+
get_provider_handler,
|
|
42
|
+
PROVIDER_HANDLERS
|
|
43
|
+
)
|
|
44
|
+
from .handlers import RequestHandler, RotationHandler
|
|
45
|
+
|
|
46
|
+
__version__ = "0.1.0"
|
|
47
|
+
__all__ = [
|
|
48
|
+
# Config
|
|
49
|
+
"config",
|
|
50
|
+
"Config",
|
|
51
|
+
"ProviderConfig",
|
|
52
|
+
"RotationConfig",
|
|
53
|
+
"AppConfig",
|
|
54
|
+
# Models
|
|
55
|
+
"Message",
|
|
56
|
+
"ChatCompletionRequest",
|
|
57
|
+
"ChatCompletionResponse",
|
|
58
|
+
"Model",
|
|
59
|
+
"Provider",
|
|
60
|
+
"ErrorTracking",
|
|
61
|
+
# Providers
|
|
62
|
+
"BaseProviderHandler",
|
|
63
|
+
"GoogleProviderHandler",
|
|
64
|
+
"OpenAIProviderHandler",
|
|
65
|
+
"AnthropicProviderHandler",
|
|
66
|
+
"OllamaProviderHandler",
|
|
67
|
+
"get_provider_handler",
|
|
68
|
+
"PROVIDER_HANDLERS",
|
|
69
|
+
# Handlers
|
|
70
|
+
"RequestHandler",
|
|
71
|
+
"RotationHandler",
|
|
72
|
+
]
|
aisbf/config.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyleft (C) 2026 Stefy Lanza <stefy@nexlab.net>
|
|
3
|
+
|
|
4
|
+
AISBF - AI Service Broker Framework || AI Should Be Free
|
|
5
|
+
|
|
6
|
+
Configuration management for AISBF.
|
|
7
|
+
|
|
8
|
+
This program is free software: you can redistribute it and/or modify
|
|
9
|
+
it under the terms of the GNU General Public License as published by
|
|
10
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
11
|
+
(at your option) any later version.
|
|
12
|
+
|
|
13
|
+
This program is distributed in the hope that it will be useful,
|
|
14
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
GNU General Public License for more details.
|
|
17
|
+
|
|
18
|
+
You should have received a copy of the GNU General Public License
|
|
19
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
20
|
+
|
|
21
|
+
Why did the programmer quit his job? Because he didn't get arrays!
|
|
22
|
+
|
|
23
|
+
Configuration management for AISBF.
|
|
24
|
+
"""
|
|
25
|
+
from typing import Dict, List, Optional
|
|
26
|
+
from pydantic import BaseModel, Field
|
|
27
|
+
import json
|
|
28
|
+
import shutil
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
class ProviderConfig(BaseModel):
|
|
32
|
+
id: str
|
|
33
|
+
name: str
|
|
34
|
+
endpoint: str
|
|
35
|
+
type: str
|
|
36
|
+
api_key_required: bool
|
|
37
|
+
|
|
38
|
+
class RotationConfig(BaseModel):
|
|
39
|
+
providers: List[Dict]
|
|
40
|
+
|
|
41
|
+
class AppConfig(BaseModel):
|
|
42
|
+
providers: Dict[str, ProviderConfig]
|
|
43
|
+
rotations: Dict[str, RotationConfig]
|
|
44
|
+
error_tracking: Dict[str, Dict]
|
|
45
|
+
|
|
46
|
+
class Config:
|
|
47
|
+
def __init__(self):
|
|
48
|
+
self._ensure_config_directory()
|
|
49
|
+
self._load_providers()
|
|
50
|
+
self._load_rotations()
|
|
51
|
+
self._initialize_error_tracking()
|
|
52
|
+
|
|
53
|
+
def _get_config_source_dir(self):
|
|
54
|
+
"""Get the directory containing default config files"""
|
|
55
|
+
# Try installed location first
|
|
56
|
+
installed_dirs = [
|
|
57
|
+
Path('/usr/local/share/aisbf'),
|
|
58
|
+
Path.home() / '.local' / 'share' / 'aisbf',
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
for installed_dir in installed_dirs:
|
|
62
|
+
if installed_dir.exists() and (installed_dir / 'providers.json').exists():
|
|
63
|
+
return installed_dir
|
|
64
|
+
|
|
65
|
+
# Fallback to source tree config directory
|
|
66
|
+
# This is for development mode
|
|
67
|
+
source_dir = Path(__file__).parent.parent.parent / 'config'
|
|
68
|
+
if source_dir.exists() and (source_dir / 'providers.json').exists():
|
|
69
|
+
return source_dir
|
|
70
|
+
|
|
71
|
+
# Last resort: try the old location in the package directory
|
|
72
|
+
package_dir = Path(__file__).parent
|
|
73
|
+
if (package_dir / 'providers.json').exists():
|
|
74
|
+
return package_dir
|
|
75
|
+
|
|
76
|
+
raise FileNotFoundError("Could not find configuration files")
|
|
77
|
+
|
|
78
|
+
def _ensure_config_directory(self):
|
|
79
|
+
"""Ensure ~/.aisbf/ directory exists and copy default config files if needed"""
|
|
80
|
+
config_dir = Path.home() / '.aisbf'
|
|
81
|
+
|
|
82
|
+
# Create config directory if it doesn't exist
|
|
83
|
+
config_dir.mkdir(exist_ok=True)
|
|
84
|
+
|
|
85
|
+
# Get the source directory for default config files
|
|
86
|
+
try:
|
|
87
|
+
source_dir = self._get_config_source_dir()
|
|
88
|
+
except FileNotFoundError:
|
|
89
|
+
print("Warning: Could not find default configuration files")
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Copy default config files if they don't exist
|
|
93
|
+
for config_file in ['providers.json', 'rotations.json']:
|
|
94
|
+
src = source_dir / config_file
|
|
95
|
+
dst = config_dir / config_file
|
|
96
|
+
|
|
97
|
+
if not dst.exists() and src.exists():
|
|
98
|
+
shutil.copy2(src, dst)
|
|
99
|
+
print(f"Created default config file: {dst}")
|
|
100
|
+
|
|
101
|
+
def _load_providers(self):
|
|
102
|
+
providers_path = Path.home() / '.aisbf' / 'providers.json'
|
|
103
|
+
if not providers_path.exists():
|
|
104
|
+
# Fallback to source config if user config doesn't exist
|
|
105
|
+
try:
|
|
106
|
+
source_dir = self._get_config_source_dir()
|
|
107
|
+
providers_path = source_dir / 'providers.json'
|
|
108
|
+
except FileNotFoundError:
|
|
109
|
+
raise FileNotFoundError("Could not find providers.json configuration file")
|
|
110
|
+
|
|
111
|
+
with open(providers_path) as f:
|
|
112
|
+
data = json.load(f)
|
|
113
|
+
self.providers = {k: ProviderConfig(**v) for k, v in data['providers'].items()}
|
|
114
|
+
|
|
115
|
+
def _load_rotations(self):
|
|
116
|
+
rotations_path = Path.home() / '.aisbf' / 'rotations.json'
|
|
117
|
+
if not rotations_path.exists():
|
|
118
|
+
# Fallback to source config if user config doesn't exist
|
|
119
|
+
try:
|
|
120
|
+
source_dir = self._get_config_source_dir()
|
|
121
|
+
rotations_path = source_dir / 'rotations.json'
|
|
122
|
+
except FileNotFoundError:
|
|
123
|
+
raise FileNotFoundError("Could not find rotations.json configuration file")
|
|
124
|
+
|
|
125
|
+
with open(rotations_path) as f:
|
|
126
|
+
data = json.load(f)
|
|
127
|
+
self.rotations = {k: RotationConfig(**v) for k, v in data['rotations'].items()}
|
|
128
|
+
|
|
129
|
+
def _initialize_error_tracking(self):
|
|
130
|
+
self.error_tracking = {}
|
|
131
|
+
for provider_id in self.providers:
|
|
132
|
+
self.error_tracking[provider_id] = {
|
|
133
|
+
'failures': 0,
|
|
134
|
+
'last_failure': None,
|
|
135
|
+
'disabled_until': None
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
def get_provider(self, provider_id: str) -> ProviderConfig:
|
|
139
|
+
return self.providers.get(provider_id)
|
|
140
|
+
|
|
141
|
+
def get_rotation(self, rotation_id: str) -> RotationConfig:
|
|
142
|
+
return self.rotations.get(rotation_id)
|
|
143
|
+
|
|
144
|
+
config = Config()
|
aisbf/handlers.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyleft (C) 2026 Stefy Lanza <stefy@nexlab.net>
|
|
3
|
+
|
|
4
|
+
AISBF - AI Service Broker Framework || AI Should Be Free
|
|
5
|
+
|
|
6
|
+
Request handlers for AISBF.
|
|
7
|
+
|
|
8
|
+
This program is free software: you can redistribute it and/or modify
|
|
9
|
+
it under the terms of the GNU General Public License as published by
|
|
10
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
11
|
+
(at your option) any later version.
|
|
12
|
+
|
|
13
|
+
This program is distributed in the hope that it will be useful,
|
|
14
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
GNU General Public License for more details.
|
|
17
|
+
|
|
18
|
+
You should have received a copy of the GNU General Public License
|
|
19
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
20
|
+
|
|
21
|
+
Why did the programmer quit his job? Because he didn't get arrays!
|
|
22
|
+
|
|
23
|
+
Request handlers for AISBF.
|
|
24
|
+
"""
|
|
25
|
+
import asyncio
|
|
26
|
+
from typing import Dict, List, Optional
|
|
27
|
+
from fastapi import HTTPException, Request
|
|
28
|
+
from fastapi.responses import JSONResponse, StreamingResponse
|
|
29
|
+
from .models import ChatCompletionRequest, ChatCompletionResponse
|
|
30
|
+
from .providers import get_provider_handler
|
|
31
|
+
from .config import config
|
|
32
|
+
|
|
33
|
+
class RequestHandler:
|
|
34
|
+
def __init__(self):
|
|
35
|
+
self.config = config
|
|
36
|
+
|
|
37
|
+
async def handle_chat_completion(self, request: Request, provider_id: str, request_data: Dict) -> Dict:
|
|
38
|
+
provider_config = self.config.get_provider(provider_id)
|
|
39
|
+
|
|
40
|
+
if provider_config.api_key_required:
|
|
41
|
+
api_key = request_data.get('api_key') or request.headers.get('Authorization', '').replace('Bearer ', '')
|
|
42
|
+
if not api_key:
|
|
43
|
+
raise HTTPException(status_code=401, detail="API key required")
|
|
44
|
+
else:
|
|
45
|
+
api_key = None
|
|
46
|
+
|
|
47
|
+
handler = get_provider_handler(provider_id, api_key)
|
|
48
|
+
|
|
49
|
+
if handler.is_rate_limited():
|
|
50
|
+
raise HTTPException(status_code=503, detail="Provider temporarily unavailable")
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
# Apply rate limiting
|
|
54
|
+
await handler.apply_rate_limit()
|
|
55
|
+
|
|
56
|
+
response = await handler.handle_request(
|
|
57
|
+
model=request_data['model'],
|
|
58
|
+
messages=request_data['messages'],
|
|
59
|
+
max_tokens=request_data.get('max_tokens'),
|
|
60
|
+
temperature=request_data.get('temperature', 1.0),
|
|
61
|
+
stream=request_data.get('stream', False)
|
|
62
|
+
)
|
|
63
|
+
handler.record_success()
|
|
64
|
+
return response
|
|
65
|
+
except Exception as e:
|
|
66
|
+
handler.record_failure()
|
|
67
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
68
|
+
|
|
69
|
+
async def handle_streaming_chat_completion(self, request: Request, provider_id: str, request_data: Dict):
|
|
70
|
+
provider_config = self.config.get_provider(provider_id)
|
|
71
|
+
|
|
72
|
+
if provider_config.api_key_required:
|
|
73
|
+
api_key = request_data.get('api_key') or request.headers.get('Authorization', '').replace('Bearer ', '')
|
|
74
|
+
if not api_key:
|
|
75
|
+
raise HTTPException(status_code=401, detail="API key required")
|
|
76
|
+
else:
|
|
77
|
+
api_key = None
|
|
78
|
+
|
|
79
|
+
handler = get_provider_handler(provider_id, api_key)
|
|
80
|
+
|
|
81
|
+
if handler.is_rate_limited():
|
|
82
|
+
raise HTTPException(status_code=503, detail="Provider temporarily unavailable")
|
|
83
|
+
|
|
84
|
+
async def stream_generator():
|
|
85
|
+
try:
|
|
86
|
+
# Apply rate limiting
|
|
87
|
+
await handler.apply_rate_limit()
|
|
88
|
+
|
|
89
|
+
response = await handler.handle_request(
|
|
90
|
+
model=request_data['model'],
|
|
91
|
+
messages=request_data['messages'],
|
|
92
|
+
max_tokens=request_data.get('max_tokens'),
|
|
93
|
+
temperature=request_data.get('temperature', 1.0),
|
|
94
|
+
stream=True
|
|
95
|
+
)
|
|
96
|
+
for chunk in response:
|
|
97
|
+
yield f"data: {chunk}\n\n".encode('utf-8')
|
|
98
|
+
handler.record_success()
|
|
99
|
+
except Exception as e:
|
|
100
|
+
handler.record_failure()
|
|
101
|
+
yield f"data: {str(e)}\n\n".encode('utf-8')
|
|
102
|
+
|
|
103
|
+
return StreamingResponse(stream_generator(), media_type="text/event-stream")
|
|
104
|
+
|
|
105
|
+
async def handle_model_list(self, request: Request, provider_id: str) -> List[Dict]:
|
|
106
|
+
provider_config = self.config.get_provider(provider_id)
|
|
107
|
+
|
|
108
|
+
if provider_config.api_key_required:
|
|
109
|
+
api_key = request.headers.get('Authorization', '').replace('Bearer ', '')
|
|
110
|
+
if not api_key:
|
|
111
|
+
raise HTTPException(status_code=401, detail="API key required")
|
|
112
|
+
else:
|
|
113
|
+
api_key = None
|
|
114
|
+
|
|
115
|
+
handler = get_provider_handler(provider_id, api_key)
|
|
116
|
+
try:
|
|
117
|
+
# Apply rate limiting
|
|
118
|
+
await handler.apply_rate_limit()
|
|
119
|
+
|
|
120
|
+
models = await handler.get_models()
|
|
121
|
+
return [model.dict() for model in models]
|
|
122
|
+
except Exception as e:
|
|
123
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
124
|
+
|
|
125
|
+
class RotationHandler:
|
|
126
|
+
def __init__(self):
|
|
127
|
+
self.config = config
|
|
128
|
+
|
|
129
|
+
async def handle_rotation_request(self, rotation_id: str, request_data: Dict) -> Dict:
|
|
130
|
+
rotation_config = self.config.get_rotation(rotation_id)
|
|
131
|
+
if not rotation_config:
|
|
132
|
+
raise HTTPException(status_code=400, detail=f"Rotation {rotation_id} not found")
|
|
133
|
+
|
|
134
|
+
providers = rotation_config.providers
|
|
135
|
+
weighted_models = []
|
|
136
|
+
|
|
137
|
+
for provider in providers:
|
|
138
|
+
for model in provider['models']:
|
|
139
|
+
weighted_models.extend([model] * model['weight'])
|
|
140
|
+
|
|
141
|
+
if not weighted_models:
|
|
142
|
+
raise HTTPException(status_code=400, detail="No models available in rotation")
|
|
143
|
+
|
|
144
|
+
import random
|
|
145
|
+
selected_model = random.choice(weighted_models)
|
|
146
|
+
|
|
147
|
+
provider_id = selected_model['provider_id']
|
|
148
|
+
api_key = selected_model.get('api_key')
|
|
149
|
+
model_name = selected_model['name']
|
|
150
|
+
|
|
151
|
+
handler = get_provider_handler(provider_id, api_key)
|
|
152
|
+
|
|
153
|
+
if handler.is_rate_limited():
|
|
154
|
+
raise HTTPException(status_code=503, detail="All providers temporarily unavailable")
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
# Apply rate limiting with model-specific rate limit if available
|
|
158
|
+
rate_limit = selected_model.get('rate_limit')
|
|
159
|
+
await handler.apply_rate_limit(rate_limit)
|
|
160
|
+
|
|
161
|
+
response = await handler.handle_request(
|
|
162
|
+
model=model_name,
|
|
163
|
+
messages=request_data['messages'],
|
|
164
|
+
max_tokens=request_data.get('max_tokens'),
|
|
165
|
+
temperature=request_data.get('temperature', 1.0),
|
|
166
|
+
stream=request_data.get('stream', False)
|
|
167
|
+
)
|
|
168
|
+
handler.record_success()
|
|
169
|
+
return response
|
|
170
|
+
except Exception as e:
|
|
171
|
+
handler.record_failure()
|
|
172
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
173
|
+
|
|
174
|
+
async def handle_rotation_model_list(self, rotation_id: str) -> List[Dict]:
|
|
175
|
+
rotation_config = self.config.get_rotation(rotation_id)
|
|
176
|
+
if not rotation_config:
|
|
177
|
+
raise HTTPException(status_code=400, detail=f"Rotation {rotation_id} not found")
|
|
178
|
+
|
|
179
|
+
all_models = []
|
|
180
|
+
for provider in rotation_config.providers:
|
|
181
|
+
for model in provider['models']:
|
|
182
|
+
all_models.append({
|
|
183
|
+
"id": f"{provider['provider_id']}/{model['name']}",
|
|
184
|
+
"name": model['name'],
|
|
185
|
+
"provider_id": provider['provider_id'],
|
|
186
|
+
"weight": model['weight'],
|
|
187
|
+
"rate_limit": model.get('rate_limit')
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
return all_models
|
aisbf/models.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyleft (C) 2026 Stefy Lanza <stefy@nexlab.net>
|
|
3
|
+
|
|
4
|
+
AISBF - AI Service Broker Framework || AI Should Be Free
|
|
5
|
+
|
|
6
|
+
A modular proxy server for managing multiple AI provider integrations.
|
|
7
|
+
|
|
8
|
+
This program is free software: you can redistribute it and/or modify
|
|
9
|
+
it under the terms of the GNU General Public License as published by
|
|
10
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
11
|
+
(at your option) any later version.
|
|
12
|
+
|
|
13
|
+
This program is distributed in the hope that it will be useful,
|
|
14
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
GNU General Public License for more details.
|
|
17
|
+
|
|
18
|
+
You should have received a copy of the GNU General Public License
|
|
19
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
20
|
+
|
|
21
|
+
Why did the programmer quit his job? Because he didn't get arrays!
|
|
22
|
+
|
|
23
|
+
A modular proxy server for managing multiple AI provider integrations.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from pydantic import BaseModel, Field
|
|
27
|
+
from typing import Dict, List, Optional, Union
|
|
28
|
+
|
|
29
|
+
class Message(BaseModel):
|
|
30
|
+
role: str
|
|
31
|
+
content: Union[str, List[Dict]]
|
|
32
|
+
|
|
33
|
+
class ChatCompletionRequest(BaseModel):
|
|
34
|
+
model: str
|
|
35
|
+
messages: List[Message]
|
|
36
|
+
max_tokens: Optional[int] = None
|
|
37
|
+
temperature: Optional[float] = 1.0
|
|
38
|
+
stream: Optional[bool] = False
|
|
39
|
+
|
|
40
|
+
class ChatCompletionResponse(BaseModel):
|
|
41
|
+
id: str
|
|
42
|
+
object: str
|
|
43
|
+
created: int
|
|
44
|
+
model: str
|
|
45
|
+
choices: List[Dict]
|
|
46
|
+
usage: Optional[Dict] = None
|
|
47
|
+
stream: Optional[bool] = False
|
|
48
|
+
|
|
49
|
+
class Model(BaseModel):
|
|
50
|
+
id: str
|
|
51
|
+
name: str
|
|
52
|
+
provider_id: str
|
|
53
|
+
weight: int = 1
|
|
54
|
+
rate_limit: Optional[float] = None
|
|
55
|
+
|
|
56
|
+
class Provider(BaseModel):
|
|
57
|
+
id: str
|
|
58
|
+
name: str
|
|
59
|
+
type: str
|
|
60
|
+
endpoint: str
|
|
61
|
+
api_key_required: bool
|
|
62
|
+
models: List[Model] = []
|
|
63
|
+
|
|
64
|
+
class ErrorTracking(BaseModel):
|
|
65
|
+
failures: int
|
|
66
|
+
last_failure: Optional[int]
|
|
67
|
+
disabled_until: Optional[int]
|