Commit 84d6f6e4 authored by Your Name's avatar Your Name

feat: Integrate existing database module for multi-user support

- Implement user-specific configuration isolation with SQLite database
- Add user management, authentication, and role-based access control
- Create user-specific providers, rotations, and autoselect configurations
- Add API token management and usage tracking per user
- Update handlers to support user-specific configs with fallback to global
- Add MCP support for user-specific configurations
- Update documentation and README with multi-user features
- Add user dashboard templates for configuration management
parent 4ec3cf51
......@@ -297,15 +297,18 @@ AISBF includes a comprehensive SQLite database system that provides persistent t
The database (`~/.aisbf/aisbf.db`) contains the following tables:
#### Core Tracking Tables
- **`context_dimensions`**: Tracks context size, condensation settings, and effective context per model
- **`token_usage`**: Persistent token usage tracking with TPM/TPH/TPD rate limiting across restarts
- **`model_embeddings`**: Caches model embeddings for semantic classification performance
#### Multi-User Tables
- **`users`**: User management with authentication, roles (admin/user), and metadata
- **`user_providers`**: Isolated provider configurations per user
- **`user_rotations`**: Isolated rotation configurations per user
- **`user_autoselects`**: Isolated autoselect configurations per user
- **`user_providers`**: Isolated provider configurations per user (JSON stored configurations)
- **`user_rotations`**: Isolated rotation configurations per user (JSON stored configurations)
- **`user_autoselects`**: Isolated autoselect configurations per user (JSON stored configurations)
- **`user_api_tokens`**: API token management per user for MCP and API access
- **`user_token_usage`**: Per-user token usage tracking
- **`user_token_usage`**: Per-user token usage tracking and analytics
### Database Initialization
......@@ -317,31 +320,56 @@ The database is automatically initialized on startup:
### Multi-User Support
AISBF supports multiple users with complete isolation:
AISBF supports multiple users with complete isolation through database-backed configurations:
#### User Authentication System
- **Database-First Authentication**: User credentials stored in SQLite database with SHA256 password hashing
- **Config Admin Fallback**: Legacy config-based admin authentication for backward compatibility
- **Role-Based Access Control**: Two roles - `admin` (full access) and `user` (isolated access)
- **Session-Based Authentication**: Secure session management for dashboard access
#### User Isolation Architecture
Each user has completely isolated configurations stored as JSON in the database:
- **Provider Configurations**: `user_providers` table stores individual API keys, endpoints, and model settings
- **Rotation Configurations**: `user_rotations` table stores personal load balancing rules
- **Autoselect Configurations**: `user_autoselects` table stores custom AI-assisted model selection rules
- **API Tokens**: `user_api_tokens` table manages multiple API tokens per user
- **Usage Tracking**: `user_token_usage` table provides per-user analytics and rate limiting
#### Handler Architecture
The system uses user-specific handler instances that load configurations from the database:
```python
# Handler instantiation with user context
request_handler = RequestHandler(user_id=user_id)
rotation_handler = RotationHandler(user_id=user_id)
autoselect_handler = AutoselectHandler(user_id=user_id)
# Automatic loading of user configs
handler.user_providers = db.get_user_providers(user_id)
handler.user_rotations = db.get_user_rotations(user_id)
handler.user_autoselects = db.get_user_autoselects(user_id)
```
#### User Authentication
- Database-first authentication with config admin fallback
- SHA256 password hashing for security
- Role-based access control (admin vs user roles)
- Session-based authentication
#### Configuration Priority
When processing requests, handlers check for user-specific configurations first:
#### User Isolation
- Each user has isolated provider, rotation, and autoselect configurations
- Separate API tokens per user
- Individual token usage tracking
- User-specific dashboard access
1. **User-specific configs** (from database)
2. **Global configs** (from JSON files)
3. **System defaults**
#### Admin Features
- Create/manage users via database
- Full system configuration access
- User management dashboard (future feature)
- Create and manage users via database
- Full access to global configurations and user management
- System-wide analytics and monitoring
- User administration dashboard
#### User Dashboard
- Usage statistics and token tracking
- Personal configuration management
- API token generation and management
- Restricted access to system settings
#### User Dashboard Features
- **Personal Configuration Management**: Create/edit/delete provider, rotation, and autoselect configs
- **Usage Statistics**: Real-time token usage tracking and analytics
- **API Token Management**: Generate, view, and delete API tokens
- **Isolated Access**: No visibility of other users' configurations or system settings
### Persistent Tracking
......
......@@ -40,6 +40,8 @@ Access the dashboard at `http://localhost:17765/dashboard` (default credentials:
- **MCP Server**: Model Context Protocol server for remote agent configuration and model access (SSE and HTTP streaming)
- **Persistent Database**: SQLite-based tracking of token usage, context dimensions, and model embeddings with automatic cleanup
- **Multi-User Support**: User management with isolated configurations, role-based access control, and API token management
- **Database Integration**: SQLite-based persistent storage for user configurations, token usage tracking, and context management
- **User-Specific Configurations**: Each user can have their own providers, rotations, and autoselect configurations stored in the database
## Author
......@@ -293,6 +295,33 @@ When context exceeds the configured percentage of `context_size`, the system aut
See `config/providers.json` and `config/rotations.json` for configuration examples.
### Multi-User Database Integration
AISBF includes comprehensive multi-user support with isolated configurations stored in a SQLite database:
#### User Management
- **Admin Users**: Full access to global configurations and user management
- **Regular Users**: Access to their own configurations and usage statistics
- **Role-Based Access**: Secure separation between admin and user roles
#### Database Features
- **Persistent Storage**: All configurations stored in SQLite database with automatic initialization
- **Token Usage Tracking**: Per-user API token usage statistics and analytics
- **Configuration Isolation**: Each user has separate providers, rotations, and autoselect configurations
- **Automatic Cleanup**: Database maintenance with configurable retention periods
#### User-Specific Configurations
Users can create and manage their own:
- **Providers**: Custom API endpoints, models, and authentication settings
- **Rotations**: Personal load balancing configurations across providers
- **Autoselect**: Custom AI-powered model selection rules
- **API Tokens**: Multiple API tokens with usage tracking and management
#### Dashboard Access
- **Admin Dashboard**: Global configuration management and user administration
- **User Dashboard**: Personal configuration management and usage statistics
- **API Token Management**: Create, view, and delete API tokens with usage analytics
### Content Classification and Semantic Selection
AISBF provides advanced content filtering and intelligent model selection based on content analysis:
......
......@@ -442,6 +442,537 @@ class DatabaseManager:
'TPD': self.get_token_usage(provider_id, model_name, '1d')
}
# User management methods
def authenticate_user(self, username: str, password_hash: str) -> Optional[Dict]:
"""
Authenticate a user by username and password hash.
Args:
username: Username to authenticate
password_hash: SHA256 hash of the password
Returns:
User dict if authenticated, None otherwise
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT id, username, role, is_active
FROM users
WHERE username = ? AND password_hash = ? AND is_active = 1
''', (username, password_hash))
row = cursor.fetchone()
if row:
return {
'id': row[0],
'username': row[1],
'role': row[2],
'is_active': row[3]
}
return None
def create_user(self, username: str, password_hash: str, role: str = 'user', created_by: str = None) -> int:
"""
Create a new user.
Args:
username: Username for the new user
password_hash: SHA256 hash of the password
role: User role ('admin' or 'user')
created_by: Username of the creator
Returns:
User ID of the created user
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT INTO users (username, password_hash, role, created_by)
VALUES (?, ?, ?, ?)
''', (username, password_hash, role, created_by))
conn.commit()
return cursor.lastrowid
def get_users(self) -> List[Dict]:
"""
Get all users.
Returns:
List of user dictionaries
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT id, username, role, created_by, created_at, last_login, is_active
FROM users
ORDER BY created_at DESC
''')
users = []
for row in cursor.fetchall():
users.append({
'id': row[0],
'username': row[1],
'role': row[2],
'created_by': row[3],
'created_at': row[4],
'last_login': row[5],
'is_active': row[6]
})
return users
def delete_user(self, user_id: int):
"""
Delete a user and all their configurations.
Args:
user_id: User ID to delete
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
# Delete user configurations first (due to foreign key constraints)
cursor.execute('DELETE FROM user_providers WHERE user_id = ?', (user_id,))
cursor.execute('DELETE FROM user_rotations WHERE user_id = ?', (user_id,))
cursor.execute('DELETE FROM user_autoselects WHERE user_id = ?', (user_id,))
cursor.execute('DELETE FROM user_api_tokens WHERE user_id = ?', (user_id,))
cursor.execute('DELETE FROM user_token_usage WHERE user_id = ?', (user_id,))
# Delete the user
cursor.execute('DELETE FROM users WHERE id = ?', (user_id,))
conn.commit()
# User-specific provider methods
def save_user_provider(self, user_id: int, provider_name: str, config: Dict):
"""
Save user-specific provider configuration.
Args:
user_id: User ID
provider_name: Provider name
config: Provider configuration dictionary
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
config_json = json.dumps(config)
cursor.execute('''
INSERT OR REPLACE INTO user_providers (user_id, provider_id, config, updated_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
''', (user_id, provider_name, config_json))
conn.commit()
def get_user_providers(self, user_id: int) -> List[Dict]:
"""
Get all user-specific providers for a user.
Args:
user_id: User ID
Returns:
List of provider configurations
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT provider_id, config, created_at, updated_at
FROM user_providers
WHERE user_id = ?
ORDER BY provider_id
''', (user_id,))
providers = []
for row in cursor.fetchall():
providers.append({
'provider_id': row[0],
'config': json.loads(row[1]),
'created_at': row[2],
'updated_at': row[3]
})
return providers
def get_user_provider(self, user_id: int, provider_name: str) -> Optional[Dict]:
"""
Get a specific user provider configuration.
Args:
user_id: User ID
provider_name: Provider name
Returns:
Provider configuration dict or None
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT config, created_at, updated_at
FROM user_providers
WHERE user_id = ? AND provider_id = ?
''', (user_id, provider_name))
row = cursor.fetchone()
if row:
return {
'config': json.loads(row[0]),
'created_at': row[1],
'updated_at': row[2]
}
return None
def delete_user_provider(self, user_id: int, provider_name: str):
"""
Delete a user-specific provider configuration.
Args:
user_id: User ID
provider_name: Provider name
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
DELETE FROM user_providers
WHERE user_id = ? AND provider_id = ?
''', (user_id, provider_name))
conn.commit()
# User-specific rotation methods
def save_user_rotation(self, user_id: int, rotation_name: str, config: Dict):
"""
Save user-specific rotation configuration.
Args:
user_id: User ID
rotation_name: Rotation name
config: Rotation configuration dictionary
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
config_json = json.dumps(config)
cursor.execute('''
INSERT OR REPLACE INTO user_rotations (user_id, rotation_id, config, updated_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
''', (user_id, rotation_name, config_json))
conn.commit()
def get_user_rotations(self, user_id: int) -> List[Dict]:
"""
Get all user-specific rotations for a user.
Args:
user_id: User ID
Returns:
List of rotation configurations
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT rotation_id, config, created_at, updated_at
FROM user_rotations
WHERE user_id = ?
ORDER BY rotation_id
''', (user_id,))
rotations = []
for row in cursor.fetchall():
rotations.append({
'rotation_id': row[0],
'config': json.loads(row[1]),
'created_at': row[2],
'updated_at': row[3]
})
return rotations
def get_user_rotation(self, user_id: int, rotation_name: str) -> Optional[Dict]:
"""
Get a specific user rotation configuration.
Args:
user_id: User ID
rotation_name: Rotation name
Returns:
Rotation configuration dict or None
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT config, created_at, updated_at
FROM user_rotations
WHERE user_id = ? AND rotation_id = ?
''', (user_id, rotation_name))
row = cursor.fetchone()
if row:
return {
'config': json.loads(row[0]),
'created_at': row[1],
'updated_at': row[2]
}
return None
def delete_user_rotation(self, user_id: int, rotation_name: str):
"""
Delete a user-specific rotation configuration.
Args:
user_id: User ID
rotation_name: Rotation name
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
DELETE FROM user_rotations
WHERE user_id = ? AND rotation_id = ?
''', (user_id, rotation_name))
conn.commit()
# User-specific autoselect methods
def save_user_autoselect(self, user_id: int, autoselect_name: str, config: Dict):
"""
Save user-specific autoselect configuration.
Args:
user_id: User ID
autoselect_name: Autoselect name
config: Autoselect configuration dictionary
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
config_json = json.dumps(config)
cursor.execute('''
INSERT OR REPLACE INTO user_autoselects (user_id, autoselect_id, config, updated_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
''', (user_id, autoselect_name, config_json))
conn.commit()
def get_user_autoselects(self, user_id: int) -> List[Dict]:
"""
Get all user-specific autoselects for a user.
Args:
user_id: User ID
Returns:
List of autoselect configurations
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT autoselect_id, config, created_at, updated_at
FROM user_autoselects
WHERE user_id = ?
ORDER BY autoselect_id
''', (user_id,))
autoselects = []
for row in cursor.fetchall():
autoselects.append({
'autoselect_id': row[0],
'config': json.loads(row[1]),
'created_at': row[2],
'updated_at': row[3]
})
return autoselects
def get_user_autoselect(self, user_id: int, autoselect_name: str) -> Optional[Dict]:
"""
Get a specific user autoselect configuration.
Args:
user_id: User ID
autoselect_name: Autoselect name
Returns:
Autoselect configuration dict or None
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT config, created_at, updated_at
FROM user_autoselects
WHERE user_id = ? AND autoselect_id = ?
''', (user_id, autoselect_name))
row = cursor.fetchone()
if row:
return {
'config': json.loads(row[0]),
'created_at': row[1],
'updated_at': row[2]
}
return None
def delete_user_autoselect(self, user_id: int, autoselect_name: str):
"""
Delete a user-specific autoselect configuration.
Args:
user_id: User ID
autoselect_name: Autoselect name
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
DELETE FROM user_autoselects
WHERE user_id = ? AND autoselect_id = ?
''', (user_id, autoselect_name))
conn.commit()
# User API token methods
def create_user_api_token(self, user_id: int, token: str, description: str = None) -> int:
"""
Create a new API token for a user.
Args:
user_id: User ID
token: The token string
description: Optional description
Returns:
Token ID
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT INTO user_api_tokens (user_id, token, description)
VALUES (?, ?, ?)
''', (user_id, token, description))
conn.commit()
return cursor.lastrowid
def get_user_api_tokens(self, user_id: int) -> List[Dict]:
"""
Get all API tokens for a user.
Args:
user_id: User ID
Returns:
List of token dictionaries
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT id, token, description, created_at, last_used, is_active
FROM user_api_tokens
WHERE user_id = ?
ORDER BY created_at DESC
''', (user_id,))
tokens = []
for row in cursor.fetchall():
tokens.append({
'id': row[0],
'token': row[1],
'description': row[2],
'created_at': row[3],
'last_used': row[4],
'is_active': row[5]
})
return tokens
def authenticate_user_token(self, token: str) -> Optional[Dict]:
"""
Authenticate a user by API token.
Args:
token: API token string
Returns:
User dict if authenticated, None otherwise
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT u.id, u.username, u.role, t.id as token_id
FROM users u
JOIN user_api_tokens t ON u.id = t.user_id
WHERE t.token = ? AND t.is_active = 1 AND u.is_active = 1
''', (token,))
row = cursor.fetchone()
if row:
return {
'user_id': row[0],
'username': row[1],
'role': row[2],
'token_id': row[3]
}
return None
def delete_user_api_token(self, user_id: int, token_id: int):
"""
Delete a user API token.
Args:
user_id: User ID
token_id: Token ID
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
DELETE FROM user_api_tokens
WHERE id = ? AND user_id = ?
''', (token_id, user_id))
conn.commit()
# User token usage methods
def record_user_token_usage(self, user_id: int, token_id: int, provider_id: str, model_name: str, tokens_used: int):
"""
Record token usage for a user API request.
Args:
user_id: User ID
token_id: API token ID
provider_id: Provider identifier
model_name: Model name
tokens_used: Number of tokens used
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT INTO user_token_usage (user_id, token_id, provider_id, model_name, tokens_used)
VALUES (?, ?, ?, ?, ?)
''', (user_id, token_id, provider_id, model_name, tokens_used))
# Update last_used timestamp for the token
cursor.execute('''
UPDATE user_api_tokens
SET last_used = CURRENT_TIMESTAMP
WHERE id = ?
''', (token_id,))
conn.commit()
def get_user_token_usage(self, user_id: int) -> List[Dict]:
"""
Get token usage for a user.
Args:
user_id: User ID
Returns:
List of token usage records
"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT provider_id, model_name, tokens_used, timestamp
FROM user_token_usage
WHERE user_id = ?
ORDER BY timestamp DESC
LIMIT 1000
''', (user_id,))
usage = []
for row in cursor.fetchall():
usage.append({
'provider_id': row[0],
'model_name': row[1],
'token_count': row[2],
'timestamp': row[3]
})
return usage
# Global database manager instance
_db_manager: Optional[DatabaseManager] = None
......
......@@ -74,8 +74,24 @@ def generate_system_fingerprint(provider_id: str, seed: Optional[int] = None) ->
return f"fp_{hash_value}"
class RequestHandler:
def __init__(self):
def __init__(self, user_id=None):
self.user_id = user_id
self.config = config
# Load user-specific configs if user_id is provided
if user_id:
self._load_user_configs()
else:
self.user_providers = {}
self.user_rotations = {}
self.user_autoselects = {}
def _load_user_configs(self):
"""Load user-specific configurations from database"""
from .database import get_database
db = get_database()
self.user_providers = db.get_user_providers(self.user_id)
self.user_rotations = db.get_user_rotations(self.user_id)
self.user_autoselects = db.get_user_autoselects(self.user_id)
async def _handle_chunked_request(
self,
......@@ -210,9 +226,17 @@ class RequestHandler:
logger = logging.getLogger(__name__)
logger.info(f"=== RequestHandler.handle_chat_completion START ===")
logger.info(f"Provider ID: {provider_id}")
logger.info(f"User ID: {self.user_id}")
logger.info(f"Request data: {request_data}")
provider_config = self.config.get_provider(provider_id)
# Check for user-specific provider config first
if self.user_id and provider_id in self.user_providers:
provider_config = self.user_providers[provider_id]
logger.info(f"Using user-specific provider config for {provider_id}")
else:
provider_config = self.config.get_provider(provider_id)
logger.info(f"Using global provider config for {provider_id}")
logger.info(f"Provider config: {provider_config}")
logger.info(f"Provider type: {provider_config.type}")
logger.info(f"Provider endpoint: {provider_config.endpoint}")
......@@ -338,7 +362,11 @@ class RequestHandler:
raise HTTPException(status_code=500, detail=str(e))
async def handle_streaming_chat_completion(self, request: Request, provider_id: str, request_data: Dict):
provider_config = self.config.get_provider(provider_id)
# Check for user-specific provider config first
if self.user_id and provider_id in self.user_providers:
provider_config = self.user_providers[provider_id]
else:
provider_config = self.config.get_provider(provider_id)
if provider_config.api_key_required:
api_key = request_data.get('api_key') or request.headers.get('Authorization', '').replace('Bearer ', '')
......@@ -1214,8 +1242,24 @@ class RequestHandler:
raise HTTPException(status_code=500, detail=f"Error fetching content: {str(e)}")
class RotationHandler:
def __init__(self):
def __init__(self, user_id=None):
self.user_id = user_id
self.config = config
# Load user-specific configs if user_id is provided
if user_id:
self._load_user_configs()
else:
self.user_providers = {}
self.user_rotations = {}
self.user_autoselects = {}
def _load_user_configs(self):
"""Load user-specific configurations from database"""
from .database import get_database
db = get_database()
self.user_providers = db.get_user_providers(self.user_id)
self.user_rotations = db.get_user_rotations(self.user_id)
self.user_autoselects = db.get_user_autoselects(self.user_id)
def _get_provider_type(self, provider_id: str) -> str:
"""Get the provider type from configuration"""
......@@ -1435,7 +1479,7 @@ class RotationHandler:
async def handle_rotation_request(self, rotation_id: str, request_data: Dict):
"""
Handle a rotation request.
For streaming requests, returns a StreamingResponse with proper handling
based on the selected provider's type (google vs others).
For non-streaming requests, returns the response dict directly.
......@@ -1445,8 +1489,16 @@ class RotationHandler:
logger = logging.getLogger(__name__)
logger.info(f"=== RotationHandler.handle_rotation_request START ===")
logger.info(f"Rotation ID: {rotation_id}")
rotation_config = self.config.get_rotation(rotation_id)
logger.info(f"User ID: {self.user_id}")
# Check for user-specific rotation config first
if self.user_id and rotation_id in self.user_rotations:
rotation_config = self.user_rotations[rotation_id]
logger.info(f"Using user-specific rotation config for {rotation_id}")
else:
rotation_config = self.config.get_rotation(rotation_id)
logger.info(f"Using global rotation config for {rotation_id}")
if not rotation_config:
logger.error(f"Rotation {rotation_id} not found")
raise HTTPException(status_code=400, detail=f"Rotation {rotation_id} not found")
......@@ -1476,11 +1528,17 @@ class RotationHandler:
logger.info(f"")
logger.info(f"--- Processing provider: {provider_id} ---")
# Check if provider exists in configuration
provider_config = self.config.get_provider(provider_id)
# Check if provider exists in configuration (user-specific first, then global)
if self.user_id and provider_id in self.user_providers:
provider_config = self.user_providers[provider_id]
logger.info(f" [USER] Using user-specific provider config for {provider_id}")
else:
provider_config = self.config.get_provider(provider_id)
logger.info(f" [GLOBAL] Using global provider config for {provider_id}")
if not provider_config:
logger.error(f" [ERROR] Provider {provider_id} not found in providers configuration")
logger.error(f" Available providers: {list(self.config.providers.keys())}")
logger.error(f" Available providers: {list(self.config.providers.keys()) if not self.user_id else list(self.user_providers.keys())}")
logger.error(f" Skipping this provider")
skipped_providers.append(provider_id)
continue
......@@ -2760,12 +2818,28 @@ class RotationHandler:
return capabilities
class AutoselectHandler:
def __init__(self):
def __init__(self, user_id=None):
self.user_id = user_id
self.config = config
self._skill_file_content = None
self._internal_model = None
self._internal_tokenizer = None
self._internal_model_lock = None
# Load user-specific configs if user_id is provided
if user_id:
self._load_user_configs()
else:
self.user_providers = {}
self.user_rotations = {}
self.user_autoselects = {}
def _load_user_configs(self):
"""Load user-specific configurations from database"""
from .database import get_database
db = get_database()
self.user_providers = db.get_user_providers(self.user_id)
self.user_rotations = db.get_user_rotations(self.user_id)
self.user_autoselects = db.get_user_autoselects(self.user_id)
def _get_skill_file_content(self) -> str:
"""Load the autoselect.md skill file content"""
......@@ -3130,8 +3204,16 @@ class AutoselectHandler:
logger = logging.getLogger(__name__)
logger.info(f"=== AUTOSELECT REQUEST START ===")
logger.info(f"Autoselect ID: {autoselect_id}")
autoselect_config = self.config.get_autoselect(autoselect_id)
logger.info(f"User ID: {self.user_id}")
# Check for user-specific autoselect config first
if self.user_id and autoselect_id in self.user_autoselects:
autoselect_config = self.user_autoselects[autoselect_id]
logger.info(f"Using user-specific autoselect config for {autoselect_id}")
else:
autoselect_config = self.config.get_autoselect(autoselect_id)
logger.info(f"Using global autoselect config for {autoselect_id}")
if not autoselect_config:
logger.error(f"Autoselect {autoselect_id} not found")
raise HTTPException(status_code=400, detail=f"Autoselect {autoselect_id} not found")
......
......@@ -427,7 +427,7 @@ class MCPServer:
return tools
async def handle_tool_call(self, tool_name: str, arguments: Dict, auth_level: int) -> Dict:
async def handle_tool_call(self, tool_name: str, arguments: Dict, auth_level: int, user_id: Optional[int] = None) -> Dict:
"""
Handle an MCP tool call.
......@@ -476,7 +476,7 @@ class MCPServer:
raise HTTPException(status_code=404, detail=f"Tool '{tool_name}' not found")
handler = handlers[tool_name]
return await handler(arguments)
return await handler(arguments, user_id)
async def _list_models(self, args: Dict) -> Dict:
"""List all available models"""
......@@ -562,18 +562,18 @@ class MCPServer:
}
return {"autoselect": autoselect_info}
async def _chat_completion(self, args: Dict) -> Dict:
async def _chat_completion(self, args: Dict, user_id: Optional[int] = None) -> Dict:
"""Handle chat completion request"""
from .handlers import RequestHandler, RotationHandler, AutoselectHandler
from .models import ChatCompletionRequest
from starlette.requests import Request
model = args.get('model')
messages = args.get('messages', [])
temperature = args.get('temperature', 1.0)
max_tokens = args.get('max_tokens', 2048)
stream = args.get('stream', False)
# Parse provider from model
if '/' in model:
parts = model.split('/', 1)
......@@ -582,7 +582,7 @@ class MCPServer:
else:
provider_id = model
actual_model = model
# Create request data
request_data = {
"model": actual_model,
......@@ -591,7 +591,7 @@ class MCPServer:
"max_tokens": max_tokens,
"stream": stream
}
# Create dummy request
scope = {
"type": "http",
......@@ -601,25 +601,26 @@ class MCPServer:
"path": f"/api/{provider_id}/chat/completions"
}
dummy_request = Request(scope)
# Route to appropriate handler
# Route to appropriate handler (with user_id support)
from main import get_user_handler
if provider_id == "autoselect":
if actual_model not in self.config.autoselect:
handler = get_user_handler('autoselect', user_id)
if actual_model not in self.config.autoselect and (not user_id or actual_model not in handler.user_autoselects):
raise HTTPException(status_code=400, detail=f"Autoselect '{actual_model}' not found")
handler = AutoselectHandler()
if stream:
return {"error": "Streaming not supported in MCP, use SSE endpoint instead"}
else:
return await handler.handle_autoselect_request(actual_model, request_data)
elif provider_id == "rotation":
if actual_model not in self.config.rotations:
handler = get_user_handler('rotation', user_id)
if actual_model not in self.config.rotations and (not user_id or actual_model not in handler.user_rotations):
raise HTTPException(status_code=400, detail=f"Rotation '{actual_model}' not found")
handler = RotationHandler()
return await handler.handle_rotation_request(actual_model, request_data)
else:
if provider_id not in self.config.providers:
handler = get_user_handler('request', user_id)
if provider_id not in self.config.providers and (not user_id or provider_id not in handler.user_providers):
raise HTTPException(status_code=400, detail=f"Provider '{provider_id}' not found")
handler = RequestHandler()
if stream:
return {"error": "Streaming not supported in MCP, use SSE endpoint instead"}
else:
......
......@@ -481,6 +481,43 @@ autoselect_handler = None
server_config = None
config = None
_initialized = False
# Cache for user-specific handlers to avoid recreating them
_user_handlers_cache = {}
def get_user_handler(handler_type: str, user_id=None):
"""Get the appropriate handler for a user, with caching"""
global request_handler, rotation_handler, autoselect_handler, _user_handlers_cache
if user_id is None:
# Return global handlers for non-authenticated requests
if handler_type == 'request':
return request_handler
elif handler_type == 'rotation':
return rotation_handler
elif handler_type == 'autoselect':
return autoselect_handler
else:
raise ValueError(f"Unknown handler type: {handler_type}")
# Check cache first
cache_key = f"{handler_type}_{user_id}"
if cache_key in _user_handlers_cache:
return _user_handlers_cache[cache_key]
# Create new handler instance for this user
if handler_type == 'request':
handler = RequestHandler(user_id)
elif handler_type == 'rotation':
handler = RotationHandler(user_id)
elif handler_type == 'autoselect':
handler = AutoselectHandler(user_id)
else:
raise ValueError(f"Unknown handler type: {handler_type}")
# Cache it
_user_handlers_cache[cache_key] = handler
return handler
tor_service = None
# Model cache for dynamically fetched provider models
......@@ -560,7 +597,7 @@ async def fetch_provider_models(provider_id: str) -> list:
}
dummy_request = Request(scope)
# Fetch models from provider API
# Fetch models from provider API (use global handler for model fetching)
models = await request_handler.handle_model_list(dummy_request, provider_id)
# Cache the results
......@@ -917,33 +954,70 @@ async def auth_middleware(request: Request, call_next):
if request.url.path == "/" or request.url.path.startswith("/dashboard"):
response = await call_next(request)
return response
# Skip auth for public models endpoints (GET only)
if request.method == "GET" and request.url.path in ["/api/models", "/api/v1/models"]:
response = await call_next(request)
return response
# Check for Authorization header
auth_header = request.headers.get('Authorization', '')
if not auth_header.startswith('Bearer '):
return JSONResponse(
status_code=401,
content={"error": "Missing or invalid Authorization header. Use: Authorization: Bearer <token>"}
)
token = auth_header.replace('Bearer ', '')
# First check global tokens (for backward compatibility)
allowed_tokens = server_config.get('auth_tokens', [])
if token not in allowed_tokens:
return JSONResponse(
status_code=403,
content={"error": "Invalid authentication token"}
)
if token in allowed_tokens:
# Store global token info in request state
request.state.user_id = None
request.state.token_id = None
request.state.is_global_token = True
else:
# Check user API tokens
from aisbf.database import get_database
db = get_database()
user_auth = db.authenticate_user_token(token)
if user_auth:
# Store user token info in request state
request.state.user_id = user_auth['user_id']
request.state.token_id = user_auth['token_id']
request.state.is_global_token = False
# Record token usage for analytics
# We'll do this asynchronously to avoid blocking the request
import asyncio
asyncio.create_task(record_token_usage_async(user_auth['user_id'], user_auth['token_id']))
else:
return JSONResponse(
status_code=403,
content={"error": "Invalid authentication token"}
)
else:
# Auth not enabled, set default state
request.state.user_id = None
request.state.token_id = None
request.state.is_global_token = False
response = await call_next(request)
return response
async def record_token_usage_async(user_id: int, token_id: int):
"""Asynchronously record token usage"""
try:
from aisbf.database import get_database
db = get_database()
# Record with dummy values for now - will be updated when we know the actual usage
db.record_user_token_usage(user_id, token_id, '', '', 0)
except Exception as e:
logger.warning(f"Failed to record token usage: {e}")
# Exception handler for validation errors
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
......@@ -1001,18 +1075,35 @@ async def dashboard_login_page(request: Request):
@app.post("/dashboard/login")
async def dashboard_login(request: Request, username: str = Form(...), password: str = Form(...)):
"""Handle dashboard login"""
dashboard_config = server_config.get('dashboard_config', {}) if server_config else {}
stored_username = dashboard_config.get('username', 'admin')
stored_password_hash = dashboard_config.get('password', '8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918')
from aisbf.database import get_database
# Hash the submitted password
password_hash = hashlib.sha256(password.encode()).hexdigest()
# Compare username and hashed password
# Try database authentication first
db = get_database()
user = db.authenticate_user(username, password_hash)
if user:
# Database user authenticated
request.session['logged_in'] = True
request.session['username'] = username
request.session['role'] = user['role']
request.session['user_id'] = user['id']
return RedirectResponse(url=url_for(request, "/dashboard"), status_code=303)
# Fallback to config admin
dashboard_config = server_config.get('dashboard_config', {}) if server_config else {}
stored_username = dashboard_config.get('username', 'admin')
stored_password_hash = dashboard_config.get('password', '8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918')
if username == stored_username and password_hash == stored_password_hash:
request.session['logged_in'] = True
request.session['username'] = username
request.session['role'] = 'admin'
request.session['user_id'] = None # Config admin has no user_id
return RedirectResponse(url=url_for(request, "/dashboard"), status_code=303)
return templates.TemplateResponse("dashboard/login.html", {"request": request, "error": "Invalid credentials"})
@app.get("/dashboard/logout")
......@@ -1027,6 +1118,15 @@ def require_dashboard_auth(request: Request):
return RedirectResponse(url=url_for(request, "/dashboard/login"), status_code=303)
return None
def require_admin(request: Request):
"""Check if user is admin"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
if request.session.get('role') != 'admin':
return RedirectResponse(url=url_for(request, "/dashboard"), status_code=303)
return None
@app.get("/dashboard", response_class=HTMLResponse)
async def dashboard_index(request: Request):
"""Dashboard overview page"""
......@@ -1046,14 +1146,50 @@ async def dashboard_index(request: Request):
})
else:
# User dashboard - show user stats
return templates.TemplateResponse("dashboard/index.html", {
from aisbf.database import get_database
db = get_database()
user_id = request.session.get('user_id')
# Get user statistics
usage_stats = {
'total_tokens': 0,
'requests_today': 0
}
if user_id:
# Get token usage for this user
token_usage = db.get_user_token_usage(user_id)
usage_stats['total_tokens'] = sum(row['token_count'] for row in token_usage)
# Count requests today
from datetime import datetime, timedelta
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
usage_stats['requests_today'] = len([
row for row in token_usage
if datetime.fromisoformat(row['timestamp']) >= today
])
# Get user config counts
providers_count = len(db.get_user_providers(user_id))
rotations_count = len(db.get_user_rotations(user_id))
autoselects_count = len(db.get_user_autoselects(user_id))
# Get recent activity (last 10)
recent_activity = token_usage[-10:] if token_usage else []
else:
providers_count = 0
rotations_count = 0
autoselects_count = 0
recent_activity = []
return templates.TemplateResponse("dashboard/user_index.html", {
"request": request,
"session": request.session,
"user_message": "User dashboard - usage statistics and configuration management coming soon",
"providers_count": 0,
"rotations_count": 0,
"autoselect_count": 0,
"server_config": {}
"usage_stats": usage_stats,
"providers_count": providers_count,
"rotations_count": rotations_count,
"autoselects_count": autoselects_count,
"recent_activity": recent_activity
})
@app.get("/dashboard/providers", response_class=HTMLResponse)
......@@ -1622,6 +1758,314 @@ async def dashboard_restart(request: Request):
return JSONResponse({"message": "Server is restarting..."})
# User-specific configuration management routes
@app.get("/dashboard/user/providers", response_class=HTMLResponse)
async def dashboard_user_providers(request: Request):
"""User provider management page"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return RedirectResponse(url=url_for(request, "/dashboard/login"), status_code=303)
from aisbf.database import get_database
db = get_database()
# Get user-specific providers
user_providers = db.get_user_providers(user_id)
return templates.TemplateResponse("dashboard/user_providers.html", {
"request": request,
"session": request.session,
"user_providers_json": json.dumps(user_providers),
"user_id": user_id
})
@app.post("/dashboard/user/providers")
async def dashboard_user_providers_save(request: Request, provider_name: str = Form(...), provider_config: str = Form(...)):
"""Save user-specific provider configuration"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return RedirectResponse(url=url_for(request, "/dashboard/login"), status_code=303)
from aisbf.database import get_database
db = get_database()
try:
# Validate JSON
provider_data = json.loads(provider_config)
# Save to database
db.save_user_provider(user_id, provider_name, provider_data)
return RedirectResponse(url=url_for(request, "/dashboard/user/providers"), status_code=303)
except json.JSONDecodeError as e:
# Reload current providers on error
user_providers = db.get_user_providers(user_id)
return templates.TemplateResponse("dashboard/user_providers.html", {
"request": request,
"session": request.session,
"user_providers_json": json.dumps(user_providers),
"user_id": user_id,
"error": f"Invalid JSON: {str(e)}"
})
@app.delete("/dashboard/user/providers/{provider_name}")
async def dashboard_user_providers_delete(request: Request, provider_name: str):
"""Delete user-specific provider configuration"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return JSONResponse(status_code=401, content={"error": "Not authenticated"})
from aisbf.database import get_database
db = get_database()
try:
db.delete_user_provider(user_id, provider_name)
return JSONResponse({"message": "Provider deleted successfully"})
except Exception as e:
return JSONResponse(status_code=500, content={"error": str(e)})
# User-specific rotation management routes
@app.get("/dashboard/user/rotations", response_class=HTMLResponse)
async def dashboard_user_rotations(request: Request):
"""User rotation management page"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return RedirectResponse(url=url_for(request, "/dashboard/login"), status_code=303)
from aisbf.database import get_database
db = get_database()
# Get user-specific rotations
user_rotations = db.get_user_rotations(user_id)
return templates.TemplateResponse("dashboard/user_rotations.html", {
"request": request,
"session": request.session,
"user_rotations_json": json.dumps(user_rotations),
"user_id": user_id
})
@app.post("/dashboard/user/rotations")
async def dashboard_user_rotations_save(request: Request, rotation_name: str = Form(...), rotation_config: str = Form(...)):
"""Save user-specific rotation configuration"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return RedirectResponse(url=url_for(request, "/dashboard/login"), status_code=303)
from aisbf.database import get_database
db = get_database()
try:
# Validate JSON
rotation_data = json.loads(rotation_config)
# Save to database
db.save_user_rotation(user_id, rotation_name, rotation_data)
return RedirectResponse(url=url_for(request, "/dashboard/user/rotations"), status_code=303)
except json.JSONDecodeError as e:
# Reload current rotations on error
user_rotations = db.get_user_rotations(user_id)
return templates.TemplateResponse("dashboard/user_rotations.html", {
"request": request,
"session": request.session,
"user_rotations_json": json.dumps(user_rotations),
"user_id": user_id,
"error": f"Invalid JSON: {str(e)}"
})
@app.delete("/dashboard/user/rotations/{rotation_name}")
async def dashboard_user_rotations_delete(request: Request, rotation_name: str):
"""Delete user-specific rotation configuration"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return JSONResponse(status_code=401, content={"error": "Not authenticated"})
from aisbf.database import get_database
db = get_database()
try:
db.delete_user_rotation(user_id, rotation_name)
return JSONResponse({"message": "Rotation deleted successfully"})
except Exception as e:
return JSONResponse(status_code=500, content={"error": str(e)})
# User-specific autoselect management routes
@app.get("/dashboard/user/autoselects", response_class=HTMLResponse)
async def dashboard_user_autoselects(request: Request):
"""User autoselect management page"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return RedirectResponse(url=url_for(request, "/dashboard/login"), status_code=303)
from aisbf.database import get_database
db = get_database()
# Get user-specific autoselects
user_autoselects = db.get_user_autoselects(user_id)
return templates.TemplateResponse("dashboard/user_autoselects.html", {
"request": request,
"session": request.session,
"user_autoselects_json": json.dumps(user_autoselects),
"user_id": user_id
})
@app.post("/dashboard/user/autoselects")
async def dashboard_user_autoselects_save(request: Request, autoselect_name: str = Form(...), autoselect_config: str = Form(...)):
"""Save user-specific autoselect configuration"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return RedirectResponse(url=url_for(request, "/dashboard/login"), status_code=303)
from aisbf.database import get_database
db = get_database()
try:
# Validate JSON
autoselect_data = json.loads(autoselect_config)
# Save to database
db.save_user_autoselect(user_id, autoselect_name, autoselect_data)
return RedirectResponse(url=url_for(request, "/dashboard/user/autoselects"), status_code=303)
except json.JSONDecodeError as e:
# Reload current autoselects on error
user_autoselects = db.get_user_autoselects(user_id)
return templates.TemplateResponse("dashboard/user_autoselects.html", {
"request": request,
"session": request.session,
"user_autoselects_json": json.dumps(user_autoselects),
"user_id": user_id,
"error": f"Invalid JSON: {str(e)}"
})
@app.delete("/dashboard/user/autoselects/{autoselect_name}")
async def dashboard_user_autoselects_delete(request: Request, autoselect_name: str):
"""Delete user-specific autoselect configuration"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return JSONResponse(status_code=401, content={"error": "Not authenticated"})
from aisbf.database import get_database
db = get_database()
try:
db.delete_user_autoselect(user_id, autoselect_name)
return JSONResponse({"message": "Autoselect deleted successfully"})
except Exception as e:
return JSONResponse(status_code=500, content={"error": str(e)})
# User API token management routes
@app.get("/dashboard/user/tokens", response_class=HTMLResponse)
async def dashboard_user_tokens(request: Request):
"""User API token management page"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return RedirectResponse(url=url_for(request, "/dashboard/login"), status_code=303)
from aisbf.database import get_database
db = get_database()
# Get user API tokens
user_tokens = db.get_user_api_tokens(user_id)
return templates.TemplateResponse("dashboard/user_tokens.html", {
"request": request,
"session": request.session,
"user_tokens": user_tokens,
"user_id": user_id
})
@app.post("/dashboard/user/tokens")
async def dashboard_user_tokens_create(request: Request, description: str = Form("")):
"""Create a new user API token"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return JSONResponse(status_code=401, content={"error": "Not authenticated"})
from aisbf.database import get_database
import secrets
db = get_database()
# Generate a secure token
token = secrets.token_urlsafe(32)
try:
token_id = db.create_user_api_token(user_id, token, description.strip() or None)
return JSONResponse({
"message": "Token created successfully",
"token": token,
"token_id": token_id
})
except Exception as e:
return JSONResponse(status_code=500, content={"error": str(e)})
@app.delete("/dashboard/user/tokens/{token_id}")
async def dashboard_user_tokens_delete(request: Request, token_id: int):
"""Delete a user API token"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return JSONResponse(status_code=401, content={"error": "Not authenticated"})
from aisbf.database import get_database
db = get_database()
try:
db.delete_user_api_token(user_id, token_id)
return JSONResponse({"message": "Token deleted successfully"})
except Exception as e:
return JSONResponse(status_code=500, content={"error": str(e)})
@app.get("/dashboard/tor/status")
async def dashboard_tor_status(request: Request):
"""Get TOR hidden service status"""
......@@ -1867,10 +2311,14 @@ async def v1_chat_completions(request: Request, body: ChatCompletionRequest):
detail=f"Autoselect '{actual_model}' not found. Available: {list(config.autoselect.keys())}"
)
body_dict['model'] = actual_model
# Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('autoselect', user_id)
if body.stream:
return await autoselect_handler.handle_autoselect_streaming_request(actual_model, body_dict)
return await handler.handle_autoselect_streaming_request(actual_model, body_dict)
else:
return await autoselect_handler.handle_autoselect_request(actual_model, body_dict)
return await handler.handle_autoselect_request(actual_model, body_dict)
# PATH 2: Check if it's a rotation (format: rotation/{name})
if provider_id == "rotation":
......@@ -1880,7 +2328,10 @@ async def v1_chat_completions(request: Request, body: ChatCompletionRequest):
detail=f"Rotation '{actual_model}' not found. Available: {list(config.rotations.keys())}"
)
body_dict['model'] = actual_model
return await rotation_handler.handle_rotation_request(actual_model, body_dict)
# Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('rotation', user_id)
return await handler.handle_rotation_request(actual_model, body_dict)
# PATH 1: Direct provider model (format: {provider}/{model})
if provider_id not in config.providers:
......@@ -1899,10 +2350,14 @@ async def v1_chat_completions(request: Request, body: ChatCompletionRequest):
# Handle as direct provider request
body_dict['model'] = actual_model
# Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('request', user_id)
if body.stream:
return await request_handler.handle_streaming_chat_completion(request, provider_id, body_dict)
return await handler.handle_streaming_chat_completion(request, provider_id, body_dict)
else:
return await request_handler.handle_chat_completion(request, provider_id, body_dict)
return await handler.handle_chat_completion(request, provider_id, body_dict)
@app.get("/api/models")
async def list_all_models(request: Request):
......@@ -2069,6 +2524,10 @@ async def v1_audio_transcriptions(request: Request):
detail=f"Provider '{provider_id}' credentials not available. Please configure credentials for this provider."
)
# Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('request', user_id)
# Create new form data with updated model
from starlette.datastructures import FormData
updated_form = FormData()
......@@ -2077,8 +2536,8 @@ async def v1_audio_transcriptions(request: Request):
updated_form[key] = actual_model
else:
updated_form[key] = value
return await request_handler.handle_audio_transcription(request, provider_id, updated_form)
return await handler.handle_audio_transcription(request, provider_id, updated_form)
@app.post("/api/v1/audio/speech")
async def v1_audio_speech(request: Request, body: dict):
......@@ -2142,7 +2601,10 @@ async def v1_audio_speech(request: Request, body: dict):
)
body['model'] = actual_model
return await request_handler.handle_text_to_speech(request, provider_id, body)
# Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('request', user_id)
return await handler.handle_text_to_speech(request, provider_id, body)
@app.post("/api/v1/images/generations")
async def v1_image_generations(request: Request, body: dict):
......@@ -2206,7 +2668,10 @@ async def v1_image_generations(request: Request, body: dict):
)
body['model'] = actual_model
return await request_handler.handle_image_generation(request, provider_id, body)
# Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('request', user_id)
return await handler.handle_image_generation(request, provider_id, body)
@app.post("/api/v1/embeddings")
async def v1_embeddings(request: Request, body: dict):
......@@ -2270,7 +2735,10 @@ async def v1_embeddings(request: Request, body: dict):
)
body['model'] = actual_model
return await request_handler.handle_embeddings(request, provider_id, body)
# Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('request', user_id)
return await handler.handle_embeddings(request, provider_id, body)
@app.get("/api/rotations")
async def list_rotations():
......@@ -2324,9 +2792,13 @@ async def rotation_chat_completions(request: Request, body: ChatCompletionReques
logger.debug("Handling rotation request")
try:
# Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('rotation', user_id)
# The rotation handler handles streaming internally and returns
# a StreamingResponse for streaming requests or a dict for non-streaming
result = await rotation_handler.handle_rotation_request(body.model, body_dict)
result = await handler.handle_rotation_request(body.model, body_dict)
logger.debug(f"Rotation response result type: {type(result)}")
return result
except Exception as e:
......@@ -2397,8 +2869,12 @@ async def autoselect_chat_completions(request: Request, body: ChatCompletionRequ
body_dict = body.model_dump()
# Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('autoselect', user_id)
# Check if the model name corresponds to an autoselect configuration
if body.model not in config.autoselect:
if body.model not in config.autoselect and (not user_id or body.model not in handler.user_autoselects):
logger.error(f"Model '{body.model}' not found in autoselect")
logger.error(f"Available autoselect: {list(config.autoselect.keys())}")
raise HTTPException(
......@@ -2412,10 +2888,10 @@ async def autoselect_chat_completions(request: Request, body: ChatCompletionRequ
try:
if body.stream:
logger.debug("Handling streaming autoselect request")
return await autoselect_handler.handle_autoselect_streaming_request(body.model, body_dict)
return await handler.handle_autoselect_streaming_request(body.model, body_dict)
else:
logger.debug("Handling non-streaming autoselect request")
result = await autoselect_handler.handle_autoselect_request(body.model, body_dict)
result = await handler.handle_autoselect_request(body.model, body_dict)
logger.debug(f"Autoselect response result: {result}")
return result
except Exception as e:
......@@ -2457,16 +2933,20 @@ async def chat_completions(provider_id: str, request: Request, body: ChatComplet
body_dict = body.model_dump()
# Get user-specific handler based on the type
user_id = getattr(request.state, 'user_id', None)
# Check if it's an autoselect
if provider_id in config.autoselect:
if provider_id in config.autoselect or (user_id and provider_id in get_user_handler('autoselect', user_id).user_autoselects):
logger.debug("Handling autoselect request")
handler = get_user_handler('autoselect', user_id)
try:
if body.stream:
logger.debug("Handling streaming autoselect request")
return await autoselect_handler.handle_autoselect_streaming_request(provider_id, body_dict)
return await handler.handle_autoselect_streaming_request(provider_id, body_dict)
else:
logger.debug("Handling non-streaming autoselect request")
result = await autoselect_handler.handle_autoselect_request(provider_id, body_dict)
result = await handler.handle_autoselect_request(provider_id, body_dict)
logger.debug(f"Autoselect response result: {result}")
return result
except Exception as e:
......@@ -2474,13 +2954,15 @@ async def chat_completions(provider_id: str, request: Request, body: ChatComplet
raise
# Check if it's a rotation
if provider_id in config.rotations:
if provider_id in config.rotations or (user_id and provider_id in get_user_handler('rotation', user_id).user_rotations):
logger.info(f"Provider ID '{provider_id}' found in rotations")
logger.debug("Handling rotation request")
return await rotation_handler.handle_rotation_request(provider_id, body_dict)
handler = get_user_handler('rotation', user_id)
return await handler.handle_rotation_request(provider_id, body_dict)
# Check if it's a provider
if provider_id not in config.providers:
handler = get_user_handler('request', user_id)
if provider_id not in config.providers and (not user_id or provider_id not in handler.user_providers):
logger.error(f"Provider ID '{provider_id}' not found in providers")
logger.error(f"Available providers: {list(config.providers.keys())}")
logger.error(f"Available rotations: {list(config.rotations.keys())}")
......@@ -2489,9 +2971,9 @@ async def chat_completions(provider_id: str, request: Request, body: ChatComplet
logger.info(f"Provider ID '{provider_id}' found in providers")
provider_config = config.get_provider(provider_id)
provider_config = handler.user_providers.get(provider_id) if user_id and provider_id in handler.user_providers else config.get_provider(provider_id)
logger.debug(f"Provider config: {provider_config}")
# Validate kiro credentials before processing request
if not validate_kiro_credentials(provider_id, provider_config):
raise HTTPException(
......@@ -2502,10 +2984,10 @@ async def chat_completions(provider_id: str, request: Request, body: ChatComplet
try:
if body.stream:
logger.debug("Handling streaming chat completion")
return await request_handler.handle_streaming_chat_completion(request, provider_id, body_dict)
return await handler.handle_streaming_chat_completion(request, provider_id, body_dict)
else:
logger.debug("Handling non-streaming chat completion")
result = await request_handler.handle_chat_completion(request, provider_id, body_dict)
result = await handler.handle_chat_completion(request, provider_id, body_dict)
logger.debug(f"Response result: {result}")
return result
except Exception as e:
......@@ -2516,11 +2998,15 @@ async def chat_completions(provider_id: str, request: Request, body: ChatComplet
async def list_models(request: Request, provider_id: str):
logger.debug(f"Received list_models request for provider: {provider_id}")
# Get user-specific handler based on the type
user_id = getattr(request.state, 'user_id', None)
# Check if it's an autoselect
if provider_id in config.autoselect:
if provider_id in config.autoselect or (user_id and provider_id in get_user_handler('autoselect', user_id).user_autoselects):
logger.debug("Handling autoselect model list request")
handler = get_user_handler('autoselect', user_id)
try:
result = await autoselect_handler.handle_autoselect_model_list(provider_id)
result = await handler.handle_autoselect_model_list(provider_id)
logger.debug(f"Autoselect models result: {result}")
return result
except Exception as e:
......@@ -2528,13 +3014,15 @@ async def list_models(request: Request, provider_id: str):
raise
# Check if it's a rotation
if provider_id in config.rotations:
if provider_id in config.rotations or (user_id and provider_id in get_user_handler('rotation', user_id).user_rotations):
logger.info(f"Provider ID '{provider_id}' found in rotations")
logger.debug("Handling rotation model list request")
return await rotation_handler.handle_rotation_model_list(provider_id)
handler = get_user_handler('rotation', user_id)
return await handler.handle_rotation_model_list(provider_id)
# Check if it's a provider
if provider_id not in config.providers:
handler = get_user_handler('request', user_id)
if provider_id not in config.providers and (not user_id or provider_id not in handler.user_providers):
logger.error(f"Provider ID '{provider_id}' not found in providers")
logger.error(f"Available providers: {list(config.providers.keys())}")
logger.error(f"Available rotations: {list(config.rotations.keys())}")
......@@ -2543,11 +3031,9 @@ async def list_models(request: Request, provider_id: str):
logger.info(f"Provider ID '{provider_id}' found in providers")
provider_config = config.get_provider(provider_id)
try:
logger.debug("Handling model list request")
result = await request_handler.handle_model_list(request, provider_id)
result = await handler.handle_model_list(request, provider_id)
logger.debug(f"Models result: {result}")
return result
except Exception as e:
......@@ -2615,6 +3101,10 @@ async def audio_transcriptions(request: Request):
detail=f"Provider '{provider_id}' not found. Available: {list(config.providers.keys())}"
)
# Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('request', user_id)
# Create new form data with updated model
from starlette.datastructures import FormData
updated_form = FormData()
......@@ -2623,8 +3113,8 @@ async def audio_transcriptions(request: Request):
updated_form[key] = actual_model
else:
updated_form[key] = value
return await request_handler.handle_audio_transcription(request, provider_id, updated_form)
return await handler.handle_audio_transcription(request, provider_id, updated_form)
@app.post("/api/audio/speech")
async def audio_speech(request: Request, body: dict):
......@@ -2678,9 +3168,12 @@ async def audio_speech(request: Request, body: dict):
status_code=400,
detail=f"Provider '{provider_id}' not found. Available: {list(config.providers.keys())}"
)
body['model'] = actual_model
return await request_handler.handle_text_to_speech(request, provider_id, body)
# Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('request', user_id)
return await handler.handle_text_to_speech(request, provider_id, body)
# Image endpoints (supports all three proxy paths)
@app.post("/api/images/generations")
......@@ -2735,9 +3228,12 @@ async def image_generations(request: Request, body: dict):
status_code=400,
detail=f"Provider '{provider_id}' not found. Available: {list(config.providers.keys())}"
)
body['model'] = actual_model
return await request_handler.handle_image_generation(request, provider_id, body)
# Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('request', user_id)
return await handler.handle_image_generation(request, provider_id, body)
# Embeddings endpoint (supports all three proxy paths)
@app.post("/api/embeddings")
......@@ -2802,7 +3298,10 @@ async def embeddings(request: Request, body: dict):
)
body['model'] = actual_model
return await request_handler.handle_embeddings(request, provider_id, body)
# Get user-specific handler
user_id = getattr(request.state, 'user_id', None)
handler = get_user_handler('request', user_id)
return await handler.handle_embeddings(request, provider_id, body)
# Content proxy endpoint
@app.get("/api/proxy/{content_id}")
......@@ -2812,6 +3311,7 @@ async def proxy_content(content_id: str):
logger.info(f"Content ID: {content_id}")
try:
# Get user-specific handler (use global for content proxy as it's shared)
result = await request_handler.handle_content_proxy(content_id)
return result
except Exception as e:
......@@ -3095,7 +3595,9 @@ async def mcp_sse(request: Request):
return
try:
result = await mcp_server.handle_tool_call(tool_name, arguments, auth_level)
# Get user_id from request state if available
user_id = getattr(request.state, 'user_id', None)
result = await mcp_server.handle_tool_call(tool_name, arguments, auth_level, user_id)
response = {
"jsonrpc": "2.0",
"id": request_id,
......@@ -3218,7 +3720,9 @@ async def mcp_post(request: Request):
)
try:
result = await mcp_server.handle_tool_call(tool_name, arguments, auth_level)
# Get user_id from request state if available
user_id = getattr(request.state, 'user_id', None)
result = await mcp_server.handle_tool_call(tool_name, arguments, auth_level, user_id)
return {
"jsonrpc": "2.0",
"id": request_id,
......@@ -3299,7 +3803,9 @@ async def mcp_call_tool(request: Request):
)
try:
result = await mcp_server.handle_tool_call(tool_name, arguments, auth_level)
# Get user_id from request state if available
user_id = getattr(request.state, 'user_id', None)
result = await mcp_server.handle_tool_call(tool_name, arguments, auth_level, user_id)
return {"result": result}
except HTTPException as e:
return JSONResponse(
......
{% extends "base.html" %}
{% block title %}My Autoselects - AISBF{% endblock %}
{% block content %}
<div class="container">
<h1>My Autoselects</h1>
<p>Manage your personal autoselect configurations</p>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<div class="card">
<h2>Autoselect Configurations</h2>
<div id="autoselects-list">
<!-- Autoselects will be loaded here -->
</div>
<button class="btn btn-primary" onclick="showAddAutoselectModal()">Add New Autoselect</button>
</div>
</div>
<!-- Modal for adding/editing autoselects -->
<div id="autoselect-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3 id="modal-title">Add Autoselect</h3>
<button class="close-btn" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body">
<form id="autoselect-form">
<div class="form-group">
<label for="autoselect-name">Autoselect Name:</label>
<input type="text" id="autoselect-name" required>
</div>
<div class="form-group">
<label for="autoselect-config">Autoselect Configuration (JSON):</label>
<textarea id="autoselect-config" rows="20" placeholder='{"model_name": "autoselect-name", "description": "My autoselect config", "fallback": "provider/model"}'></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
</div>
</form>
</div>
</div>
</div>
<script>
let autoselects = {{ user_autoselects_json | safe }};
let currentEditingIndex = -1;
function renderAutoselects() {
const container = document.getElementById('autoselects-list');
container.innerHTML = '';
if (autoselects.length === 0) {
container.innerHTML = '<p class="empty-state">No autoselects configured yet. Click "Add New Autoselect" to get started.</p>';
return;
}
autoselects.forEach((autoselect, index) => {
const div = document.createElement('div');
div.className = 'autoselect-item';
div.innerHTML = `
<div class="autoselect-header">
<h3>${autoselect.autoselect_id}</h3>
<div class="autoselect-actions">
<button class="btn btn-secondary btn-sm" onclick="editAutoselect(${index})">Edit</button>
<button class="btn btn-danger btn-sm" onclick="deleteAutoselect('${autoselect.autoselect_id}')">Delete</button>
</div>
</div>
<div class="autoselect-details">
<p><strong>Created:</strong> ${new Date(autoselect.created_at).toLocaleString()}</p>
<p><strong>Last Updated:</strong> ${new Date(autoselect.updated_at).toLocaleString()}</p>
<details>
<summary>Configuration (JSON)</summary>
<pre>${JSON.stringify(autoselect.config, null, 2)}</pre>
</details>
</div>
`;
container.appendChild(div);
});
}
function showAddAutoselectModal() {
currentEditingIndex = -1;
document.getElementById('modal-title').textContent = 'Add Autoselect';
document.getElementById('autoselect-name').value = '';
document.getElementById('autoselect-config').value = '{"model_name": "my-autoselect", "description": "My autoselect configuration", "fallback": "provider/model"}';
document.getElementById('autoselect-modal').style.display = 'block';
}
function editAutoselect(index) {
currentEditingIndex = index;
const autoselect = autoselects[index];
document.getElementById('modal-title').textContent = 'Edit Autoselect';
document.getElementById('autoselect-name').value = autoselect.autoselect_id;
document.getElementById('autoselect-name').readOnly = true;
document.getElementById('autoselect-config').value = JSON.stringify(autoselect.config, null, 2);
document.getElementById('autoselect-modal').style.display = 'block';
}
function closeModal() {
document.getElementById('autoselect-modal').style.display = 'none';
document.getElementById('autoselect-name').readOnly = false;
currentEditingIndex = -1;
}
function deleteAutoselect(autoselectName) {
if (!confirm(`Are you sure you want to delete the autoselect "${autoselectName}"? This action cannot be undone.`)) return;
fetch('{{ url_for(request, "/dashboard/user/autoselects") }}/' + encodeURIComponent(autoselectName), {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
}).then(response => {
if (response.ok) {
location.reload();
} else {
return response.json().then(data => {
throw new Error(data.error || 'Failed to delete autoselect');
});
}
}).catch(error => {
alert('Error: ' + error.message);
});
}
// Handle form submission
document.getElementById('autoselect-form').addEventListener('submit', function(e) {
e.preventDefault();
const autoselectName = document.getElementById('autoselect-name').value.trim();
const configText = document.getElementById('autoselect-config').value.trim();
if (!autoselectName) {
alert('Autoselect name is required');
return;
}
if (!configText) {
alert('Autoselect configuration is required');
return;
}
let configObj;
try {
configObj = JSON.parse(configText);
} catch (e) {
alert('Invalid JSON configuration: ' + e.message);
return;
}
const formData = new FormData();
formData.append('autoselect_name', autoselectName);
formData.append('autoselect_config', JSON.stringify(configObj));
const url = '{{ url_for(request, "/dashboard/user/autoselects") }}';
const method = currentEditingIndex >= 0 ? 'PUT' : 'POST';
fetch(url, {
method: method,
body: formData
}).then(response => {
if (response.ok) {
closeModal();
location.reload();
} else {
return response.text().then(text => {
throw new Error(text || 'Failed to save autoselect');
});
}
}).catch(error => {
alert('Error: ' + error.message);
});
});
renderAutoselects();
</script>
<style>
.autoselect-item {
background: #1a1a2e;
padding: 1rem;
margin: 1rem 0;
border-radius: 8px;
}
.autoselect-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.autoselect-header h3 {
margin: 0;
}
.autoselect-actions {
display: flex;
gap: 0.5rem;
}
.autoselect-details p {
margin: 0.5rem 0;
}
.autoselect-details pre {
background: #0f3460;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
.empty-state {
text-align: center;
color: #a0a0a0;
padding: 2rem;
}
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: #1a1a2e;
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #0f3460;
}
.modal-header h3 {
margin: 0;
}
.close-btn {
background: none;
border: none;
color: #e0e0e0;
font-size: 1.5rem;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-body {
padding: 1rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #e0e0e0;
}
.form-group input[type="text"],
.form-group textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid #0f3460;
border-radius: 4px;
background: #16213e;
color: #e0e0e0;
font-family: monospace;
font-size: 14px;
}
.form-group textarea {
resize: vertical;
min-height: 300px;
}
.form-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
margin-top: 1rem;
}
</style>
{% endblock %}
\ No newline at end of file
{% extends "base.html" %}
{% block title %}User Dashboard - AISBF{% endblock %}
{% block content %}
<div class="container">
<h1>User Dashboard</h1>
<p>Welcome, {{ session.username }}!</p>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<!-- Usage Statistics -->
<div class="card">
<h2>Usage Statistics</h2>
<div class="stats-grid">
<div class="stat-item">
<h3>Total Tokens Used</h3>
<p class="stat-value">{{ usage_stats.total_tokens|default(0) }}</p>
</div>
<div class="stat-item">
<h3>Requests Today</h3>
<p class="stat-value">{{ usage_stats.requests_today|default(0) }}</p>
</div>
<div class="stat-item">
<h3>Active Providers</h3>
<p class="stat-value">{{ providers_count|default(0) }}</p>
</div>
<div class="stat-item">
<h3>Active Rotations</h3>
<p class="stat-value">{{ rotations_count|default(0) }}</p>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="card">
<h2>Quick Actions</h2>
<div class="button-group">
<a href="{{ url_for(request, '/dashboard/user/providers') }}" class="btn btn-primary">Manage Providers</a>
<a href="{{ url_for(request, '/dashboard/user/rotations') }}" class="btn btn-primary">Manage Rotations</a>
<a href="{{ url_for(request, '/dashboard/user/autoselect') }}" class="btn btn-primary">Manage Autoselect</a>
<a href="{{ url_for(request, '/dashboard/user/tokens') }}" class="btn btn-primary">API Tokens</a>
</div>
</div>
<!-- Recent Activity -->
<div class="card">
<h2>Recent Activity</h2>
<table class="table">
<thead>
<tr>
<th>Timestamp</th>
<th>Provider</th>
<th>Model</th>
<th>Tokens</th>
</tr>
</thead>
<tbody>
{% if recent_activity %}
{% for activity in recent_activity %}
<tr>
<td>{{ activity.timestamp }}</td>
<td>{{ activity.provider_id }}</td>
<td>{{ activity.model }}</td>
<td>{{ activity.token_count }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="4" style="text-align: center;">No recent activity</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
<style>
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.stat-item {
background: #1a1a2e;
padding: 1.5rem;
border-radius: 8px;
text-align: center;
}
.stat-item h3 {
font-size: 0.9rem;
color: #888;
margin-bottom: 0.5rem;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
color: #16213e;
}
.button-group {
display: flex;
gap: 1rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.table {
width: 100%;
margin-top: 1rem;
border-collapse: collapse;
}
.table th,
.table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #333;
}
.table th {
background: #1a1a2e;
font-weight: bold;
}
.table tbody tr:hover {
background: #1a1a2e;
}
</style>
{% endblock %}
{% extends "base.html" %}
{% block title %}My Providers - AISBF{% endblock %}
{% block content %}
<div class="container">
<h1>My Providers</h1>
<p>Manage your personal provider configurations</p>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<div class="card">
<h2>Provider Configurations</h2>
<div id="providers-list">
<!-- Providers will be loaded here -->
</div>
<button class="btn btn-primary" onclick="showAddProviderModal()">Add New Provider</button>
</div>
</div>
<!-- Modal for adding/editing providers -->
<div id="provider-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3 id="modal-title">Add Provider</h3>
<button class="close-btn" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body">
<form id="provider-form">
<div class="form-group">
<label for="provider-name">Provider Name:</label>
<input type="text" id="provider-name" required>
</div>
<div class="form-group">
<label for="provider-config">Provider Configuration (JSON):</label>
<textarea id="provider-config" rows="20" placeholder='{"endpoint": "https://api.example.com", "api_key": "your-api-key", "models": []}'></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
</div>
</form>
</div>
</div>
</div>
<script>
let providers = {{ user_providers_json | safe }};
let currentEditingIndex = -1;
function renderProviders() {
const container = document.getElementById('providers-list');
container.innerHTML = '';
if (providers.length === 0) {
container.innerHTML = '<p class="empty-state">No providers configured yet. Click "Add New Provider" to get started.</p>';
return;
}
providers.forEach((provider, index) => {
const div = document.createElement('div');
div.className = 'provider-item';
div.innerHTML = `
<div class="provider-header">
<h3>${provider.provider_id}</h3>
<div class="provider-actions">
<button class="btn btn-secondary btn-sm" onclick="editProvider(${index})">Edit</button>
<button class="btn btn-danger btn-sm" onclick="deleteProvider('${provider.provider_id}')">Delete</button>
</div>
</div>
<div class="provider-details">
<p><strong>Created:</strong> ${new Date(provider.created_at).toLocaleString()}</p>
<p><strong>Last Updated:</strong> ${new Date(provider.updated_at).toLocaleString()}</p>
<details>
<summary>Configuration (JSON)</summary>
<pre>${JSON.stringify(provider.config, null, 2)}</pre>
</details>
</div>
`;
container.appendChild(div);
});
}
function showAddProviderModal() {
currentEditingIndex = -1;
document.getElementById('modal-title').textContent = 'Add Provider';
document.getElementById('provider-name').value = '';
document.getElementById('provider-config').value = '{"endpoint": "https://api.example.com", "api_key": "your-api-key", "models": []}';
document.getElementById('provider-modal').style.display = 'block';
}
function editProvider(index) {
currentEditingIndex = index;
const provider = providers[index];
document.getElementById('modal-title').textContent = 'Edit Provider';
document.getElementById('provider-name').value = provider.provider_id;
document.getElementById('provider-name').readOnly = true;
document.getElementById('provider-config').value = JSON.stringify(provider.config, null, 2);
document.getElementById('provider-modal').style.display = 'block';
}
function closeModal() {
document.getElementById('provider-modal').style.display = 'none';
document.getElementById('provider-name').readOnly = false;
currentEditingIndex = -1;
}
function deleteProvider(providerName) {
if (!confirm(`Are you sure you want to delete the provider "${providerName}"? This action cannot be undone.`)) return;
fetch('{{ url_for(request, "/dashboard/user/providers") }}/' + encodeURIComponent(providerName), {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
}).then(response => {
if (response.ok) {
location.reload();
} else {
return response.json().then(data => {
throw new Error(data.error || 'Failed to delete provider');
});
}
}).catch(error => {
alert('Error: ' + error.message);
});
}
// Handle form submission
document.getElementById('provider-form').addEventListener('submit', function(e) {
e.preventDefault();
const providerName = document.getElementById('provider-name').value.trim();
const configText = document.getElementById('provider-config').value.trim();
if (!providerName) {
alert('Provider name is required');
return;
}
if (!configText) {
alert('Provider configuration is required');
return;
}
let configObj;
try {
configObj = JSON.parse(configText);
} catch (e) {
alert('Invalid JSON configuration: ' + e.message);
return;
}
const formData = new FormData();
formData.append('provider_name', providerName);
formData.append('provider_config', JSON.stringify(configObj));
const url = '{{ url_for(request, "/dashboard/user/providers") }}';
const method = currentEditingIndex >= 0 ? 'PUT' : 'POST';
fetch(url, {
method: method,
body: formData
}).then(response => {
if (response.ok) {
closeModal();
location.reload();
} else {
return response.text().then(text => {
throw new Error(text || 'Failed to save provider');
});
}
}).catch(error => {
alert('Error: ' + error.message);
});
});
renderProviders();
</script>
<style>
.provider-item {
background: #1a1a2e;
padding: 1rem;
margin: 1rem 0;
border-radius: 8px;
}
.provider-item h3 {
margin-top: 0;
}
.provider-item code {
background: #0f3460;
padding: 0.2rem 0.5rem;
border-radius: 4px;
}
</style>
{% endblock %}
{% extends "base.html" %}
{% block title %}My Rotations - AISBF{% endblock %}
{% block content %}
<div class="container">
<h1>My Rotations</h1>
<p>Manage your personal rotation configurations</p>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<div class="card">
<h2>Rotation Configurations</h2>
<div id="rotations-list">
<!-- Rotations will be loaded here -->
</div>
<button class="btn btn-primary" onclick="showAddRotationModal()">Add New Rotation</button>
</div>
</div>
<!-- Modal for adding/editing rotations -->
<div id="rotation-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3 id="modal-title">Add Rotation</h3>
<button class="close-btn" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body">
<form id="rotation-form">
<div class="form-group">
<label for="rotation-name">Rotation Name:</label>
<input type="text" id="rotation-name" required>
</div>
<div class="form-group">
<label for="rotation-config">Rotation Configuration (JSON):</label>
<textarea id="rotation-config" rows="20" placeholder='{"model_name": "rotation-name", "providers": []}'></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
</div>
</form>
</div>
</div>
</div>
<script>
let rotations = {{ user_rotations_json | safe }};
let currentEditingIndex = -1;
function renderRotations() {
const container = document.getElementById('rotations-list');
container.innerHTML = '';
if (rotations.length === 0) {
container.innerHTML = '<p class="empty-state">No rotations configured yet. Click "Add New Rotation" to get started.</p>';
return;
}
rotations.forEach((rotation, index) => {
const div = document.createElement('div');
div.className = 'rotation-item';
div.innerHTML = `
<div class="rotation-header">
<h3>${rotation.rotation_id}</h3>
<div class="rotation-actions">
<button class="btn btn-secondary btn-sm" onclick="editRotation(${index})">Edit</button>
<button class="btn btn-danger btn-sm" onclick="deleteRotation('${rotation.rotation_id}')">Delete</button>
</div>
</div>
<div class="rotation-details">
<p><strong>Created:</strong> ${new Date(rotation.created_at).toLocaleString()}</p>
<p><strong>Last Updated:</strong> ${new Date(rotation.updated_at).toLocaleString()}</p>
<details>
<summary>Configuration (JSON)</summary>
<pre>${JSON.stringify(rotation.config, null, 2)}</pre>
</details>
</div>
`;
container.appendChild(div);
});
}
function showAddRotationModal() {
currentEditingIndex = -1;
document.getElementById('modal-title').textContent = 'Add Rotation';
document.getElementById('rotation-name').value = '';
document.getElementById('rotation-config').value = '{"model_name": "my-rotation", "providers": []}';
document.getElementById('rotation-modal').style.display = 'block';
}
function editRotation(index) {
currentEditingIndex = index;
const rotation = rotations[index];
document.getElementById('modal-title').textContent = 'Edit Rotation';
document.getElementById('rotation-name').value = rotation.rotation_id;
document.getElementById('rotation-name').readOnly = true;
document.getElementById('rotation-config').value = JSON.stringify(rotation.config, null, 2);
document.getElementById('rotation-modal').style.display = 'block';
}
function closeModal() {
document.getElementById('rotation-modal').style.display = 'none';
document.getElementById('rotation-name').readOnly = false;
currentEditingIndex = -1;
}
function deleteRotation(rotationName) {
if (!confirm(`Are you sure you want to delete the rotation "${rotationName}"? This action cannot be undone.`)) return;
fetch('{{ url_for(request, "/dashboard/user/rotations") }}/' + encodeURIComponent(rotationName), {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
}).then(response => {
if (response.ok) {
location.reload();
} else {
return response.json().then(data => {
throw new Error(data.error || 'Failed to delete rotation');
});
}
}).catch(error => {
alert('Error: ' + error.message);
});
}
// Handle form submission
document.getElementById('rotation-form').addEventListener('submit', function(e) {
e.preventDefault();
const rotationName = document.getElementById('rotation-name').value.trim();
const configText = document.getElementById('rotation-config').value.trim();
if (!rotationName) {
alert('Rotation name is required');
return;
}
if (!configText) {
alert('Rotation configuration is required');
return;
}
let configObj;
try {
configObj = JSON.parse(configText);
} catch (e) {
alert('Invalid JSON configuration: ' + e.message);
return;
}
const formData = new FormData();
formData.append('rotation_name', rotationName);
formData.append('rotation_config', JSON.stringify(configObj));
const url = '{{ url_for(request, "/dashboard/user/rotations") }}';
const method = currentEditingIndex >= 0 ? 'PUT' : 'POST';
fetch(url, {
method: method,
body: formData
}).then(response => {
if (response.ok) {
closeModal();
location.reload();
} else {
return response.text().then(text => {
throw new Error(text || 'Failed to save rotation');
});
}
}).catch(error => {
alert('Error: ' + error.message);
});
});
renderRotations();
</script>
<style>
.rotation-item {
background: #1a1a2e;
padding: 1rem;
margin: 1rem 0;
border-radius: 8px;
}
.rotation-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.rotation-header h3 {
margin: 0;
}
.rotation-actions {
display: flex;
gap: 0.5rem;
}
.rotation-details p {
margin: 0.5rem 0;
}
.rotation-details pre {
background: #0f3460;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
.empty-state {
text-align: center;
color: #a0a0a0;
padding: 2rem;
}
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: #1a1a2e;
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #0f3460;
}
.modal-header h3 {
margin: 0;
}
.close-btn {
background: none;
border: none;
color: #e0e0e0;
font-size: 1.5rem;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-body {
padding: 1rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #e0e0e0;
}
.form-group input[type="text"],
.form-group textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid #0f3460;
border-radius: 4px;
background: #16213e;
color: #e0e0e0;
font-family: monospace;
font-size: 14px;
}
.form-group textarea {
resize: vertical;
min-height: 300px;
}
.form-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
margin-top: 1rem;
}
</style>
{% endblock %}
\ No newline at end of file
{% extends "base.html" %}
{% block title %}My API Tokens - AISBF{% endblock %}
{% block content %}
<div class="container">
<h1>My API Tokens</h1>
<p>Manage your personal API tokens for accessing the AISBF API</p>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<div class="card">
<h2>API Tokens</h2>
<div id="tokens-list">
<!-- Tokens will be loaded here -->
</div>
<button class="btn btn-primary" onclick="createToken()">Create New Token</button>
</div>
<!-- Modal for creating tokens -->
<div id="token-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>Create New API Token</h3>
<button class="close-btn" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body">
<div id="token-creation-form">
<form id="create-token-form">
<div class="form-group">
<label for="token-description">Description (optional):</label>
<input type="text" id="token-description" placeholder="e.g., My application token">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Create Token</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
</div>
</form>
</div>
<div id="token-result" style="display: none;">
<div class="alert alert-success">
<h4>Token Created Successfully!</h4>
<p><strong>Token:</strong> <code id="created-token"></code></p>
<p class="warning-text">⚠️ <strong>Important:</strong> Copy this token now. You won't be able to see it again!</p>
<div class="form-actions">
<button type="button" class="btn btn-primary" onclick="copyToken()">Copy Token</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Close</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
let tokens = {{ user_tokens | tojson }};
function renderTokens() {
const container = document.getElementById('tokens-list');
container.innerHTML = '';
if (tokens.length === 0) {
container.innerHTML = '<p class="empty-state">No API tokens created yet. Click "Create New Token" to get started.</p>';
return;
}
tokens.forEach((token) => {
const div = document.createElement('div');
div.className = 'token-item';
div.innerHTML = `
<div class="token-header">
<div class="token-info">
<h4>${token.description || 'No description'}</h4>
<p class="token-meta">
Created: ${new Date(token.created_at).toLocaleString()}
${token.last_used ? `| Last used: ${new Date(token.last_used).toLocaleString()}` : ''}
</p>
</div>
<div class="token-actions">
<span class="token-status ${token.is_active ? 'active' : 'inactive'}">
${token.is_active ? 'Active' : 'Inactive'}
</span>
<button class="btn btn-danger btn-sm" onclick="deleteToken(${token.id})">Delete</button>
</div>
</div>
<div class="token-details">
<p><strong>Token ID:</strong> ${token.id}</p>
<p><strong>Token:</strong> ${token.token.substring(0, 20)}...</p>
</div>
`;
container.appendChild(div);
});
}
function createToken() {
document.getElementById('token-creation-form').style.display = 'block';
document.getElementById('token-result').style.display = 'none';
document.getElementById('create-token-form').reset();
document.getElementById('token-modal').style.display = 'block';
}
function closeModal() {
document.getElementById('token-modal').style.display = 'none';
}
function deleteToken(tokenId) {
if (!confirm('Are you sure you want to delete this API token? This action cannot be undone and will immediately revoke access.')) return;
fetch('{{ url_for(request, "/dashboard/user/tokens") }}/' + tokenId, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
}).then(response => {
if (response.ok) {
location.reload();
} else {
return response.json().then(data => {
throw new Error(data.error || 'Failed to delete token');
});
}
}).catch(error => {
alert('Error: ' + error.message);
});
}
function copyToken() {
const tokenElement = document.getElementById('created-token');
const token = tokenElement.textContent;
navigator.clipboard.writeText(token).then(() => {
alert('Token copied to clipboard!');
}).catch(() => {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = token;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
alert('Token copied to clipboard!');
});
}
// Handle form submission
document.getElementById('create-token-form').addEventListener('submit', function(e) {
e.preventDefault();
const description = document.getElementById('token-description').value.trim();
const formData = new FormData();
if (description) {
formData.append('description', description);
}
fetch('{{ url_for(request, "/dashboard/user/tokens") }}', {
method: 'POST',
body: formData
}).then(response => {
if (response.ok) {
return response.json();
} else {
return response.json().then(data => {
throw new Error(data.error || 'Failed to create token');
});
}
}).then(data => {
// Show the created token
document.getElementById('created-token').textContent = data.token;
document.getElementById('token-creation-form').style.display = 'none';
document.getElementById('token-result').style.display = 'block';
// Refresh the token list
tokens.push({
id: data.token_id,
token: data.token,
description: description || null,
created_at: new Date().toISOString(),
last_used: null,
is_active: true
});
renderTokens();
}).catch(error => {
alert('Error: ' + error.message);
});
});
renderTokens();
</script>
<style>
.token-item {
background: #1a1a2e;
padding: 1rem;
margin: 1rem 0;
border-radius: 8px;
border: 1px solid #0f3460;
}
.token-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.token-info h4 {
margin: 0 0 0.5rem 0;
color: #e0e0e0;
}
.token-meta {
color: #a0a0a0;
font-size: 0.9rem;
margin: 0;
}
.token-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.token-status {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
}
.token-status.active {
background: #28a745;
color: white;
}
.token-status.inactive {
background: #dc3545;
color: white;
}
.token-details {
background: #16213e;
padding: 1rem;
border-radius: 4px;
font-family: monospace;
font-size: 0.9rem;
}
.token-details p {
margin: 0.5rem 0;
word-break: break-all;
}
.empty-state {
text-align: center;
color: #a0a0a0;
padding: 2rem;
}
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: #1a1a2e;
border-radius: 8px;
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #0f3460;
}
.modal-header h3 {
margin: 0;
}
.close-btn {
background: none;
border: none;
color: #e0e0e0;
font-size: 1.5rem;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.modal-body {
padding: 1rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #e0e0e0;
}
.form-group input[type="text"] {
width: 100%;
padding: 0.5rem;
border: 1px solid #0f3460;
border-radius: 4px;
background: #16213e;
color: #e0e0e0;
font-size: 14px;
}
.form-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
margin-top: 1rem;
}
.warning-text {
color: #ffc107;
font-weight: 500;
}
#created-token {
background: #0f3460;
padding: 0.5rem;
border-radius: 4px;
font-family: monospace;
word-break: break-all;
}
</style>
{% endblock %}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment