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.
- {aisbf-0.2.2/aisbf.egg-info → aisbf-0.2.3}/PKG-INFO +1 -1
- {aisbf-0.2.2 → aisbf-0.2.3}/aisbf/config.py +37 -1
- {aisbf-0.2.2 → aisbf-0.2.3}/aisbf/handlers.py +57 -0
- {aisbf-0.2.2 → aisbf-0.2.3}/aisbf/providers.py +122 -9
- {aisbf-0.2.2 → aisbf-0.2.3/aisbf.egg-info}/PKG-INFO +1 -1
- {aisbf-0.2.2 → aisbf-0.2.3}/main.py +47 -3
- {aisbf-0.2.2 → aisbf-0.2.3}/pyproject.toml +1 -1
- {aisbf-0.2.2 → aisbf-0.2.3}/setup.py +1 -1
- {aisbf-0.2.2 → aisbf-0.2.3}/LICENSE.txt +0 -0
- {aisbf-0.2.2 → aisbf-0.2.3}/MANIFEST.in +0 -0
- {aisbf-0.2.2 → aisbf-0.2.3}/README.md +0 -0
- {aisbf-0.2.2 → aisbf-0.2.3}/aisbf/__init__.py +0 -0
- {aisbf-0.2.2 → aisbf-0.2.3}/aisbf/models.py +0 -0
- {aisbf-0.2.2 → aisbf-0.2.3}/aisbf.egg-info/SOURCES.txt +0 -0
- {aisbf-0.2.2 → aisbf-0.2.3}/aisbf.egg-info/dependency_links.txt +0 -0
- {aisbf-0.2.2 → aisbf-0.2.3}/aisbf.egg-info/entry_points.txt +0 -0
- {aisbf-0.2.2 → aisbf-0.2.3}/aisbf.egg-info/requires.txt +0 -0
- {aisbf-0.2.2 → aisbf-0.2.3}/aisbf.egg-info/top_level.txt +0 -0
- {aisbf-0.2.2 → aisbf-0.2.3}/aisbf.sh +0 -0
- {aisbf-0.2.2 → aisbf-0.2.3}/cli.py +0 -0
- {aisbf-0.2.2 → aisbf-0.2.3}/config/autoselect.json +0 -0
- {aisbf-0.2.2 → aisbf-0.2.3}/config/autoselect.md +0 -0
- {aisbf-0.2.2 → aisbf-0.2.3}/config/providers.json +0 -0
- {aisbf-0.2.2 → aisbf-0.2.3}/config/rotations.json +0 -0
- {aisbf-0.2.2 → aisbf-0.2.3}/requirements.txt +0 -0
- {aisbf-0.2.2 → aisbf-0.2.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aisbf
|
|
3
|
-
Version: 0.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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|