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
aisbf/providers.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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,78 @@ 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
|
+
|
|
167
|
+
# Validate that all providers referenced in rotations exist
|
|
168
|
+
logger.info(f"=== VALIDATING ROTATION PROVIDERS ===")
|
|
169
|
+
available_providers = list(self.providers.keys())
|
|
170
|
+
logger.info(f"Available providers: {available_providers}")
|
|
171
|
+
|
|
172
|
+
for rotation_id, rotation_config in self.rotations.items():
|
|
173
|
+
logger.info(f"Validating rotation: {rotation_id}")
|
|
174
|
+
for provider in rotation_config.providers:
|
|
175
|
+
provider_id = provider['provider_id']
|
|
176
|
+
if provider_id not in self.providers:
|
|
177
|
+
logger.warning(f"!!! CONFIGURATION WARNING !!!")
|
|
178
|
+
logger.warning(f"Rotation '{rotation_id}' references provider '{provider_id}' which is NOT defined in providers.json")
|
|
179
|
+
logger.warning(f"Available providers: {available_providers}")
|
|
180
|
+
logger.warning(f"This provider will be SKIPPED during rotation requests")
|
|
181
|
+
logger.warning(f"Please add the provider to providers.json or remove it from the rotation configuration")
|
|
182
|
+
logger.warning(f"!!! END WARNING !!!")
|
|
183
|
+
else:
|
|
184
|
+
logger.info(f" ✓ Provider '{provider_id}' is available")
|
|
185
|
+
|
|
186
|
+
logger.info(f"=== Config._load_rotations END ===")
|
|
140
187
|
|
|
141
188
|
def _load_autoselect(self):
|
|
142
189
|
autoselect_path = Path.home() / '.aisbf' / 'autoselect.json'
|
|
@@ -162,7 +209,16 @@ class Config:
|
|
|
162
209
|
}
|
|
163
210
|
|
|
164
211
|
def get_provider(self, provider_id: str) -> ProviderConfig:
|
|
165
|
-
|
|
212
|
+
import logging
|
|
213
|
+
logger = logging.getLogger(__name__)
|
|
214
|
+
logger.info(f"Config.get_provider called with provider_id: {provider_id}")
|
|
215
|
+
logger.info(f"Available providers: {list(self.providers.keys())}")
|
|
216
|
+
result = self.providers.get(provider_id)
|
|
217
|
+
if result:
|
|
218
|
+
logger.info(f"Found provider: {result}")
|
|
219
|
+
else:
|
|
220
|
+
logger.warning(f"Provider {provider_id} not found!")
|
|
221
|
+
return result
|
|
166
222
|
|
|
167
223
|
def get_rotation(self, rotation_id: str) -> RotationConfig:
|
|
168
224
|
return self.rotations.get(rotation_id)
|