aisbf 0.2.2__tar.gz → 0.2.3__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aisbf
3
- Version: 0.2.2
3
+ Version: 0.2.3
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
@@ -34,6 +34,7 @@ class ProviderConfig(BaseModel):
34
34
  endpoint: str
35
35
  type: str
36
36
  api_key_required: bool
37
+ rate_limit: float = 0.0
37
38
 
38
39
  class RotationConfig(BaseModel):
39
40
  providers: List[Dict]
@@ -111,32 +112,58 @@ class Config:
111
112
  print(f"Created default config file: {dst}")
112
113
 
113
114
  def _load_providers(self):
115
+ import logging
116
+ logger = logging.getLogger(__name__)
117
+ logger.info(f"=== Config._load_providers START ===")
118
+
114
119
  providers_path = Path.home() / '.aisbf' / 'providers.json'
120
+ logger.info(f"Looking for providers at: {providers_path}")
121
+
115
122
  if not providers_path.exists():
123
+ logger.info(f"User config not found, falling back to source config")
116
124
  # Fallback to source config if user config doesn't exist
117
125
  try:
118
126
  source_dir = self._get_config_source_dir()
119
127
  providers_path = source_dir / 'providers.json'
128
+ logger.info(f"Using source config at: {providers_path}")
120
129
  except FileNotFoundError:
130
+ logger.error("Could not find providers.json configuration file")
121
131
  raise FileNotFoundError("Could not find providers.json configuration file")
122
132
 
133
+ logger.info(f"Loading providers from: {providers_path}")
123
134
  with open(providers_path) as f:
124
135
  data = json.load(f)
125
136
  self.providers = {k: ProviderConfig(**v) for k, v in data['providers'].items()}
137
+ logger.info(f"Loaded {len(self.providers)} providers: {list(self.providers.keys())}")
138
+ for provider_id, provider_config in self.providers.items():
139
+ logger.info(f" - {provider_id}: type={provider_config.type}, endpoint={provider_config.endpoint}")
140
+ logger.info(f"=== Config._load_providers END ===")
126
141
 
127
142
  def _load_rotations(self):
143
+ import logging
144
+ logger = logging.getLogger(__name__)
145
+ logger.info(f"=== Config._load_rotations START ===")
146
+
128
147
  rotations_path = Path.home() / '.aisbf' / 'rotations.json'
148
+ logger.info(f"Looking for rotations at: {rotations_path}")
149
+
129
150
  if not rotations_path.exists():
151
+ logger.info(f"User config not found, falling back to source config")
130
152
  # Fallback to source config if user config doesn't exist
131
153
  try:
132
154
  source_dir = self._get_config_source_dir()
133
155
  rotations_path = source_dir / 'rotations.json'
156
+ logger.info(f"Using source config at: {rotations_path}")
134
157
  except FileNotFoundError:
158
+ logger.error("Could not find rotations.json configuration file")
135
159
  raise FileNotFoundError("Could not find rotations.json configuration file")
136
160
 
161
+ logger.info(f"Loading rotations from: {rotations_path}")
137
162
  with open(rotations_path) as f:
138
163
  data = json.load(f)
139
164
  self.rotations = {k: RotationConfig(**v) for k, v in data['rotations'].items()}
165
+ logger.info(f"Loaded {len(self.rotations)} rotations: {list(self.rotations.keys())}")
166
+ logger.info(f"=== Config._load_rotations END ===")
140
167
 
141
168
  def _load_autoselect(self):
142
169
  autoselect_path = Path.home() / '.aisbf' / 'autoselect.json'
@@ -162,7 +189,16 @@ class Config:
162
189
  }
163
190
 
164
191
  def get_provider(self, provider_id: str) -> ProviderConfig:
165
- return self.providers.get(provider_id)
192
+ import logging
193
+ logger = logging.getLogger(__name__)
194
+ logger.info(f"Config.get_provider called with provider_id: {provider_id}")
195
+ logger.info(f"Available providers: {list(self.providers.keys())}")
196
+ result = self.providers.get(provider_id)
197
+ if result:
198
+ logger.info(f"Found provider: {result}")
199
+ else:
200
+ logger.warning(f"Provider {provider_id} not found!")
201
+ return result
166
202
 
167
203
  def get_rotation(self, rotation_id: str) -> RotationConfig:
168
204
  return self.rotations.get(rotation_id)
@@ -37,24 +37,47 @@ class RequestHandler:
37
37
  self.config = config
38
38
 
39
39
  async def handle_chat_completion(self, request: Request, provider_id: str, request_data: Dict) -> Dict:
40
+ import logging
41
+ logger = logging.getLogger(__name__)
42
+ logger.info(f"=== RequestHandler.handle_chat_completion START ===")
43
+ logger.info(f"Provider ID: {provider_id}")
44
+ logger.info(f"Request data: {request_data}")
45
+
40
46
  provider_config = self.config.get_provider(provider_id)
47
+ logger.info(f"Provider config: {provider_config}")
48
+ logger.info(f"Provider type: {provider_config.type}")
49
+ logger.info(f"Provider endpoint: {provider_config.endpoint}")
50
+ logger.info(f"API key required: {provider_config.api_key_required}")
41
51
 
42
52
  if provider_config.api_key_required:
43
53
  api_key = request_data.get('api_key') or request.headers.get('Authorization', '').replace('Bearer ', '')
54
+ logger.info(f"API key from request: {'***' if api_key else 'None'}")
44
55
  if not api_key:
45
56
  raise HTTPException(status_code=401, detail="API key required")
46
57
  else:
47
58
  api_key = None
59
+ logger.info("No API key required for this provider")
48
60
 
61
+ logger.info(f"Getting provider handler for {provider_id}")
49
62
  handler = get_provider_handler(provider_id, api_key)
63
+ logger.info(f"Provider handler obtained: {handler.__class__.__name__}")
50
64
 
51
65
  if handler.is_rate_limited():
52
66
  raise HTTPException(status_code=503, detail="Provider temporarily unavailable")
53
67
 
54
68
  try:
69
+ logger.info(f"Model requested: {request_data.get('model')}")
70
+ logger.info(f"Messages count: {len(request_data.get('messages', []))}")
71
+ logger.info(f"Max tokens: {request_data.get('max_tokens')}")
72
+ logger.info(f"Temperature: {request_data.get('temperature', 1.0)}")
73
+ logger.info(f"Stream: {request_data.get('stream', False)}")
74
+
55
75
  # Apply rate limiting
76
+ logger.info("Applying rate limiting...")
56
77
  await handler.apply_rate_limit()
78
+ logger.info("Rate limiting applied")
57
79
 
80
+ logger.info(f"Sending request to provider handler...")
58
81
  response = await handler.handle_request(
59
82
  model=request_data['model'],
60
83
  messages=request_data['messages'],
@@ -62,7 +85,9 @@ class RequestHandler:
62
85
  temperature=request_data.get('temperature', 1.0),
63
86
  stream=request_data.get('stream', False)
64
87
  )
88
+ logger.info(f"Response received from provider")
65
89
  handler.record_success()
90
+ logger.info(f"=== RequestHandler.handle_chat_completion END ===")
66
91
  return response
67
92
  except Exception as e:
68
93
  handler.record_failure()
@@ -129,37 +154,67 @@ class RotationHandler:
129
154
  self.config = config
130
155
 
131
156
  async def handle_rotation_request(self, rotation_id: str, request_data: Dict) -> Dict:
157
+ import logging
158
+ logger = logging.getLogger(__name__)
159
+ logger.info(f"=== RotationHandler.handle_rotation_request START ===")
160
+ logger.info(f"Rotation ID: {rotation_id}")
161
+
132
162
  rotation_config = self.config.get_rotation(rotation_id)
133
163
  if not rotation_config:
164
+ logger.error(f"Rotation {rotation_id} not found")
134
165
  raise HTTPException(status_code=400, detail=f"Rotation {rotation_id} not found")
135
166
 
167
+ logger.info(f"Rotation config: {rotation_config}")
136
168
  providers = rotation_config.providers
169
+ logger.info(f"Number of providers in rotation: {len(providers)}")
170
+
137
171
  weighted_models = []
138
172
 
139
173
  for provider in providers:
174
+ logger.info(f"Processing provider: {provider['provider_id']}")
140
175
  for model in provider['models']:
176
+ logger.info(f" Model: {model['name']}, weight: {model['weight']}")
141
177
  weighted_models.extend([model] * model['weight'])
142
178
 
179
+ logger.info(f"Total weighted models: {len(weighted_models)}")
143
180
  if not weighted_models:
181
+ logger.error("No models available in rotation")
144
182
  raise HTTPException(status_code=400, detail="No models available in rotation")
145
183
 
146
184
  import random
147
185
  selected_model = random.choice(weighted_models)
186
+ logger.info(f"Selected model: {selected_model}")
148
187
 
149
188
  provider_id = selected_model['provider_id']
150
189
  api_key = selected_model.get('api_key')
151
190
  model_name = selected_model['name']
191
+
192
+ logger.info(f"Selected provider_id: {provider_id}")
193
+ logger.info(f"Selected model_name: {model_name}")
194
+ logger.info(f"API key present: {bool(api_key)}")
152
195
 
196
+ logger.info(f"Getting provider handler for {provider_id}")
153
197
  handler = get_provider_handler(provider_id, api_key)
198
+ logger.info(f"Provider handler obtained: {handler.__class__.__name__}")
154
199
 
155
200
  if handler.is_rate_limited():
156
201
  raise HTTPException(status_code=503, detail="All providers temporarily unavailable")
157
202
 
158
203
  try:
204
+ logger.info(f"Model requested: {model_name}")
205
+ logger.info(f"Messages count: {len(request_data.get('messages', []))}")
206
+ logger.info(f"Max tokens: {request_data.get('max_tokens')}")
207
+ logger.info(f"Temperature: {request_data.get('temperature', 1.0)}")
208
+ logger.info(f"Stream: {request_data.get('stream', False)}")
209
+
159
210
  # Apply rate limiting with model-specific rate limit if available
160
211
  rate_limit = selected_model.get('rate_limit')
212
+ logger.info(f"Model-specific rate limit: {rate_limit}")
213
+ logger.info("Applying rate limiting...")
161
214
  await handler.apply_rate_limit(rate_limit)
215
+ logger.info("Rate limiting applied")
162
216
 
217
+ logger.info(f"Sending request to provider handler...")
163
218
  response = await handler.handle_request(
164
219
  model=model_name,
165
220
  messages=request_data['messages'],
@@ -167,7 +222,9 @@ class RotationHandler:
167
222
  temperature=request_data.get('temperature', 1.0),
168
223
  stream=request_data.get('stream', False)
169
224
  )
225
+ logger.info(f"Response received from provider")
170
226
  handler.record_success()
227
+ logger.info(f"=== RotationHandler.handle_rotation_request END ===")
171
228
  return response
172
229
  except Exception as e:
173
230
  handler.record_failure()
@@ -249,30 +249,122 @@ class AnthropicProviderHandler(BaseProviderHandler):
249
249
  ]
250
250
 
251
251
  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)
252
+ def __init__(self, provider_id: str, api_key: Optional[str] = None):
253
+ super().__init__(provider_id, api_key)
254
+ # Increase timeout for Ollama requests (especially for cloud models)
255
+ # Using separate timeouts for connect, read, write, and pool
256
+ timeout = httpx.Timeout(
257
+ connect=60.0, # 60 seconds to establish connection
258
+ read=300.0, # 5 minutes to read response
259
+ write=60.0, # 60 seconds to write request
260
+ pool=60.0 # 60 seconds for pool acquisition
261
+ )
262
+ self.client = httpx.AsyncClient(base_url=config.providers[provider_id].endpoint, timeout=timeout)
255
263
 
256
264
  async def handle_request(self, model: str, messages: List[Dict], max_tokens: Optional[int] = None,
257
265
  temperature: Optional[float] = 1.0, stream: Optional[bool] = False) -> Dict:
266
+ import logging
267
+ import json
268
+ logger = logging.getLogger(__name__)
269
+ logger.info(f"=== OllamaProviderHandler.handle_request START ===")
270
+ logger.info(f"Provider ID: {self.provider_id}")
271
+ logger.info(f"Endpoint: {self.client.base_url}")
272
+ logger.info(f"Model: {model}")
273
+ logger.info(f"Messages count: {len(messages)}")
274
+ logger.info(f"Max tokens: {max_tokens}")
275
+ logger.info(f"Temperature: {temperature}")
276
+ logger.info(f"Stream: {stream}")
277
+ logger.info(f"API key provided: {bool(self.api_key)}")
278
+
258
279
  if self.is_rate_limited():
280
+ logger.error("Provider is rate limited")
259
281
  raise Exception("Provider rate limited")
260
282
 
261
283
  try:
284
+ # Test connection first
285
+ logger.info("Testing Ollama connection...")
286
+ try:
287
+ health_response = await self.client.get("/api/tags", timeout=10.0)
288
+ logger.info(f"Ollama health check passed: {health_response.status_code}")
289
+ logger.info(f"Available models: {health_response.json().get('models', [])}")
290
+ except Exception as e:
291
+ logger.error(f"Ollama health check failed: {str(e)}")
292
+ logger.error(f"Cannot connect to Ollama at {self.client.base_url}")
293
+ logger.error(f"Please ensure Ollama is running and accessible")
294
+ raise Exception(f"Cannot connect to Ollama at {self.client.base_url}: {str(e)}")
295
+
262
296
  # Apply rate limiting
297
+ logger.info("Applying rate limiting...")
263
298
  await self.apply_rate_limit()
299
+ logger.info("Rate limiting applied")
264
300
 
265
- response = await self.client.post("/api/generate", json={
301
+ prompt = "\n\n".join([f"{msg['role']}: {msg['content']}" for msg in messages])
302
+ logger.info(f"Prompt length: {len(prompt)} characters")
303
+
304
+ request_data = {
266
305
  "model": model,
267
- "prompt": "\n\n".join([f"{msg['role']}: {msg['content']}" for msg in messages]),
306
+ "prompt": prompt,
268
307
  "options": {
269
308
  "temperature": temperature,
270
309
  "num_predict": max_tokens
271
- }
272
- })
310
+ },
311
+ "stream": False # Explicitly disable streaming for non-streaming requests
312
+ }
313
+
314
+ # Add API key to headers if provided (for Ollama cloud models)
315
+ headers = {}
316
+ if self.api_key:
317
+ headers["Authorization"] = f"Bearer {self.api_key}"
318
+ logger.info("API key added to request headers for Ollama cloud")
319
+
320
+ logger.info(f"Sending POST request to {self.client.base_url}/api/generate")
321
+ logger.info(f"Request data: {request_data}")
322
+ logger.info(f"Request headers: {headers}")
323
+ logger.info(f"Client timeout: {self.client.timeout}")
324
+
325
+ response = await self.client.post("/api/generate", json=request_data, headers=headers)
326
+ logger.info(f"Response status code: {response.status_code}")
327
+ logger.info(f"Response content type: {response.headers.get('content-type')}")
328
+ logger.info(f"Response content length: {len(response.content)} bytes")
329
+ logger.info(f"Raw response content (first 500 chars): {response.text[:500]}")
273
330
  response.raise_for_status()
331
+
332
+ # Ollama may return multiple JSON objects, parse them all
333
+ content = response.text
334
+ logger.info(f"Attempting to parse response as JSON...")
335
+
336
+ try:
337
+ # Try parsing as single JSON first
338
+ response_json = response.json()
339
+ logger.info(f"Response parsed as single JSON: {response_json}")
340
+ except json.JSONDecodeError as e:
341
+ # If that fails, try parsing multiple JSON objects
342
+ logger.warning(f"Failed to parse as single JSON: {e}")
343
+ logger.info(f"Attempting to parse as multiple JSON objects...")
344
+
345
+ # Parse multiple JSON objects (one per line)
346
+ responses = []
347
+ for line in content.strip().split('\n'):
348
+ if line.strip():
349
+ try:
350
+ obj = json.loads(line)
351
+ responses.append(obj)
352
+ except json.JSONDecodeError as line_error:
353
+ logger.error(f"Failed to parse line: {line}")
354
+ logger.error(f"Error: {line_error}")
355
+
356
+ if not responses:
357
+ raise Exception("No valid JSON objects found in response")
358
+
359
+ # Combine responses - take the last complete response
360
+ # Ollama sends multiple chunks, we want the final one
361
+ response_json = responses[-1]
362
+ logger.info(f"Parsed {len(responses)} JSON objects, using last one: {response_json}")
363
+
364
+ logger.info(f"Final response: {response_json}")
274
365
  self.record_success()
275
- return response.json()
366
+ logger.info(f"=== OllamaProviderHandler.handle_request END ===")
367
+ return response_json
276
368
  except Exception as e:
277
369
  self.record_failure()
278
370
  raise e
@@ -294,8 +386,29 @@ PROVIDER_HANDLERS = {
294
386
  }
295
387
 
296
388
  def get_provider_handler(provider_id: str, api_key: Optional[str] = None) -> BaseProviderHandler:
389
+ import logging
390
+ logger = logging.getLogger(__name__)
391
+ logger.info(f"=== get_provider_handler START ===")
392
+ logger.info(f"Provider ID: {provider_id}")
393
+ logger.info(f"API key provided: {bool(api_key)}")
394
+
297
395
  provider_config = config.get_provider(provider_id)
396
+ logger.info(f"Provider config: {provider_config}")
397
+ logger.info(f"Provider type: {provider_config.type}")
398
+ logger.info(f"Provider endpoint: {provider_config.endpoint}")
399
+
298
400
  handler_class = PROVIDER_HANDLERS.get(provider_config.type)
401
+ logger.info(f"Handler class: {handler_class.__name__ if handler_class else 'None'}")
402
+ logger.info(f"Available handler types: {list(PROVIDER_HANDLERS.keys())}")
403
+
299
404
  if not handler_class:
405
+ logger.error(f"Unsupported provider type: {provider_config.type}")
300
406
  raise ValueError(f"Unsupported provider type: {provider_config.type}")
301
- return handler_class(provider_id, api_key)
407
+
408
+ # All handlers now accept api_key as optional parameter
409
+ logger.info(f"Creating handler with provider_id and optional api_key")
410
+ handler = handler_class(provider_id, api_key)
411
+
412
+ logger.info(f"Handler created: {handler.__class__.__name__}")
413
+ logger.info(f"=== get_provider_handler END ===")
414
+ return handler
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aisbf
3
- Version: 0.2.2
3
+ Version: 0.2.3
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
@@ -120,7 +120,14 @@ async def root():
120
120
 
121
121
  @app.post("/api/{provider_id}/chat/completions")
122
122
  async def chat_completions(provider_id: str, request: Request, body: ChatCompletionRequest):
123
- logger.debug(f"Received chat_completions request for provider: {provider_id}")
123
+ logger.info(f"=== CHAT COMPLETION REQUEST START ===")
124
+ logger.info(f"Request path: {request.url.path}")
125
+ logger.info(f"Provider ID: {provider_id}")
126
+ logger.info(f"Request headers: {dict(request.headers)}")
127
+ logger.info(f"Request body: {body}")
128
+ logger.info(f"Available providers: {list(config.providers.keys())}")
129
+ logger.info(f"Available rotations: {list(config.rotations.keys())}")
130
+ logger.info(f"Available autoselect: {list(config.autoselect.keys())}")
124
131
  logger.debug(f"Request headers: {dict(request.headers)}")
125
132
  logger.debug(f"Request body: {body}")
126
133
 
@@ -144,13 +151,19 @@ async def chat_completions(provider_id: str, request: Request, body: ChatComplet
144
151
 
145
152
  # Check if it's a rotation
146
153
  if provider_id in config.rotations:
154
+ logger.info(f"Provider ID '{provider_id}' found in rotations")
147
155
  logger.debug("Handling rotation request")
148
156
  return await rotation_handler.handle_rotation_request(provider_id, body_dict)
149
157
 
150
158
  # Check if it's a provider
151
159
  if provider_id not in config.providers:
152
- logger.error(f"Provider {provider_id} not found")
160
+ logger.error(f"Provider ID '{provider_id}' not found in providers")
161
+ logger.error(f"Available providers: {list(config.providers.keys())}")
162
+ logger.error(f"Available rotations: {list(config.rotations.keys())}")
163
+ logger.error(f"Available autoselect: {list(config.autoselect.keys())}")
153
164
  raise HTTPException(status_code=400, detail=f"Provider {provider_id} not found")
165
+
166
+ logger.info(f"Provider ID '{provider_id}' found in providers")
154
167
 
155
168
  provider_config = config.get_provider(provider_id)
156
169
  logger.debug(f"Provider config: {provider_config}")
@@ -185,13 +198,19 @@ async def list_models(request: Request, provider_id: str):
185
198
 
186
199
  # Check if it's a rotation
187
200
  if provider_id in config.rotations:
201
+ logger.info(f"Provider ID '{provider_id}' found in rotations")
188
202
  logger.debug("Handling rotation model list request")
189
203
  return await rotation_handler.handle_rotation_model_list(provider_id)
190
204
 
191
205
  # Check if it's a provider
192
206
  if provider_id not in config.providers:
193
- logger.error(f"Provider {provider_id} not found")
207
+ logger.error(f"Provider ID '{provider_id}' not found in providers")
208
+ logger.error(f"Available providers: {list(config.providers.keys())}")
209
+ logger.error(f"Available rotations: {list(config.rotations.keys())}")
210
+ logger.error(f"Available autoselect: {list(config.autoselect.keys())}")
194
211
  raise HTTPException(status_code=400, detail=f"Provider {provider_id} not found")
212
+
213
+ logger.info(f"Provider ID '{provider_id}' found in providers")
195
214
 
196
215
  provider_config = config.get_provider(provider_id)
197
216
 
@@ -204,6 +223,31 @@ async def list_models(request: Request, provider_id: str):
204
223
  logger.error(f"Error handling list_models: {str(e)}", exc_info=True)
205
224
  raise
206
225
 
226
+ @app.post("/api/{provider_id}")
227
+ async def catch_all_post(provider_id: str, request: Request):
228
+ """Catch-all for POST requests to help debug routing issues"""
229
+ logger.info(f"=== CATCH-ALL POST REQUEST ===")
230
+ logger.info(f"Request path: {request.url.path}")
231
+ logger.info(f"Provider ID: {provider_id}")
232
+ logger.info(f"Request headers: {dict(request.headers)}")
233
+ logger.info(f"Available providers: {list(config.providers.keys())}")
234
+ logger.info(f"Available rotations: {list(config.rotations.keys())}")
235
+ logger.info(f"Available autoselect: {list(config.autoselect.keys())}")
236
+
237
+ error_msg = f"""
238
+ Invalid endpoint: {request.url.path}
239
+
240
+ The correct endpoint format is: /api/{{provider_id}}/chat/completions
241
+
242
+ Available providers: {list(config.providers.keys())}
243
+ Available rotations: {list(config.rotations.keys())}
244
+ Available autoselect: {list(config.autoselect.keys())}
245
+
246
+ Example: POST /api/ollama/chat/completions
247
+ """
248
+ logger.error(error_msg)
249
+ raise HTTPException(status_code=404, detail=error_msg.strip())
250
+
207
251
  def main():
208
252
  """Main entry point for the AISBF server"""
209
253
  import uvicorn
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aisbf"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations"
9
9
  readme = "README.md"
10
10
  license = "GPL-3.0-or-later"
@@ -49,7 +49,7 @@ class InstallCommand(_install):
49
49
 
50
50
  setup(
51
51
  name="aisbf",
52
- version="0.2.2",
52
+ version="0.2.3",
53
53
  author="AISBF Contributors",
54
54
  author_email="stefy@nexlab.net",
55
55
  description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations",
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
File without changes
File without changes
File without changes