aisbf 0.1.0__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/__init__.py ADDED
@@ -0,0 +1,72 @@
1
+ """
2
+ Copyleft (C) 2026 Stefy Lanza <stefy@nexlab.net>
3
+
4
+ AISBF - AI Service Broker Framework || AI Should Be Free
5
+
6
+ A modular proxy server for managing multiple AI provider integrations.
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
+ A modular proxy server for managing multiple AI provider integrations.
24
+ """
25
+
26
+ from .config import config, Config, ProviderConfig, RotationConfig, AppConfig
27
+ from .models import (
28
+ Message,
29
+ ChatCompletionRequest,
30
+ ChatCompletionResponse,
31
+ Model,
32
+ Provider,
33
+ ErrorTracking
34
+ )
35
+ from .providers import (
36
+ BaseProviderHandler,
37
+ GoogleProviderHandler,
38
+ OpenAIProviderHandler,
39
+ AnthropicProviderHandler,
40
+ OllamaProviderHandler,
41
+ get_provider_handler,
42
+ PROVIDER_HANDLERS
43
+ )
44
+ from .handlers import RequestHandler, RotationHandler
45
+
46
+ __version__ = "0.1.0"
47
+ __all__ = [
48
+ # Config
49
+ "config",
50
+ "Config",
51
+ "ProviderConfig",
52
+ "RotationConfig",
53
+ "AppConfig",
54
+ # Models
55
+ "Message",
56
+ "ChatCompletionRequest",
57
+ "ChatCompletionResponse",
58
+ "Model",
59
+ "Provider",
60
+ "ErrorTracking",
61
+ # Providers
62
+ "BaseProviderHandler",
63
+ "GoogleProviderHandler",
64
+ "OpenAIProviderHandler",
65
+ "AnthropicProviderHandler",
66
+ "OllamaProviderHandler",
67
+ "get_provider_handler",
68
+ "PROVIDER_HANDLERS",
69
+ # Handlers
70
+ "RequestHandler",
71
+ "RotationHandler",
72
+ ]
aisbf/config.py ADDED
@@ -0,0 +1,144 @@
1
+ """
2
+ Copyleft (C) 2026 Stefy Lanza <stefy@nexlab.net>
3
+
4
+ AISBF - AI Service Broker Framework || AI Should Be Free
5
+
6
+ Configuration management 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
+ Configuration management for AISBF.
24
+ """
25
+ from typing import Dict, List, Optional
26
+ from pydantic import BaseModel, Field
27
+ import json
28
+ import shutil
29
+ from pathlib import Path
30
+
31
+ class ProviderConfig(BaseModel):
32
+ id: str
33
+ name: str
34
+ endpoint: str
35
+ type: str
36
+ api_key_required: bool
37
+
38
+ class RotationConfig(BaseModel):
39
+ providers: List[Dict]
40
+
41
+ class AppConfig(BaseModel):
42
+ providers: Dict[str, ProviderConfig]
43
+ rotations: Dict[str, RotationConfig]
44
+ error_tracking: Dict[str, Dict]
45
+
46
+ class Config:
47
+ def __init__(self):
48
+ self._ensure_config_directory()
49
+ self._load_providers()
50
+ self._load_rotations()
51
+ self._initialize_error_tracking()
52
+
53
+ def _get_config_source_dir(self):
54
+ """Get the directory containing default config files"""
55
+ # Try installed location first
56
+ installed_dirs = [
57
+ Path('/usr/local/share/aisbf'),
58
+ Path.home() / '.local' / 'share' / 'aisbf',
59
+ ]
60
+
61
+ for installed_dir in installed_dirs:
62
+ if installed_dir.exists() and (installed_dir / 'providers.json').exists():
63
+ return installed_dir
64
+
65
+ # Fallback to source tree config directory
66
+ # This is for development mode
67
+ source_dir = Path(__file__).parent.parent.parent / 'config'
68
+ if source_dir.exists() and (source_dir / 'providers.json').exists():
69
+ return source_dir
70
+
71
+ # Last resort: try the old location in the package directory
72
+ package_dir = Path(__file__).parent
73
+ if (package_dir / 'providers.json').exists():
74
+ return package_dir
75
+
76
+ raise FileNotFoundError("Could not find configuration files")
77
+
78
+ def _ensure_config_directory(self):
79
+ """Ensure ~/.aisbf/ directory exists and copy default config files if needed"""
80
+ config_dir = Path.home() / '.aisbf'
81
+
82
+ # Create config directory if it doesn't exist
83
+ config_dir.mkdir(exist_ok=True)
84
+
85
+ # Get the source directory for default config files
86
+ try:
87
+ source_dir = self._get_config_source_dir()
88
+ except FileNotFoundError:
89
+ print("Warning: Could not find default configuration files")
90
+ return
91
+
92
+ # Copy default config files if they don't exist
93
+ for config_file in ['providers.json', 'rotations.json']:
94
+ src = source_dir / config_file
95
+ dst = config_dir / config_file
96
+
97
+ if not dst.exists() and src.exists():
98
+ shutil.copy2(src, dst)
99
+ print(f"Created default config file: {dst}")
100
+
101
+ def _load_providers(self):
102
+ providers_path = Path.home() / '.aisbf' / 'providers.json'
103
+ if not providers_path.exists():
104
+ # Fallback to source config if user config doesn't exist
105
+ try:
106
+ source_dir = self._get_config_source_dir()
107
+ providers_path = source_dir / 'providers.json'
108
+ except FileNotFoundError:
109
+ raise FileNotFoundError("Could not find providers.json configuration file")
110
+
111
+ with open(providers_path) as f:
112
+ data = json.load(f)
113
+ self.providers = {k: ProviderConfig(**v) for k, v in data['providers'].items()}
114
+
115
+ def _load_rotations(self):
116
+ rotations_path = Path.home() / '.aisbf' / 'rotations.json'
117
+ if not rotations_path.exists():
118
+ # Fallback to source config if user config doesn't exist
119
+ try:
120
+ source_dir = self._get_config_source_dir()
121
+ rotations_path = source_dir / 'rotations.json'
122
+ except FileNotFoundError:
123
+ raise FileNotFoundError("Could not find rotations.json configuration file")
124
+
125
+ with open(rotations_path) as f:
126
+ data = json.load(f)
127
+ self.rotations = {k: RotationConfig(**v) for k, v in data['rotations'].items()}
128
+
129
+ def _initialize_error_tracking(self):
130
+ self.error_tracking = {}
131
+ for provider_id in self.providers:
132
+ self.error_tracking[provider_id] = {
133
+ 'failures': 0,
134
+ 'last_failure': None,
135
+ 'disabled_until': None
136
+ }
137
+
138
+ def get_provider(self, provider_id: str) -> ProviderConfig:
139
+ return self.providers.get(provider_id)
140
+
141
+ def get_rotation(self, rotation_id: str) -> RotationConfig:
142
+ return self.rotations.get(rotation_id)
143
+
144
+ config = Config()
aisbf/handlers.py ADDED
@@ -0,0 +1,190 @@
1
+ """
2
+ Copyleft (C) 2026 Stefy Lanza <stefy@nexlab.net>
3
+
4
+ AISBF - AI Service Broker Framework || AI Should Be Free
5
+
6
+ Request handlers for AISBF.
7
+
8
+ This program is free software: you can redistribute it and/or modify
9
+ it under the terms of the GNU General Public License as published by
10
+ the Free Software Foundation, either version 3 of the License, or
11
+ (at your option) any later version.
12
+
13
+ This program is distributed in the hope that it will be useful,
14
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ GNU General Public License for more details.
17
+
18
+ You should have received a copy of the GNU General Public License
19
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
20
+
21
+ Why did the programmer quit his job? Because he didn't get arrays!
22
+
23
+ Request handlers for AISBF.
24
+ """
25
+ import asyncio
26
+ from typing import Dict, List, Optional
27
+ from fastapi import HTTPException, Request
28
+ from fastapi.responses import JSONResponse, StreamingResponse
29
+ from .models import ChatCompletionRequest, ChatCompletionResponse
30
+ from .providers import get_provider_handler
31
+ from .config import config
32
+
33
+ class RequestHandler:
34
+ def __init__(self):
35
+ self.config = config
36
+
37
+ async def handle_chat_completion(self, request: Request, provider_id: str, request_data: Dict) -> Dict:
38
+ provider_config = self.config.get_provider(provider_id)
39
+
40
+ if provider_config.api_key_required:
41
+ api_key = request_data.get('api_key') or request.headers.get('Authorization', '').replace('Bearer ', '')
42
+ if not api_key:
43
+ raise HTTPException(status_code=401, detail="API key required")
44
+ else:
45
+ api_key = None
46
+
47
+ handler = get_provider_handler(provider_id, api_key)
48
+
49
+ if handler.is_rate_limited():
50
+ raise HTTPException(status_code=503, detail="Provider temporarily unavailable")
51
+
52
+ try:
53
+ # Apply rate limiting
54
+ await handler.apply_rate_limit()
55
+
56
+ response = await handler.handle_request(
57
+ model=request_data['model'],
58
+ messages=request_data['messages'],
59
+ max_tokens=request_data.get('max_tokens'),
60
+ temperature=request_data.get('temperature', 1.0),
61
+ stream=request_data.get('stream', False)
62
+ )
63
+ handler.record_success()
64
+ return response
65
+ except Exception as e:
66
+ handler.record_failure()
67
+ raise HTTPException(status_code=500, detail=str(e))
68
+
69
+ async def handle_streaming_chat_completion(self, request: Request, provider_id: str, request_data: Dict):
70
+ provider_config = self.config.get_provider(provider_id)
71
+
72
+ if provider_config.api_key_required:
73
+ api_key = request_data.get('api_key') or request.headers.get('Authorization', '').replace('Bearer ', '')
74
+ if not api_key:
75
+ raise HTTPException(status_code=401, detail="API key required")
76
+ else:
77
+ api_key = None
78
+
79
+ handler = get_provider_handler(provider_id, api_key)
80
+
81
+ if handler.is_rate_limited():
82
+ raise HTTPException(status_code=503, detail="Provider temporarily unavailable")
83
+
84
+ async def stream_generator():
85
+ try:
86
+ # Apply rate limiting
87
+ await handler.apply_rate_limit()
88
+
89
+ response = await handler.handle_request(
90
+ model=request_data['model'],
91
+ messages=request_data['messages'],
92
+ max_tokens=request_data.get('max_tokens'),
93
+ temperature=request_data.get('temperature', 1.0),
94
+ stream=True
95
+ )
96
+ for chunk in response:
97
+ yield f"data: {chunk}\n\n".encode('utf-8')
98
+ handler.record_success()
99
+ except Exception as e:
100
+ handler.record_failure()
101
+ yield f"data: {str(e)}\n\n".encode('utf-8')
102
+
103
+ return StreamingResponse(stream_generator(), media_type="text/event-stream")
104
+
105
+ async def handle_model_list(self, request: Request, provider_id: str) -> List[Dict]:
106
+ provider_config = self.config.get_provider(provider_id)
107
+
108
+ if provider_config.api_key_required:
109
+ api_key = request.headers.get('Authorization', '').replace('Bearer ', '')
110
+ if not api_key:
111
+ raise HTTPException(status_code=401, detail="API key required")
112
+ else:
113
+ api_key = None
114
+
115
+ handler = get_provider_handler(provider_id, api_key)
116
+ try:
117
+ # Apply rate limiting
118
+ await handler.apply_rate_limit()
119
+
120
+ models = await handler.get_models()
121
+ return [model.dict() for model in models]
122
+ except Exception as e:
123
+ raise HTTPException(status_code=500, detail=str(e))
124
+
125
+ class RotationHandler:
126
+ def __init__(self):
127
+ self.config = config
128
+
129
+ async def handle_rotation_request(self, rotation_id: str, request_data: Dict) -> Dict:
130
+ rotation_config = self.config.get_rotation(rotation_id)
131
+ if not rotation_config:
132
+ raise HTTPException(status_code=400, detail=f"Rotation {rotation_id} not found")
133
+
134
+ providers = rotation_config.providers
135
+ weighted_models = []
136
+
137
+ for provider in providers:
138
+ for model in provider['models']:
139
+ weighted_models.extend([model] * model['weight'])
140
+
141
+ if not weighted_models:
142
+ raise HTTPException(status_code=400, detail="No models available in rotation")
143
+
144
+ import random
145
+ selected_model = random.choice(weighted_models)
146
+
147
+ provider_id = selected_model['provider_id']
148
+ api_key = selected_model.get('api_key')
149
+ model_name = selected_model['name']
150
+
151
+ handler = get_provider_handler(provider_id, api_key)
152
+
153
+ if handler.is_rate_limited():
154
+ raise HTTPException(status_code=503, detail="All providers temporarily unavailable")
155
+
156
+ try:
157
+ # Apply rate limiting with model-specific rate limit if available
158
+ rate_limit = selected_model.get('rate_limit')
159
+ await handler.apply_rate_limit(rate_limit)
160
+
161
+ response = await handler.handle_request(
162
+ model=model_name,
163
+ messages=request_data['messages'],
164
+ max_tokens=request_data.get('max_tokens'),
165
+ temperature=request_data.get('temperature', 1.0),
166
+ stream=request_data.get('stream', False)
167
+ )
168
+ handler.record_success()
169
+ return response
170
+ except Exception as e:
171
+ handler.record_failure()
172
+ raise HTTPException(status_code=500, detail=str(e))
173
+
174
+ async def handle_rotation_model_list(self, rotation_id: str) -> List[Dict]:
175
+ rotation_config = self.config.get_rotation(rotation_id)
176
+ if not rotation_config:
177
+ raise HTTPException(status_code=400, detail=f"Rotation {rotation_id} not found")
178
+
179
+ all_models = []
180
+ for provider in rotation_config.providers:
181
+ for model in provider['models']:
182
+ all_models.append({
183
+ "id": f"{provider['provider_id']}/{model['name']}",
184
+ "name": model['name'],
185
+ "provider_id": provider['provider_id'],
186
+ "weight": model['weight'],
187
+ "rate_limit": model.get('rate_limit')
188
+ })
189
+
190
+ return all_models
aisbf/models.py ADDED
@@ -0,0 +1,67 @@
1
+ """
2
+ Copyleft (C) 2026 Stefy Lanza <stefy@nexlab.net>
3
+
4
+ AISBF - AI Service Broker Framework || AI Should Be Free
5
+
6
+ A modular proxy server for managing multiple AI provider integrations.
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
+ A modular proxy server for managing multiple AI provider integrations.
24
+ """
25
+
26
+ from pydantic import BaseModel, Field
27
+ from typing import Dict, List, Optional, Union
28
+
29
+ class Message(BaseModel):
30
+ role: str
31
+ content: Union[str, List[Dict]]
32
+
33
+ class ChatCompletionRequest(BaseModel):
34
+ model: str
35
+ messages: List[Message]
36
+ max_tokens: Optional[int] = None
37
+ temperature: Optional[float] = 1.0
38
+ stream: Optional[bool] = False
39
+
40
+ class ChatCompletionResponse(BaseModel):
41
+ id: str
42
+ object: str
43
+ created: int
44
+ model: str
45
+ choices: List[Dict]
46
+ usage: Optional[Dict] = None
47
+ stream: Optional[bool] = False
48
+
49
+ class Model(BaseModel):
50
+ id: str
51
+ name: str
52
+ provider_id: str
53
+ weight: int = 1
54
+ rate_limit: Optional[float] = None
55
+
56
+ class Provider(BaseModel):
57
+ id: str
58
+ name: str
59
+ type: str
60
+ endpoint: str
61
+ api_key_required: bool
62
+ models: List[Model] = []
63
+
64
+ class ErrorTracking(BaseModel):
65
+ failures: int
66
+ last_failure: Optional[int]
67
+ disabled_until: Optional[int]