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.
Files changed (24) hide show
  1. aisbf/config.py +57 -1
  2. aisbf/handlers.py +314 -33
  3. aisbf/providers.py +164 -9
  4. {aisbf-0.2.2.data → aisbf-0.2.4.data}/data/share/aisbf/aisbf/config.py +57 -1
  5. aisbf-0.2.4.data/data/share/aisbf/aisbf/handlers.py +664 -0
  6. {aisbf-0.2.2.data → aisbf-0.2.4.data}/data/share/aisbf/aisbf/providers.py +164 -9
  7. aisbf-0.2.4.data/data/share/aisbf/main.py +421 -0
  8. {aisbf-0.2.2.dist-info → aisbf-0.2.4.dist-info}/METADATA +1 -1
  9. aisbf-0.2.4.dist-info/RECORD +24 -0
  10. aisbf-0.2.2.data/data/share/aisbf/aisbf/handlers.py +0 -383
  11. aisbf-0.2.2.data/data/share/aisbf/main.py +0 -214
  12. aisbf-0.2.2.dist-info/RECORD +0 -24
  13. {aisbf-0.2.2.data → aisbf-0.2.4.data}/data/share/aisbf/aisbf/__init__.py +0 -0
  14. {aisbf-0.2.2.data → aisbf-0.2.4.data}/data/share/aisbf/aisbf/models.py +0 -0
  15. {aisbf-0.2.2.data → aisbf-0.2.4.data}/data/share/aisbf/aisbf.sh +0 -0
  16. {aisbf-0.2.2.data → aisbf-0.2.4.data}/data/share/aisbf/autoselect.json +0 -0
  17. {aisbf-0.2.2.data → aisbf-0.2.4.data}/data/share/aisbf/autoselect.md +0 -0
  18. {aisbf-0.2.2.data → aisbf-0.2.4.data}/data/share/aisbf/providers.json +0 -0
  19. {aisbf-0.2.2.data → aisbf-0.2.4.data}/data/share/aisbf/requirements.txt +0 -0
  20. {aisbf-0.2.2.data → aisbf-0.2.4.data}/data/share/aisbf/rotations.json +0 -0
  21. {aisbf-0.2.2.dist-info → aisbf-0.2.4.dist-info}/WHEEL +0 -0
  22. {aisbf-0.2.2.dist-info → aisbf-0.2.4.dist-info}/entry_points.txt +0 -0
  23. {aisbf-0.2.2.dist-info → aisbf-0.2.4.dist-info}/licenses/LICENSE.txt +0 -0
  24. {aisbf-0.2.2.dist-info → aisbf-0.2.4.dist-info}/top_level.txt +0 -0
@@ -62,14 +62,56 @@ class BaseProviderHandler:
62
62
  self.last_request_time = time.time()
63
63
 
64
64
  def record_failure(self):
65
+ import logging
66
+ logger = logging.getLogger(__name__)
67
+
65
68
  self.error_tracking['failures'] += 1
66
69
  self.error_tracking['last_failure'] = time.time()
70
+
71
+ failure_count = self.error_tracking['failures']
72
+ logger.warning(f"=== PROVIDER FAILURE RECORDED ===")
73
+ logger.warning(f"Provider: {self.provider_id}")
74
+ logger.warning(f"Failure count: {failure_count}/3")
75
+ logger.warning(f"Last failure time: {self.error_tracking['last_failure']}")
76
+
67
77
  if self.error_tracking['failures'] >= 3:
68
78
  self.error_tracking['disabled_until'] = time.time() + 300 # 5 minutes
79
+ disabled_until_time = self.error_tracking['disabled_until']
80
+ cooldown_remaining = int(disabled_until_time - time.time())
81
+ logger.error(f"!!! PROVIDER DISABLED !!!")
82
+ logger.error(f"Provider: {self.provider_id}")
83
+ logger.error(f"Reason: 3 consecutive failures reached")
84
+ logger.error(f"Disabled until: {disabled_until_time}")
85
+ logger.error(f"Cooldown period: {cooldown_remaining} seconds (5 minutes)")
86
+ logger.error(f"Provider will be automatically re-enabled after cooldown")
87
+ else:
88
+ remaining_failures = 3 - failure_count
89
+ logger.warning(f"Provider still active. {remaining_failures} more failure(s) will disable it")
90
+ logger.warning(f"=== END FAILURE RECORDING ===")
69
91
 
70
92
  def record_success(self):
93
+ import logging
94
+ logger = logging.getLogger(__name__)
95
+
96
+ was_disabled = self.error_tracking['disabled_until'] is not None
97
+ previous_failures = self.error_tracking['failures']
98
+
71
99
  self.error_tracking['failures'] = 0
72
100
  self.error_tracking['disabled_until'] = None
101
+
102
+ logger.info(f"=== PROVIDER SUCCESS RECORDED ===")
103
+ logger.info(f"Provider: {self.provider_id}")
104
+ logger.info(f"Previous failure count: {previous_failures}")
105
+ logger.info(f"Failure count reset to: 0")
106
+
107
+ if was_disabled:
108
+ logger.info(f"!!! PROVIDER RE-ENABLED !!!")
109
+ logger.info(f"Provider: {self.provider_id}")
110
+ logger.info(f"Reason: Successful request after cooldown period")
111
+ logger.info(f"Provider is now active and available for requests")
112
+ else:
113
+ logger.info(f"Provider remains active")
114
+ logger.info(f"=== END SUCCESS RECORDING ===")
73
115
 
74
116
  class GoogleProviderHandler(BaseProviderHandler):
75
117
  def __init__(self, provider_id: str, api_key: str):
@@ -249,30 +291,122 @@ class AnthropicProviderHandler(BaseProviderHandler):
249
291
  ]
250
292
 
251
293
  class OllamaProviderHandler(BaseProviderHandler):
252
- def __init__(self, provider_id: str):
253
- super().__init__(provider_id)
254
- self.client = httpx.AsyncClient(base_url=config.providers[provider_id].endpoint)
294
+ def __init__(self, provider_id: str, api_key: Optional[str] = None):
295
+ super().__init__(provider_id, api_key)
296
+ # Increase timeout for Ollama requests (especially for cloud models)
297
+ # Using separate timeouts for connect, read, write, and pool
298
+ timeout = httpx.Timeout(
299
+ connect=60.0, # 60 seconds to establish connection
300
+ read=300.0, # 5 minutes to read response
301
+ write=60.0, # 60 seconds to write request
302
+ pool=60.0 # 60 seconds for pool acquisition
303
+ )
304
+ self.client = httpx.AsyncClient(base_url=config.providers[provider_id].endpoint, timeout=timeout)
255
305
 
256
306
  async def handle_request(self, model: str, messages: List[Dict], max_tokens: Optional[int] = None,
257
307
  temperature: Optional[float] = 1.0, stream: Optional[bool] = False) -> Dict:
308
+ import logging
309
+ import json
310
+ logger = logging.getLogger(__name__)
311
+ logger.info(f"=== OllamaProviderHandler.handle_request START ===")
312
+ logger.info(f"Provider ID: {self.provider_id}")
313
+ logger.info(f"Endpoint: {self.client.base_url}")
314
+ logger.info(f"Model: {model}")
315
+ logger.info(f"Messages count: {len(messages)}")
316
+ logger.info(f"Max tokens: {max_tokens}")
317
+ logger.info(f"Temperature: {temperature}")
318
+ logger.info(f"Stream: {stream}")
319
+ logger.info(f"API key provided: {bool(self.api_key)}")
320
+
258
321
  if self.is_rate_limited():
322
+ logger.error("Provider is rate limited")
259
323
  raise Exception("Provider rate limited")
260
324
 
261
325
  try:
326
+ # Test connection first
327
+ logger.info("Testing Ollama connection...")
328
+ try:
329
+ health_response = await self.client.get("/api/tags", timeout=10.0)
330
+ logger.info(f"Ollama health check passed: {health_response.status_code}")
331
+ logger.info(f"Available models: {health_response.json().get('models', [])}")
332
+ except Exception as e:
333
+ logger.error(f"Ollama health check failed: {str(e)}")
334
+ logger.error(f"Cannot connect to Ollama at {self.client.base_url}")
335
+ logger.error(f"Please ensure Ollama is running and accessible")
336
+ raise Exception(f"Cannot connect to Ollama at {self.client.base_url}: {str(e)}")
337
+
262
338
  # Apply rate limiting
339
+ logger.info("Applying rate limiting...")
263
340
  await self.apply_rate_limit()
341
+ logger.info("Rate limiting applied")
264
342
 
265
- response = await self.client.post("/api/generate", json={
343
+ prompt = "\n\n".join([f"{msg['role']}: {msg['content']}" for msg in messages])
344
+ logger.info(f"Prompt length: {len(prompt)} characters")
345
+
346
+ request_data = {
266
347
  "model": model,
267
- "prompt": "\n\n".join([f"{msg['role']}: {msg['content']}" for msg in messages]),
348
+ "prompt": prompt,
268
349
  "options": {
269
350
  "temperature": temperature,
270
351
  "num_predict": max_tokens
271
- }
272
- })
352
+ },
353
+ "stream": False # Explicitly disable streaming for non-streaming requests
354
+ }
355
+
356
+ # Add API key to headers if provided (for Ollama cloud models)
357
+ headers = {}
358
+ if self.api_key:
359
+ headers["Authorization"] = f"Bearer {self.api_key}"
360
+ logger.info("API key added to request headers for Ollama cloud")
361
+
362
+ logger.info(f"Sending POST request to {self.client.base_url}/api/generate")
363
+ logger.info(f"Request data: {request_data}")
364
+ logger.info(f"Request headers: {headers}")
365
+ logger.info(f"Client timeout: {self.client.timeout}")
366
+
367
+ response = await self.client.post("/api/generate", json=request_data, headers=headers)
368
+ logger.info(f"Response status code: {response.status_code}")
369
+ logger.info(f"Response content type: {response.headers.get('content-type')}")
370
+ logger.info(f"Response content length: {len(response.content)} bytes")
371
+ logger.info(f"Raw response content (first 500 chars): {response.text[:500]}")
273
372
  response.raise_for_status()
373
+
374
+ # Ollama may return multiple JSON objects, parse them all
375
+ content = response.text
376
+ logger.info(f"Attempting to parse response as JSON...")
377
+
378
+ try:
379
+ # Try parsing as single JSON first
380
+ response_json = response.json()
381
+ logger.info(f"Response parsed as single JSON: {response_json}")
382
+ except json.JSONDecodeError as e:
383
+ # If that fails, try parsing multiple JSON objects
384
+ logger.warning(f"Failed to parse as single JSON: {e}")
385
+ logger.info(f"Attempting to parse as multiple JSON objects...")
386
+
387
+ # Parse multiple JSON objects (one per line)
388
+ responses = []
389
+ for line in content.strip().split('\n'):
390
+ if line.strip():
391
+ try:
392
+ obj = json.loads(line)
393
+ responses.append(obj)
394
+ except json.JSONDecodeError as line_error:
395
+ logger.error(f"Failed to parse line: {line}")
396
+ logger.error(f"Error: {line_error}")
397
+
398
+ if not responses:
399
+ raise Exception("No valid JSON objects found in response")
400
+
401
+ # Combine responses - take the last complete response
402
+ # Ollama sends multiple chunks, we want the final one
403
+ response_json = responses[-1]
404
+ logger.info(f"Parsed {len(responses)} JSON objects, using last one: {response_json}")
405
+
406
+ logger.info(f"Final response: {response_json}")
274
407
  self.record_success()
275
- return response.json()
408
+ logger.info(f"=== OllamaProviderHandler.handle_request END ===")
409
+ return response_json
276
410
  except Exception as e:
277
411
  self.record_failure()
278
412
  raise e
@@ -294,8 +428,29 @@ PROVIDER_HANDLERS = {
294
428
  }
295
429
 
296
430
  def get_provider_handler(provider_id: str, api_key: Optional[str] = None) -> BaseProviderHandler:
431
+ import logging
432
+ logger = logging.getLogger(__name__)
433
+ logger.info(f"=== get_provider_handler START ===")
434
+ logger.info(f"Provider ID: {provider_id}")
435
+ logger.info(f"API key provided: {bool(api_key)}")
436
+
297
437
  provider_config = config.get_provider(provider_id)
438
+ logger.info(f"Provider config: {provider_config}")
439
+ logger.info(f"Provider type: {provider_config.type}")
440
+ logger.info(f"Provider endpoint: {provider_config.endpoint}")
441
+
298
442
  handler_class = PROVIDER_HANDLERS.get(provider_config.type)
443
+ logger.info(f"Handler class: {handler_class.__name__ if handler_class else 'None'}")
444
+ logger.info(f"Available handler types: {list(PROVIDER_HANDLERS.keys())}")
445
+
299
446
  if not handler_class:
447
+ logger.error(f"Unsupported provider type: {provider_config.type}")
300
448
  raise ValueError(f"Unsupported provider type: {provider_config.type}")
301
- return handler_class(provider_id, api_key)
449
+
450
+ # All handlers now accept api_key as optional parameter
451
+ logger.info(f"Creating handler with provider_id and optional api_key")
452
+ handler = handler_class(provider_id, api_key)
453
+
454
+ logger.info(f"Handler created: {handler.__class__.__name__}")
455
+ logger.info(f"=== get_provider_handler END ===")
456
+ return handler
@@ -0,0 +1,421 @@
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 {
120
+ "message": "AI Proxy Server is running",
121
+ "providers": list(config.providers.keys()),
122
+ "rotations": list(config.rotations.keys()),
123
+ "autoselect": list(config.autoselect.keys())
124
+ }
125
+
126
+ @app.get("/api/rotations")
127
+ async def list_rotations():
128
+ """List all available rotations"""
129
+ logger.info("=== LIST ROTATIONS REQUEST ===")
130
+ rotations_info = {}
131
+ for rotation_id, rotation_config in config.rotations.items():
132
+ models = []
133
+ for provider in rotation_config.providers:
134
+ for model in provider['models']:
135
+ models.append({
136
+ "name": model['name'],
137
+ "provider_id": provider['provider_id'],
138
+ "weight": model['weight'],
139
+ "rate_limit": model.get('rate_limit')
140
+ })
141
+ rotations_info[rotation_id] = {
142
+ "model_name": rotation_config.model_name,
143
+ "models": models
144
+ }
145
+ logger.info(f"Available rotations: {list(rotations_info.keys())}")
146
+ return rotations_info
147
+
148
+ @app.post("/api/rotations/chat/completions")
149
+ async def rotation_chat_completions(request: Request, body: ChatCompletionRequest):
150
+ """Handle chat completions for rotations using model name to select rotation"""
151
+ logger.info(f"=== ROTATION CHAT COMPLETION REQUEST START ===")
152
+ logger.info(f"Request path: {request.url.path}")
153
+ logger.info(f"Model requested: {body.model}")
154
+ logger.info(f"Request headers: {dict(request.headers)}")
155
+ logger.info(f"Request body: {body}")
156
+ logger.info(f"Available rotations: {list(config.rotations.keys())}")
157
+
158
+ body_dict = body.model_dump()
159
+
160
+ # Check if the model name corresponds to a rotation
161
+ if body.model not in config.rotations:
162
+ logger.error(f"Model '{body.model}' not found in rotations")
163
+ logger.error(f"Available rotations: {list(config.rotations.keys())}")
164
+ raise HTTPException(
165
+ status_code=400,
166
+ detail=f"Model '{body.model}' not found. Available rotations: {list(config.rotations.keys())}"
167
+ )
168
+
169
+ logger.info(f"Model '{body.model}' found in rotations")
170
+ logger.debug("Handling rotation request")
171
+
172
+ try:
173
+ if body.stream:
174
+ logger.debug("Handling streaming rotation request")
175
+ return await rotation_handler.handle_rotation_request(body.model, body_dict)
176
+ else:
177
+ logger.debug("Handling non-streaming rotation request")
178
+ result = await rotation_handler.handle_rotation_request(body.model, body_dict)
179
+ logger.debug(f"Rotation response result: {result}")
180
+ return result
181
+ except Exception as e:
182
+ logger.error(f"Error handling rotation chat_completions: {str(e)}", exc_info=True)
183
+ raise
184
+
185
+ @app.get("/api/rotations/models")
186
+ async def list_rotation_models():
187
+ """List all models across all rotations"""
188
+ logger.info("=== LIST ROTATION MODELS REQUEST ===")
189
+ all_models = []
190
+ for rotation_id, rotation_config in config.rotations.items():
191
+ for provider in rotation_config.providers:
192
+ for model in provider['models']:
193
+ all_models.append({
194
+ "id": f"{rotation_id}/{model['name']}",
195
+ "name": rotation_id, # Use rotation name as the model name for selection
196
+ "object": "model",
197
+ "owned_by": provider['provider_id'],
198
+ "rotation_id": rotation_id,
199
+ "actual_model": model['name'],
200
+ "provider_id": provider['provider_id'],
201
+ "weight": model['weight'],
202
+ "rate_limit": model.get('rate_limit')
203
+ })
204
+ logger.info(f"Total rotation models available: {len(all_models)}")
205
+ return {"data": all_models}
206
+
207
+ @app.get("/api/autoselect")
208
+ async def list_autoselect():
209
+ """List all available autoselect configurations"""
210
+ logger.info("=== LIST AUTOSELECT REQUEST ===")
211
+ autoselect_info = {}
212
+ for autoselect_id, autoselect_config in config.autoselect.items():
213
+ autoselect_info[autoselect_id] = {
214
+ "model_name": autoselect_config.model_name,
215
+ "description": autoselect_config.description,
216
+ "fallback": autoselect_config.fallback,
217
+ "available_models": [
218
+ {
219
+ "model_id": m.model_id,
220
+ "description": m.description
221
+ }
222
+ for m in autoselect_config.available_models
223
+ ]
224
+ }
225
+ logger.info(f"Available autoselect: {list(autoselect_info.keys())}")
226
+ return autoselect_info
227
+
228
+ @app.post("/api/autoselect/chat/completions")
229
+ async def autoselect_chat_completions(request: Request, body: ChatCompletionRequest):
230
+ """Handle chat completions for autoselect using model name to select autoselect configuration"""
231
+ logger.info(f"=== AUTOSELECT CHAT COMPLETION REQUEST START ===")
232
+ logger.info(f"Request path: {request.url.path}")
233
+ logger.info(f"Model requested: {body.model}")
234
+ logger.info(f"Request headers: {dict(request.headers)}")
235
+ logger.info(f"Request body: {body}")
236
+ logger.info(f"Available autoselect: {list(config.autoselect.keys())}")
237
+
238
+ body_dict = body.model_dump()
239
+
240
+ # Check if the model name corresponds to an autoselect configuration
241
+ if body.model not in config.autoselect:
242
+ logger.error(f"Model '{body.model}' not found in autoselect")
243
+ logger.error(f"Available autoselect: {list(config.autoselect.keys())}")
244
+ raise HTTPException(
245
+ status_code=400,
246
+ detail=f"Model '{body.model}' not found. Available autoselect: {list(config.autoselect.keys())}"
247
+ )
248
+
249
+ logger.info(f"Model '{body.model}' found in autoselect")
250
+ logger.debug("Handling autoselect request")
251
+
252
+ try:
253
+ if body.stream:
254
+ logger.debug("Handling streaming autoselect request")
255
+ return await autoselect_handler.handle_autoselect_streaming_request(body.model, body_dict)
256
+ else:
257
+ logger.debug("Handling non-streaming autoselect request")
258
+ result = await autoselect_handler.handle_autoselect_request(body.model, body_dict)
259
+ logger.debug(f"Autoselect response result: {result}")
260
+ return result
261
+ except Exception as e:
262
+ logger.error(f"Error handling autoselect chat_completions: {str(e)}", exc_info=True)
263
+ raise
264
+
265
+ @app.get("/api/autoselect/models")
266
+ async def list_autoselect_models():
267
+ """List all models across all autoselect configurations"""
268
+ logger.info("=== LIST AUTOSELECT MODELS REQUEST ===")
269
+ all_models = []
270
+ for autoselect_id, autoselect_config in config.autoselect.items():
271
+ for model_info in autoselect_config.available_models:
272
+ all_models.append({
273
+ "id": model_info.model_id,
274
+ "name": autoselect_id, # Use autoselect name as the model name for selection
275
+ "object": "model",
276
+ "owned_by": "autoselect",
277
+ "autoselect_id": autoselect_id,
278
+ "description": model_info.description,
279
+ "fallback": autoselect_config.fallback
280
+ })
281
+ logger.info(f"Total autoselect models available: {len(all_models)}")
282
+ return {"data": all_models}
283
+
284
+ @app.post("/api/{provider_id}/chat/completions")
285
+ async def chat_completions(provider_id: str, request: Request, body: ChatCompletionRequest):
286
+ logger.info(f"=== CHAT COMPLETION REQUEST START ===")
287
+ logger.info(f"Request path: {request.url.path}")
288
+ logger.info(f"Provider ID: {provider_id}")
289
+ logger.info(f"Request headers: {dict(request.headers)}")
290
+ logger.info(f"Request body: {body}")
291
+ logger.info(f"Available providers: {list(config.providers.keys())}")
292
+ logger.info(f"Available rotations: {list(config.rotations.keys())}")
293
+ logger.info(f"Available autoselect: {list(config.autoselect.keys())}")
294
+ logger.debug(f"Request headers: {dict(request.headers)}")
295
+ logger.debug(f"Request body: {body}")
296
+
297
+ body_dict = body.model_dump()
298
+
299
+ # Check if it's an autoselect
300
+ if provider_id in config.autoselect:
301
+ logger.debug("Handling autoselect request")
302
+ try:
303
+ if body.stream:
304
+ logger.debug("Handling streaming autoselect request")
305
+ return await autoselect_handler.handle_autoselect_streaming_request(provider_id, body_dict)
306
+ else:
307
+ logger.debug("Handling non-streaming autoselect request")
308
+ result = await autoselect_handler.handle_autoselect_request(provider_id, body_dict)
309
+ logger.debug(f"Autoselect response result: {result}")
310
+ return result
311
+ except Exception as e:
312
+ logger.error(f"Error handling autoselect: {str(e)}", exc_info=True)
313
+ raise
314
+
315
+ # Check if it's a rotation
316
+ if provider_id in config.rotations:
317
+ logger.info(f"Provider ID '{provider_id}' found in rotations")
318
+ logger.debug("Handling rotation request")
319
+ return await rotation_handler.handle_rotation_request(provider_id, body_dict)
320
+
321
+ # Check if it's a provider
322
+ if provider_id not in config.providers:
323
+ logger.error(f"Provider ID '{provider_id}' not found in providers")
324
+ logger.error(f"Available providers: {list(config.providers.keys())}")
325
+ logger.error(f"Available rotations: {list(config.rotations.keys())}")
326
+ logger.error(f"Available autoselect: {list(config.autoselect.keys())}")
327
+ raise HTTPException(status_code=400, detail=f"Provider {provider_id} not found")
328
+
329
+ logger.info(f"Provider ID '{provider_id}' found in providers")
330
+
331
+ provider_config = config.get_provider(provider_id)
332
+ logger.debug(f"Provider config: {provider_config}")
333
+
334
+ try:
335
+ if body.stream:
336
+ logger.debug("Handling streaming chat completion")
337
+ return await request_handler.handle_streaming_chat_completion(request, provider_id, body_dict)
338
+ else:
339
+ logger.debug("Handling non-streaming chat completion")
340
+ result = await request_handler.handle_chat_completion(request, provider_id, body_dict)
341
+ logger.debug(f"Response result: {result}")
342
+ return result
343
+ except Exception as e:
344
+ logger.error(f"Error handling chat_completions: {str(e)}", exc_info=True)
345
+ raise
346
+
347
+ @app.get("/api/{provider_id}/models")
348
+ async def list_models(request: Request, provider_id: str):
349
+ logger.debug(f"Received list_models request for provider: {provider_id}")
350
+
351
+ # Check if it's an autoselect
352
+ if provider_id in config.autoselect:
353
+ logger.debug("Handling autoselect model list request")
354
+ try:
355
+ result = await autoselect_handler.handle_autoselect_model_list(provider_id)
356
+ logger.debug(f"Autoselect models result: {result}")
357
+ return result
358
+ except Exception as e:
359
+ logger.error(f"Error handling autoselect model list: {str(e)}", exc_info=True)
360
+ raise
361
+
362
+ # Check if it's a rotation
363
+ if provider_id in config.rotations:
364
+ logger.info(f"Provider ID '{provider_id}' found in rotations")
365
+ logger.debug("Handling rotation model list request")
366
+ return await rotation_handler.handle_rotation_model_list(provider_id)
367
+
368
+ # Check if it's a provider
369
+ if provider_id not in config.providers:
370
+ logger.error(f"Provider ID '{provider_id}' not found in providers")
371
+ logger.error(f"Available providers: {list(config.providers.keys())}")
372
+ logger.error(f"Available rotations: {list(config.rotations.keys())}")
373
+ logger.error(f"Available autoselect: {list(config.autoselect.keys())}")
374
+ raise HTTPException(status_code=400, detail=f"Provider {provider_id} not found")
375
+
376
+ logger.info(f"Provider ID '{provider_id}' found in providers")
377
+
378
+ provider_config = config.get_provider(provider_id)
379
+
380
+ try:
381
+ logger.debug("Handling model list request")
382
+ result = await request_handler.handle_model_list(request, provider_id)
383
+ logger.debug(f"Models result: {result}")
384
+ return result
385
+ except Exception as e:
386
+ logger.error(f"Error handling list_models: {str(e)}", exc_info=True)
387
+ raise
388
+
389
+ @app.post("/api/{provider_id}")
390
+ async def catch_all_post(provider_id: str, request: Request):
391
+ """Catch-all for POST requests to help debug routing issues"""
392
+ logger.info(f"=== CATCH-ALL POST REQUEST ===")
393
+ logger.info(f"Request path: {request.url.path}")
394
+ logger.info(f"Provider ID: {provider_id}")
395
+ logger.info(f"Request headers: {dict(request.headers)}")
396
+ logger.info(f"Available providers: {list(config.providers.keys())}")
397
+ logger.info(f"Available rotations: {list(config.rotations.keys())}")
398
+ logger.info(f"Available autoselect: {list(config.autoselect.keys())}")
399
+
400
+ error_msg = f"""
401
+ Invalid endpoint: {request.url.path}
402
+
403
+ The correct endpoint format is: /api/{{provider_id}}/chat/completions
404
+
405
+ Available providers: {list(config.providers.keys())}
406
+ Available rotations: {list(config.rotations.keys())}
407
+ Available autoselect: {list(config.autoselect.keys())}
408
+
409
+ Example: POST /api/ollama/chat/completions
410
+ """
411
+ logger.error(error_msg)
412
+ raise HTTPException(status_code=404, detail=error_msg.strip())
413
+
414
+ def main():
415
+ """Main entry point for the AISBF server"""
416
+ import uvicorn
417
+ logger.info("Starting AI Proxy Server on http://localhost:8000")
418
+ uvicorn.run(app, host="0.0.0.0", port=8000)
419
+
420
+ if __name__ == "__main__":
421
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aisbf
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations
5
5
  Home-page: https://git.nexlab.net/nexlab/aisbf.git
6
6
  Author: AISBF Contributors
@@ -0,0 +1,24 @@
1
+ cli.py,sha256=SpjVC1iBdDhQXuhJcjVFkODu4BH-nj_1hNFD_d8wPbw,2503
2
+ aisbf/__init__.py,sha256=D3-tZRWCu31CltN_pjx8IikwPl0OGEJkvoASm8QjvcQ,2156
3
+ aisbf/config.py,sha256=xXcUQG3OcteCi8__qm6-yEnwjwFVpLBYLrs-zMnDfl0,9643
4
+ aisbf/handlers.py,sha256=tXYOQYkhIK6X1EmgivVyO6fElkrRj1cqNQfQ5KGldO8,31557
5
+ aisbf/models.py,sha256=LT1NaQVAw0VWXL-j3hdfNlXCA9HeiET_O3GDj3t9XC4,1883
6
+ aisbf/providers.py,sha256=gh3qe-bthAxb-hRPPpp_YLgGzbdTKRN5HKVjf-Yvnp0,19920
7
+ aisbf-0.2.4.data/data/share/aisbf/aisbf.sh,sha256=ntI4UPefBtU2jrTwMR3hddHEPG_pDyJyO0J3SD7e5PA,4574
8
+ aisbf-0.2.4.data/data/share/aisbf/autoselect.json,sha256=Anud0hTE1mehonmMmhOTPK2ANUxfruE2yMdLqiEkEUA,659
9
+ aisbf-0.2.4.data/data/share/aisbf/autoselect.md,sha256=F8PilhaYBs0qdpIxIkokrjtIOdhAx5Bi1tA0hyfnqps,4301
10
+ aisbf-0.2.4.data/data/share/aisbf/main.py,sha256=hCTxLA-txJpSpMTE5mV1XG_j8Kn17ZTHJsnx59nKscg,17241
11
+ aisbf-0.2.4.data/data/share/aisbf/providers.json,sha256=9L5GO6sQ2Z6zndGed0AckvYNV1DMr9r7tSdZ9fJxYlA,3934
12
+ aisbf-0.2.4.data/data/share/aisbf/requirements.txt,sha256=lp6cPakAO3lpTCwQ27THf-PNz_HIpzCELrtpdgo6-2o,133
13
+ aisbf-0.2.4.data/data/share/aisbf/rotations.json,sha256=SzbmMeTRR0vVTrYTMwxSPxjXLVr8zxjaI4HYRxjyExQ,2123
14
+ aisbf-0.2.4.data/data/share/aisbf/aisbf/__init__.py,sha256=D3-tZRWCu31CltN_pjx8IikwPl0OGEJkvoASm8QjvcQ,2156
15
+ aisbf-0.2.4.data/data/share/aisbf/aisbf/config.py,sha256=xXcUQG3OcteCi8__qm6-yEnwjwFVpLBYLrs-zMnDfl0,9643
16
+ aisbf-0.2.4.data/data/share/aisbf/aisbf/handlers.py,sha256=tXYOQYkhIK6X1EmgivVyO6fElkrRj1cqNQfQ5KGldO8,31557
17
+ aisbf-0.2.4.data/data/share/aisbf/aisbf/models.py,sha256=LT1NaQVAw0VWXL-j3hdfNlXCA9HeiET_O3GDj3t9XC4,1883
18
+ aisbf-0.2.4.data/data/share/aisbf/aisbf/providers.py,sha256=gh3qe-bthAxb-hRPPpp_YLgGzbdTKRN5HKVjf-Yvnp0,19920
19
+ aisbf-0.2.4.dist-info/licenses/LICENSE.txt,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
20
+ aisbf-0.2.4.dist-info/METADATA,sha256=A_x1Mb5ryEHXXCKpdzfFz8Pgdn7FKgR4POzRKAmbdZU,4190
21
+ aisbf-0.2.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
22
+ aisbf-0.2.4.dist-info/entry_points.txt,sha256=dV_E5f6UvgSe9AoyPTzGxBK8IYaIeLR8yTe7EwBZ3F8,35
23
+ aisbf-0.2.4.dist-info/top_level.txt,sha256=odXp1LYymu31EdVSmMGCg3ZYAI5HeB8tZkaXh9Pw3kE,10
24
+ aisbf-0.2.4.dist-info/RECORD,,