aisbf 0.2.2__py3-none-any.whl → 0.2.4__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/config.py +57 -1
- aisbf/handlers.py +314 -33
- aisbf/providers.py +164 -9
- {aisbf-0.2.2.data → aisbf-0.2.4.data}/data/share/aisbf/aisbf/config.py +57 -1
- aisbf-0.2.4.data/data/share/aisbf/aisbf/handlers.py +664 -0
- {aisbf-0.2.2.data → aisbf-0.2.4.data}/data/share/aisbf/aisbf/providers.py +164 -9
- aisbf-0.2.4.data/data/share/aisbf/main.py +421 -0
- {aisbf-0.2.2.dist-info → aisbf-0.2.4.dist-info}/METADATA +1 -1
- aisbf-0.2.4.dist-info/RECORD +24 -0
- aisbf-0.2.2.data/data/share/aisbf/aisbf/handlers.py +0 -383
- aisbf-0.2.2.data/data/share/aisbf/main.py +0 -214
- aisbf-0.2.2.dist-info/RECORD +0 -24
- {aisbf-0.2.2.data → aisbf-0.2.4.data}/data/share/aisbf/aisbf/__init__.py +0 -0
- {aisbf-0.2.2.data → aisbf-0.2.4.data}/data/share/aisbf/aisbf/models.py +0 -0
- {aisbf-0.2.2.data → aisbf-0.2.4.data}/data/share/aisbf/aisbf.sh +0 -0
- {aisbf-0.2.2.data → aisbf-0.2.4.data}/data/share/aisbf/autoselect.json +0 -0
- {aisbf-0.2.2.data → aisbf-0.2.4.data}/data/share/aisbf/autoselect.md +0 -0
- {aisbf-0.2.2.data → aisbf-0.2.4.data}/data/share/aisbf/providers.json +0 -0
- {aisbf-0.2.2.data → aisbf-0.2.4.data}/data/share/aisbf/requirements.txt +0 -0
- {aisbf-0.2.2.data → aisbf-0.2.4.data}/data/share/aisbf/rotations.json +0 -0
- {aisbf-0.2.2.dist-info → aisbf-0.2.4.dist-info}/WHEEL +0 -0
- {aisbf-0.2.2.dist-info → aisbf-0.2.4.dist-info}/entry_points.txt +0 -0
- {aisbf-0.2.2.dist-info → aisbf-0.2.4.dist-info}/licenses/LICENSE.txt +0 -0
- {aisbf-0.2.2.dist-info → aisbf-0.2.4.dist-info}/top_level.txt +0 -0
|
@@ -1,383 +0,0 @@
|
|
|
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
|
-
import re
|
|
27
|
-
from typing import Dict, List, Optional
|
|
28
|
-
from pathlib import Path
|
|
29
|
-
from fastapi import HTTPException, Request
|
|
30
|
-
from fastapi.responses import JSONResponse, StreamingResponse
|
|
31
|
-
from .models import ChatCompletionRequest, ChatCompletionResponse
|
|
32
|
-
from .providers import get_provider_handler
|
|
33
|
-
from .config import config
|
|
34
|
-
|
|
35
|
-
class RequestHandler:
|
|
36
|
-
def __init__(self):
|
|
37
|
-
self.config = config
|
|
38
|
-
|
|
39
|
-
async def handle_chat_completion(self, request: Request, provider_id: str, request_data: Dict) -> Dict:
|
|
40
|
-
provider_config = self.config.get_provider(provider_id)
|
|
41
|
-
|
|
42
|
-
if provider_config.api_key_required:
|
|
43
|
-
api_key = request_data.get('api_key') or request.headers.get('Authorization', '').replace('Bearer ', '')
|
|
44
|
-
if not api_key:
|
|
45
|
-
raise HTTPException(status_code=401, detail="API key required")
|
|
46
|
-
else:
|
|
47
|
-
api_key = None
|
|
48
|
-
|
|
49
|
-
handler = get_provider_handler(provider_id, api_key)
|
|
50
|
-
|
|
51
|
-
if handler.is_rate_limited():
|
|
52
|
-
raise HTTPException(status_code=503, detail="Provider temporarily unavailable")
|
|
53
|
-
|
|
54
|
-
try:
|
|
55
|
-
# Apply rate limiting
|
|
56
|
-
await handler.apply_rate_limit()
|
|
57
|
-
|
|
58
|
-
response = await handler.handle_request(
|
|
59
|
-
model=request_data['model'],
|
|
60
|
-
messages=request_data['messages'],
|
|
61
|
-
max_tokens=request_data.get('max_tokens'),
|
|
62
|
-
temperature=request_data.get('temperature', 1.0),
|
|
63
|
-
stream=request_data.get('stream', False)
|
|
64
|
-
)
|
|
65
|
-
handler.record_success()
|
|
66
|
-
return response
|
|
67
|
-
except Exception as e:
|
|
68
|
-
handler.record_failure()
|
|
69
|
-
raise HTTPException(status_code=500, detail=str(e))
|
|
70
|
-
|
|
71
|
-
async def handle_streaming_chat_completion(self, request: Request, provider_id: str, request_data: Dict):
|
|
72
|
-
provider_config = self.config.get_provider(provider_id)
|
|
73
|
-
|
|
74
|
-
if provider_config.api_key_required:
|
|
75
|
-
api_key = request_data.get('api_key') or request.headers.get('Authorization', '').replace('Bearer ', '')
|
|
76
|
-
if not api_key:
|
|
77
|
-
raise HTTPException(status_code=401, detail="API key required")
|
|
78
|
-
else:
|
|
79
|
-
api_key = None
|
|
80
|
-
|
|
81
|
-
handler = get_provider_handler(provider_id, api_key)
|
|
82
|
-
|
|
83
|
-
if handler.is_rate_limited():
|
|
84
|
-
raise HTTPException(status_code=503, detail="Provider temporarily unavailable")
|
|
85
|
-
|
|
86
|
-
async def stream_generator():
|
|
87
|
-
try:
|
|
88
|
-
# Apply rate limiting
|
|
89
|
-
await handler.apply_rate_limit()
|
|
90
|
-
|
|
91
|
-
response = await handler.handle_request(
|
|
92
|
-
model=request_data['model'],
|
|
93
|
-
messages=request_data['messages'],
|
|
94
|
-
max_tokens=request_data.get('max_tokens'),
|
|
95
|
-
temperature=request_data.get('temperature', 1.0),
|
|
96
|
-
stream=True
|
|
97
|
-
)
|
|
98
|
-
for chunk in response:
|
|
99
|
-
yield f"data: {chunk}\n\n".encode('utf-8')
|
|
100
|
-
handler.record_success()
|
|
101
|
-
except Exception as e:
|
|
102
|
-
handler.record_failure()
|
|
103
|
-
yield f"data: {str(e)}\n\n".encode('utf-8')
|
|
104
|
-
|
|
105
|
-
return StreamingResponse(stream_generator(), media_type="text/event-stream")
|
|
106
|
-
|
|
107
|
-
async def handle_model_list(self, request: Request, provider_id: str) -> List[Dict]:
|
|
108
|
-
provider_config = self.config.get_provider(provider_id)
|
|
109
|
-
|
|
110
|
-
if provider_config.api_key_required:
|
|
111
|
-
api_key = request.headers.get('Authorization', '').replace('Bearer ', '')
|
|
112
|
-
if not api_key:
|
|
113
|
-
raise HTTPException(status_code=401, detail="API key required")
|
|
114
|
-
else:
|
|
115
|
-
api_key = None
|
|
116
|
-
|
|
117
|
-
handler = get_provider_handler(provider_id, api_key)
|
|
118
|
-
try:
|
|
119
|
-
# Apply rate limiting
|
|
120
|
-
await handler.apply_rate_limit()
|
|
121
|
-
|
|
122
|
-
models = await handler.get_models()
|
|
123
|
-
return [model.dict() for model in models]
|
|
124
|
-
except Exception as e:
|
|
125
|
-
raise HTTPException(status_code=500, detail=str(e))
|
|
126
|
-
|
|
127
|
-
class RotationHandler:
|
|
128
|
-
def __init__(self):
|
|
129
|
-
self.config = config
|
|
130
|
-
|
|
131
|
-
async def handle_rotation_request(self, rotation_id: str, request_data: Dict) -> Dict:
|
|
132
|
-
rotation_config = self.config.get_rotation(rotation_id)
|
|
133
|
-
if not rotation_config:
|
|
134
|
-
raise HTTPException(status_code=400, detail=f"Rotation {rotation_id} not found")
|
|
135
|
-
|
|
136
|
-
providers = rotation_config.providers
|
|
137
|
-
weighted_models = []
|
|
138
|
-
|
|
139
|
-
for provider in providers:
|
|
140
|
-
for model in provider['models']:
|
|
141
|
-
weighted_models.extend([model] * model['weight'])
|
|
142
|
-
|
|
143
|
-
if not weighted_models:
|
|
144
|
-
raise HTTPException(status_code=400, detail="No models available in rotation")
|
|
145
|
-
|
|
146
|
-
import random
|
|
147
|
-
selected_model = random.choice(weighted_models)
|
|
148
|
-
|
|
149
|
-
provider_id = selected_model['provider_id']
|
|
150
|
-
api_key = selected_model.get('api_key')
|
|
151
|
-
model_name = selected_model['name']
|
|
152
|
-
|
|
153
|
-
handler = get_provider_handler(provider_id, api_key)
|
|
154
|
-
|
|
155
|
-
if handler.is_rate_limited():
|
|
156
|
-
raise HTTPException(status_code=503, detail="All providers temporarily unavailable")
|
|
157
|
-
|
|
158
|
-
try:
|
|
159
|
-
# Apply rate limiting with model-specific rate limit if available
|
|
160
|
-
rate_limit = selected_model.get('rate_limit')
|
|
161
|
-
await handler.apply_rate_limit(rate_limit)
|
|
162
|
-
|
|
163
|
-
response = await handler.handle_request(
|
|
164
|
-
model=model_name,
|
|
165
|
-
messages=request_data['messages'],
|
|
166
|
-
max_tokens=request_data.get('max_tokens'),
|
|
167
|
-
temperature=request_data.get('temperature', 1.0),
|
|
168
|
-
stream=request_data.get('stream', False)
|
|
169
|
-
)
|
|
170
|
-
handler.record_success()
|
|
171
|
-
return response
|
|
172
|
-
except Exception as e:
|
|
173
|
-
handler.record_failure()
|
|
174
|
-
raise HTTPException(status_code=500, detail=str(e))
|
|
175
|
-
|
|
176
|
-
async def handle_rotation_model_list(self, rotation_id: str) -> List[Dict]:
|
|
177
|
-
rotation_config = self.config.get_rotation(rotation_id)
|
|
178
|
-
if not rotation_config:
|
|
179
|
-
raise HTTPException(status_code=400, detail=f"Rotation {rotation_id} not found")
|
|
180
|
-
|
|
181
|
-
all_models = []
|
|
182
|
-
for provider in rotation_config.providers:
|
|
183
|
-
for model in provider['models']:
|
|
184
|
-
all_models.append({
|
|
185
|
-
"id": f"{provider['provider_id']}/{model['name']}",
|
|
186
|
-
"name": model['name'],
|
|
187
|
-
"provider_id": provider['provider_id'],
|
|
188
|
-
"weight": model['weight'],
|
|
189
|
-
"rate_limit": model.get('rate_limit')
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
return all_models
|
|
193
|
-
|
|
194
|
-
class AutoselectHandler:
|
|
195
|
-
def __init__(self):
|
|
196
|
-
self.config = config
|
|
197
|
-
self._skill_file_content = None
|
|
198
|
-
|
|
199
|
-
def _get_skill_file_content(self) -> str:
|
|
200
|
-
"""Load the autoselect.md skill file content"""
|
|
201
|
-
if self._skill_file_content is None:
|
|
202
|
-
# Try installed locations first
|
|
203
|
-
installed_dirs = [
|
|
204
|
-
Path('/usr/share/aisbf'),
|
|
205
|
-
Path.home() / '.local' / 'share' / 'aisbf',
|
|
206
|
-
]
|
|
207
|
-
|
|
208
|
-
for installed_dir in installed_dirs:
|
|
209
|
-
skill_file = installed_dir / 'autoselect.md'
|
|
210
|
-
if skill_file.exists():
|
|
211
|
-
with open(skill_file) as f:
|
|
212
|
-
self._skill_file_content = f.read()
|
|
213
|
-
return self._skill_file_content
|
|
214
|
-
|
|
215
|
-
# Fallback to source tree config directory
|
|
216
|
-
source_dir = Path(__file__).parent.parent / 'config'
|
|
217
|
-
skill_file = source_dir / 'autoselect.md'
|
|
218
|
-
if skill_file.exists():
|
|
219
|
-
with open(skill_file) as f:
|
|
220
|
-
self._skill_file_content = f.read()
|
|
221
|
-
return self._skill_file_content
|
|
222
|
-
|
|
223
|
-
raise FileNotFoundError("Could not find autoselect.md skill file")
|
|
224
|
-
|
|
225
|
-
return self._skill_file_content
|
|
226
|
-
|
|
227
|
-
def _build_autoselect_prompt(self, user_prompt: str, autoselect_config) -> str:
|
|
228
|
-
"""Build the prompt for model selection"""
|
|
229
|
-
skill_content = self._get_skill_file_content()
|
|
230
|
-
|
|
231
|
-
# Build the available models list
|
|
232
|
-
models_list = ""
|
|
233
|
-
for model_info in autoselect_config.available_models:
|
|
234
|
-
models_list += f"<model><model_id>{model_info.model_id}</model_id><model_description>{model_info.description}</model_description></model>\n"
|
|
235
|
-
|
|
236
|
-
# Build the complete prompt
|
|
237
|
-
prompt = f"""{skill_content}
|
|
238
|
-
|
|
239
|
-
<aisbf_user_prompt>{user_prompt}</aisbf_user_prompt>
|
|
240
|
-
<aisbf_autoselect_list>
|
|
241
|
-
{models_list}
|
|
242
|
-
</aisbf_autoselect_list>
|
|
243
|
-
<aisbf_autoselect_fallback>{autoselect_config.fallback}</aisbf_autoselect_fallback>
|
|
244
|
-
"""
|
|
245
|
-
return prompt
|
|
246
|
-
|
|
247
|
-
def _extract_model_selection(self, response: str) -> Optional[str]:
|
|
248
|
-
"""Extract the model_id from the autoselection response"""
|
|
249
|
-
match = re.search(r'<aisbf_model_autoselection>(.*?)</aisbf_model_autoselection>', response, re.DOTALL)
|
|
250
|
-
if match:
|
|
251
|
-
return match.group(1).strip()
|
|
252
|
-
return None
|
|
253
|
-
|
|
254
|
-
async def _get_model_selection(self, prompt: str) -> str:
|
|
255
|
-
"""Send the autoselect prompt to a model and get the selection"""
|
|
256
|
-
# Use the first available provider/model for the selection
|
|
257
|
-
# This is a simple implementation - could be enhanced to use a specific selection model
|
|
258
|
-
rotation_handler = RotationHandler()
|
|
259
|
-
|
|
260
|
-
# Create a minimal request for model selection
|
|
261
|
-
selection_request = {
|
|
262
|
-
"messages": [{"role": "user", "content": prompt}],
|
|
263
|
-
"temperature": 0.1, # Low temperature for more deterministic selection
|
|
264
|
-
"max_tokens": 100, # We only need a short response
|
|
265
|
-
"stream": False
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
# Use the fallback rotation for the selection
|
|
269
|
-
try:
|
|
270
|
-
response = await rotation_handler.handle_rotation_request("general", selection_request)
|
|
271
|
-
content = response.get('choices', [{}])[0].get('message', {}).get('content', '')
|
|
272
|
-
model_id = self._extract_model_selection(content)
|
|
273
|
-
return model_id
|
|
274
|
-
except Exception as e:
|
|
275
|
-
# If selection fails, we'll handle it in the main handler
|
|
276
|
-
return None
|
|
277
|
-
|
|
278
|
-
async def handle_autoselect_request(self, autoselect_id: str, request_data: Dict) -> Dict:
|
|
279
|
-
"""Handle an autoselect request"""
|
|
280
|
-
autoselect_config = self.config.get_autoselect(autoselect_id)
|
|
281
|
-
if not autoselect_config:
|
|
282
|
-
raise HTTPException(status_code=400, detail=f"Autoselect {autoselect_id} not found")
|
|
283
|
-
|
|
284
|
-
# Extract the user prompt from the request
|
|
285
|
-
user_messages = request_data.get('messages', [])
|
|
286
|
-
if not user_messages:
|
|
287
|
-
raise HTTPException(status_code=400, detail="No messages provided")
|
|
288
|
-
|
|
289
|
-
# Build a string representation of the user prompt
|
|
290
|
-
user_prompt = ""
|
|
291
|
-
for msg in user_messages:
|
|
292
|
-
role = msg.get('role', 'user')
|
|
293
|
-
content = msg.get('content', '')
|
|
294
|
-
if isinstance(content, list):
|
|
295
|
-
# Handle complex content (e.g., with images)
|
|
296
|
-
content = str(content)
|
|
297
|
-
user_prompt += f"{role}: {content}\n"
|
|
298
|
-
|
|
299
|
-
# Build the autoselect prompt
|
|
300
|
-
autoselect_prompt = self._build_autoselect_prompt(user_prompt, autoselect_config)
|
|
301
|
-
|
|
302
|
-
# Get the model selection
|
|
303
|
-
selected_model_id = await self._get_model_selection(autoselect_prompt)
|
|
304
|
-
|
|
305
|
-
# Validate the selected model
|
|
306
|
-
if not selected_model_id:
|
|
307
|
-
# Fallback to the configured fallback model
|
|
308
|
-
selected_model_id = autoselect_config.fallback
|
|
309
|
-
else:
|
|
310
|
-
# Check if the selected model is in the available models list
|
|
311
|
-
available_ids = [m.model_id for m in autoselect_config.available_models]
|
|
312
|
-
if selected_model_id not in available_ids:
|
|
313
|
-
selected_model_id = autoselect_config.fallback
|
|
314
|
-
|
|
315
|
-
# Now proxy the actual request to the selected rotation
|
|
316
|
-
rotation_handler = RotationHandler()
|
|
317
|
-
return await rotation_handler.handle_rotation_request(selected_model_id, request_data)
|
|
318
|
-
|
|
319
|
-
async def handle_autoselect_streaming_request(self, autoselect_id: str, request_data: Dict):
|
|
320
|
-
"""Handle an autoselect streaming request"""
|
|
321
|
-
autoselect_config = self.config.get_autoselect(autoselect_id)
|
|
322
|
-
if not autoselect_config:
|
|
323
|
-
raise HTTPException(status_code=400, detail=f"Autoselect {autoselect_id} not found")
|
|
324
|
-
|
|
325
|
-
# Extract the user prompt from the request
|
|
326
|
-
user_messages = request_data.get('messages', [])
|
|
327
|
-
if not user_messages:
|
|
328
|
-
raise HTTPException(status_code=400, detail="No messages provided")
|
|
329
|
-
|
|
330
|
-
# Build a string representation of the user prompt
|
|
331
|
-
user_prompt = ""
|
|
332
|
-
for msg in user_messages:
|
|
333
|
-
role = msg.get('role', 'user')
|
|
334
|
-
content = msg.get('content', '')
|
|
335
|
-
if isinstance(content, list):
|
|
336
|
-
content = str(content)
|
|
337
|
-
user_prompt += f"{role}: {content}\n"
|
|
338
|
-
|
|
339
|
-
# Build the autoselect prompt
|
|
340
|
-
autoselect_prompt = self._build_autoselect_prompt(user_prompt, autoselect_config)
|
|
341
|
-
|
|
342
|
-
# Get the model selection
|
|
343
|
-
selected_model_id = await self._get_model_selection(autoselect_prompt)
|
|
344
|
-
|
|
345
|
-
# Validate the selected model
|
|
346
|
-
if not selected_model_id:
|
|
347
|
-
selected_model_id = autoselect_config.fallback
|
|
348
|
-
else:
|
|
349
|
-
available_ids = [m.model_id for m in autoselect_config.available_models]
|
|
350
|
-
if selected_model_id not in available_ids:
|
|
351
|
-
selected_model_id = autoselect_config.fallback
|
|
352
|
-
|
|
353
|
-
# Now proxy the actual streaming request to the selected rotation
|
|
354
|
-
rotation_handler = RotationHandler()
|
|
355
|
-
|
|
356
|
-
async def stream_generator():
|
|
357
|
-
try:
|
|
358
|
-
response = await rotation_handler.handle_rotation_request(
|
|
359
|
-
selected_model_id,
|
|
360
|
-
{**request_data, "stream": True}
|
|
361
|
-
)
|
|
362
|
-
for chunk in response:
|
|
363
|
-
yield f"data: {chunk}\n\n".encode('utf-8')
|
|
364
|
-
except Exception as e:
|
|
365
|
-
yield f"data: {str(e)}\n\n".encode('utf-8')
|
|
366
|
-
|
|
367
|
-
return StreamingResponse(stream_generator(), media_type="text/event-stream")
|
|
368
|
-
|
|
369
|
-
async def handle_autoselect_model_list(self, autoselect_id: str) -> List[Dict]:
|
|
370
|
-
"""List available models for an autoselect endpoint"""
|
|
371
|
-
autoselect_config = self.config.get_autoselect(autoselect_id)
|
|
372
|
-
if not autoselect_config:
|
|
373
|
-
raise HTTPException(status_code=400, detail=f"Autoselect {autoselect_id} not found")
|
|
374
|
-
|
|
375
|
-
# Return the available models that can be selected
|
|
376
|
-
return [
|
|
377
|
-
{
|
|
378
|
-
"id": model_info.model_id,
|
|
379
|
-
"name": model_info.model_id,
|
|
380
|
-
"description": model_info.description
|
|
381
|
-
}
|
|
382
|
-
for model_info in autoselect_config.available_models
|
|
383
|
-
]
|
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Copyleft (C) 2026 Stefy Lanza <stefy@nexlab.net>
|
|
3
|
-
|
|
4
|
-
AISBF - AI Service Broker Framework || AI Should Be Free
|
|
5
|
-
|
|
6
|
-
Main application 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
|
-
Main application for AISBF.
|
|
24
|
-
"""
|
|
25
|
-
from fastapi import FastAPI, HTTPException, Request, status
|
|
26
|
-
from fastapi.responses import JSONResponse, StreamingResponse
|
|
27
|
-
from fastapi.middleware.cors import CORSMiddleware
|
|
28
|
-
from aisbf.models import ChatCompletionRequest, ChatCompletionResponse
|
|
29
|
-
from aisbf.handlers import RequestHandler, RotationHandler, AutoselectHandler
|
|
30
|
-
from aisbf.config import config
|
|
31
|
-
import time
|
|
32
|
-
import logging
|
|
33
|
-
import sys
|
|
34
|
-
import os
|
|
35
|
-
from logging.handlers import RotatingFileHandler
|
|
36
|
-
from datetime import datetime, timedelta
|
|
37
|
-
from collections import defaultdict
|
|
38
|
-
from pathlib import Path
|
|
39
|
-
|
|
40
|
-
def setup_logging():
|
|
41
|
-
"""Setup logging with rotating file handlers"""
|
|
42
|
-
# Determine log directory based on user
|
|
43
|
-
if os.geteuid() == 0:
|
|
44
|
-
# Running as root - use /var/log/aisbf
|
|
45
|
-
log_dir = Path('/var/log/aisbf')
|
|
46
|
-
else:
|
|
47
|
-
# Running as user - use ~/.local/var/log/aisbf
|
|
48
|
-
log_dir = Path.home() / '.local' / 'var' / 'log' / 'aisbf'
|
|
49
|
-
|
|
50
|
-
# Create log directory if it doesn't exist
|
|
51
|
-
log_dir.mkdir(parents=True, exist_ok=True)
|
|
52
|
-
|
|
53
|
-
# Setup rotating file handler for general logs
|
|
54
|
-
log_file = log_dir / 'aisbf.log'
|
|
55
|
-
file_handler = RotatingFileHandler(
|
|
56
|
-
log_file,
|
|
57
|
-
maxBytes=50*1024*1024, # 50 MB
|
|
58
|
-
backupCount=5,
|
|
59
|
-
encoding='utf-8'
|
|
60
|
-
)
|
|
61
|
-
file_handler.setLevel(logging.DEBUG)
|
|
62
|
-
file_formatter = logging.Formatter(
|
|
63
|
-
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
64
|
-
)
|
|
65
|
-
file_handler.setFormatter(file_formatter)
|
|
66
|
-
|
|
67
|
-
# Setup rotating file handler for error logs
|
|
68
|
-
error_log_file = log_dir / 'aisbf_error.log'
|
|
69
|
-
error_handler = RotatingFileHandler(
|
|
70
|
-
error_log_file,
|
|
71
|
-
maxBytes=50*1024*1024, # 50 MB
|
|
72
|
-
backupCount=5,
|
|
73
|
-
encoding='utf-8'
|
|
74
|
-
)
|
|
75
|
-
error_handler.setLevel(logging.ERROR)
|
|
76
|
-
error_handler.setFormatter(file_formatter)
|
|
77
|
-
|
|
78
|
-
# Setup console handler
|
|
79
|
-
console_handler = logging.StreamHandler(sys.stdout)
|
|
80
|
-
console_handler.setLevel(logging.INFO)
|
|
81
|
-
console_formatter = logging.Formatter(
|
|
82
|
-
'%(asctime)s - %(levelname)s - %(message)s'
|
|
83
|
-
)
|
|
84
|
-
console_handler.setFormatter(console_formatter)
|
|
85
|
-
|
|
86
|
-
# Configure root logger
|
|
87
|
-
root_logger = logging.getLogger()
|
|
88
|
-
root_logger.setLevel(logging.DEBUG)
|
|
89
|
-
root_logger.addHandler(file_handler)
|
|
90
|
-
root_logger.addHandler(error_handler)
|
|
91
|
-
root_logger.addHandler(console_handler)
|
|
92
|
-
|
|
93
|
-
# Redirect stderr to error log
|
|
94
|
-
sys.stderr = open(log_dir / 'aisbf_stderr.log', 'a')
|
|
95
|
-
|
|
96
|
-
return logging.getLogger(__name__)
|
|
97
|
-
|
|
98
|
-
# Configure logging
|
|
99
|
-
logger = setup_logging()
|
|
100
|
-
|
|
101
|
-
# Initialize handlers
|
|
102
|
-
request_handler = RequestHandler()
|
|
103
|
-
rotation_handler = RotationHandler()
|
|
104
|
-
autoselect_handler = AutoselectHandler()
|
|
105
|
-
|
|
106
|
-
app = FastAPI(title="AI Proxy Server")
|
|
107
|
-
|
|
108
|
-
# CORS middleware
|
|
109
|
-
app.add_middleware(
|
|
110
|
-
CORSMiddleware,
|
|
111
|
-
allow_origins=["*"],
|
|
112
|
-
allow_credentials=True,
|
|
113
|
-
allow_methods=["*"],
|
|
114
|
-
allow_headers=["*"],
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
@app.get("/")
|
|
118
|
-
async def root():
|
|
119
|
-
return {"message": "AI Proxy Server is running", "providers": list(config.providers.keys())}
|
|
120
|
-
|
|
121
|
-
@app.post("/api/{provider_id}/chat/completions")
|
|
122
|
-
async def chat_completions(provider_id: str, request: Request, body: ChatCompletionRequest):
|
|
123
|
-
logger.debug(f"Received chat_completions request for provider: {provider_id}")
|
|
124
|
-
logger.debug(f"Request headers: {dict(request.headers)}")
|
|
125
|
-
logger.debug(f"Request body: {body}")
|
|
126
|
-
|
|
127
|
-
body_dict = body.model_dump()
|
|
128
|
-
|
|
129
|
-
# Check if it's an autoselect
|
|
130
|
-
if provider_id in config.autoselect:
|
|
131
|
-
logger.debug("Handling autoselect request")
|
|
132
|
-
try:
|
|
133
|
-
if body.stream:
|
|
134
|
-
logger.debug("Handling streaming autoselect request")
|
|
135
|
-
return await autoselect_handler.handle_autoselect_streaming_request(provider_id, body_dict)
|
|
136
|
-
else:
|
|
137
|
-
logger.debug("Handling non-streaming autoselect request")
|
|
138
|
-
result = await autoselect_handler.handle_autoselect_request(provider_id, body_dict)
|
|
139
|
-
logger.debug(f"Autoselect response result: {result}")
|
|
140
|
-
return result
|
|
141
|
-
except Exception as e:
|
|
142
|
-
logger.error(f"Error handling autoselect: {str(e)}", exc_info=True)
|
|
143
|
-
raise
|
|
144
|
-
|
|
145
|
-
# Check if it's a rotation
|
|
146
|
-
if provider_id in config.rotations:
|
|
147
|
-
logger.debug("Handling rotation request")
|
|
148
|
-
return await rotation_handler.handle_rotation_request(provider_id, body_dict)
|
|
149
|
-
|
|
150
|
-
# Check if it's a provider
|
|
151
|
-
if provider_id not in config.providers:
|
|
152
|
-
logger.error(f"Provider {provider_id} not found")
|
|
153
|
-
raise HTTPException(status_code=400, detail=f"Provider {provider_id} not found")
|
|
154
|
-
|
|
155
|
-
provider_config = config.get_provider(provider_id)
|
|
156
|
-
logger.debug(f"Provider config: {provider_config}")
|
|
157
|
-
|
|
158
|
-
try:
|
|
159
|
-
if body.stream:
|
|
160
|
-
logger.debug("Handling streaming chat completion")
|
|
161
|
-
return await request_handler.handle_streaming_chat_completion(request, provider_id, body_dict)
|
|
162
|
-
else:
|
|
163
|
-
logger.debug("Handling non-streaming chat completion")
|
|
164
|
-
result = await request_handler.handle_chat_completion(request, provider_id, body_dict)
|
|
165
|
-
logger.debug(f"Response result: {result}")
|
|
166
|
-
return result
|
|
167
|
-
except Exception as e:
|
|
168
|
-
logger.error(f"Error handling chat_completions: {str(e)}", exc_info=True)
|
|
169
|
-
raise
|
|
170
|
-
|
|
171
|
-
@app.get("/api/{provider_id}/models")
|
|
172
|
-
async def list_models(request: Request, provider_id: str):
|
|
173
|
-
logger.debug(f"Received list_models request for provider: {provider_id}")
|
|
174
|
-
|
|
175
|
-
# Check if it's an autoselect
|
|
176
|
-
if provider_id in config.autoselect:
|
|
177
|
-
logger.debug("Handling autoselect model list request")
|
|
178
|
-
try:
|
|
179
|
-
result = await autoselect_handler.handle_autoselect_model_list(provider_id)
|
|
180
|
-
logger.debug(f"Autoselect models result: {result}")
|
|
181
|
-
return result
|
|
182
|
-
except Exception as e:
|
|
183
|
-
logger.error(f"Error handling autoselect model list: {str(e)}", exc_info=True)
|
|
184
|
-
raise
|
|
185
|
-
|
|
186
|
-
# Check if it's a rotation
|
|
187
|
-
if provider_id in config.rotations:
|
|
188
|
-
logger.debug("Handling rotation model list request")
|
|
189
|
-
return await rotation_handler.handle_rotation_model_list(provider_id)
|
|
190
|
-
|
|
191
|
-
# Check if it's a provider
|
|
192
|
-
if provider_id not in config.providers:
|
|
193
|
-
logger.error(f"Provider {provider_id} not found")
|
|
194
|
-
raise HTTPException(status_code=400, detail=f"Provider {provider_id} not found")
|
|
195
|
-
|
|
196
|
-
provider_config = config.get_provider(provider_id)
|
|
197
|
-
|
|
198
|
-
try:
|
|
199
|
-
logger.debug("Handling model list request")
|
|
200
|
-
result = await request_handler.handle_model_list(request, provider_id)
|
|
201
|
-
logger.debug(f"Models result: {result}")
|
|
202
|
-
return result
|
|
203
|
-
except Exception as e:
|
|
204
|
-
logger.error(f"Error handling list_models: {str(e)}", exc_info=True)
|
|
205
|
-
raise
|
|
206
|
-
|
|
207
|
-
def main():
|
|
208
|
-
"""Main entry point for the AISBF server"""
|
|
209
|
-
import uvicorn
|
|
210
|
-
logger.info("Starting AI Proxy Server on http://localhost:8000")
|
|
211
|
-
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
212
|
-
|
|
213
|
-
if __name__ == "__main__":
|
|
214
|
-
main()
|
aisbf-0.2.2.dist-info/RECORD
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
cli.py,sha256=SpjVC1iBdDhQXuhJcjVFkODu4BH-nj_1hNFD_d8wPbw,2503
|
|
2
|
-
aisbf/__init__.py,sha256=D3-tZRWCu31CltN_pjx8IikwPl0OGEJkvoASm8QjvcQ,2156
|
|
3
|
-
aisbf/config.py,sha256=-pJa69vw_d_VuGPYGgKNm0hhMEgbVYSg2ZdWNl8LubY,6349
|
|
4
|
-
aisbf/handlers.py,sha256=exBfP1oLHYxNvq_xqVbWCl1NPxZgn-tlZL7j7acE558,15828
|
|
5
|
-
aisbf/models.py,sha256=LT1NaQVAw0VWXL-j3hdfNlXCA9HeiET_O3GDj3t9XC4,1883
|
|
6
|
-
aisbf/providers.py,sha256=jQGKYqrb-1RB3s4XrKK7AbX-7W5Cl3HG7Yc_riMkcyA,11942
|
|
7
|
-
aisbf-0.2.2.data/data/share/aisbf/aisbf.sh,sha256=ntI4UPefBtU2jrTwMR3hddHEPG_pDyJyO0J3SD7e5PA,4574
|
|
8
|
-
aisbf-0.2.2.data/data/share/aisbf/autoselect.json,sha256=Anud0hTE1mehonmMmhOTPK2ANUxfruE2yMdLqiEkEUA,659
|
|
9
|
-
aisbf-0.2.2.data/data/share/aisbf/autoselect.md,sha256=F8PilhaYBs0qdpIxIkokrjtIOdhAx5Bi1tA0hyfnqps,4301
|
|
10
|
-
aisbf-0.2.2.data/data/share/aisbf/main.py,sha256=ikv0-3KVHbawExNy6X4juE_pjZKaggkv-qAcUrqCw_s,7809
|
|
11
|
-
aisbf-0.2.2.data/data/share/aisbf/providers.json,sha256=9L5GO6sQ2Z6zndGed0AckvYNV1DMr9r7tSdZ9fJxYlA,3934
|
|
12
|
-
aisbf-0.2.2.data/data/share/aisbf/requirements.txt,sha256=lp6cPakAO3lpTCwQ27THf-PNz_HIpzCELrtpdgo6-2o,133
|
|
13
|
-
aisbf-0.2.2.data/data/share/aisbf/rotations.json,sha256=SzbmMeTRR0vVTrYTMwxSPxjXLVr8zxjaI4HYRxjyExQ,2123
|
|
14
|
-
aisbf-0.2.2.data/data/share/aisbf/aisbf/__init__.py,sha256=D3-tZRWCu31CltN_pjx8IikwPl0OGEJkvoASm8QjvcQ,2156
|
|
15
|
-
aisbf-0.2.2.data/data/share/aisbf/aisbf/config.py,sha256=-pJa69vw_d_VuGPYGgKNm0hhMEgbVYSg2ZdWNl8LubY,6349
|
|
16
|
-
aisbf-0.2.2.data/data/share/aisbf/aisbf/handlers.py,sha256=exBfP1oLHYxNvq_xqVbWCl1NPxZgn-tlZL7j7acE558,15828
|
|
17
|
-
aisbf-0.2.2.data/data/share/aisbf/aisbf/models.py,sha256=LT1NaQVAw0VWXL-j3hdfNlXCA9HeiET_O3GDj3t9XC4,1883
|
|
18
|
-
aisbf-0.2.2.data/data/share/aisbf/aisbf/providers.py,sha256=jQGKYqrb-1RB3s4XrKK7AbX-7W5Cl3HG7Yc_riMkcyA,11942
|
|
19
|
-
aisbf-0.2.2.dist-info/licenses/LICENSE.txt,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
20
|
-
aisbf-0.2.2.dist-info/METADATA,sha256=gi0VlbVGDAR2oAwVMmJaOU-8xIksAY3vKMI2i6UGXbE,4190
|
|
21
|
-
aisbf-0.2.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
22
|
-
aisbf-0.2.2.dist-info/entry_points.txt,sha256=dV_E5f6UvgSe9AoyPTzGxBK8IYaIeLR8yTe7EwBZ3F8,35
|
|
23
|
-
aisbf-0.2.2.dist-info/top_level.txt,sha256=odXp1LYymu31EdVSmMGCg3ZYAI5HeB8tZkaXh9Pw3kE,10
|
|
24
|
-
aisbf-0.2.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|