Commit 76c5318f authored by Your Name's avatar Your Name

Add user-specific API endpoints and MCP configuration

- Added /api/user/* endpoints for authenticated users to access their own configurations
- Admin users get access to global + user configs, regular users get user-only
- Global tokens from aisbf.json have full access to all configurations
- Enhanced MCP with user-specific tools for authenticated users
- Updated user dashboard with comprehensive API endpoint documentation
- Updated README.md, DOCUMENTATION.md with new endpoint documentation
- Updated CHANGELOG.md with new features
- Bumped version to 0.9.1
parent 53558e03
...@@ -2,6 +2,21 @@ ...@@ -2,6 +2,21 @@
## [Unreleased] ## [Unreleased]
### Added ### Added
- **User-Specific API Endpoints**: New API endpoints for authenticated users to access their own configurations
- `GET /api/user/models` - List user's own models
- `GET /api/user/providers` - List user's provider configurations
- `GET /api/user/rotations` - List user's rotation configurations
- `GET /api/user/autoselects` - List user's autoselect configurations
- `POST /api/user/chat/completions` - Chat completions using user's own models
- `GET /api/user/{config_type}/models` - List models for specific config type
- Requires Bearer token or query parameter authentication
- Admin users get access to global + user configs, regular users get user-only configs
- Global tokens (in aisbf.json) have full access to all configurations
- **MCP User Configuration**: Enhanced MCP server with user-specific tools for authenticated users
- User can configure their own models, providers, autoselects, and rotations through MCP
- Admin users get access to both global and user tools
- Regular users get access to user-only tools
- **Dashboard API Documentation**: User dashboard now includes comprehensive API endpoint documentation
- **Adaptive Rate Limiting**: Intelligent rate limit management that learns from 429 responses - **Adaptive Rate Limiting**: Intelligent rate limit management that learns from 429 responses
- Per-provider adaptive rate limiters with learning capability - Per-provider adaptive rate limiters with learning capability
- Exponential backoff with jitter (configurable base and jitter factor) - Exponential backoff with jitter (configurable base and jitter factor)
......
...@@ -563,6 +563,68 @@ Cache backend can be configured via the web dashboard or configuration file: ...@@ -563,6 +563,68 @@ Cache backend can be configured via the web dashboard or configuration file:
- Supports both streaming and non-streaming responses - Supports both streaming and non-streaming responses
- `GET /api/autoselect/models` - List all models across all autoselect configurations - `GET /api/autoselect/models` - List all models across all autoselect configurations
### User-Specific API Endpoints
Authenticated users can access their own configurations via user-specific API endpoints. These endpoints require either a valid API token (generated in the user dashboard) or session authentication.
#### Authentication
**Option 1: Bearer Token (Recommended for API access)**
```bash
Authorization: Bearer YOUR_API_TOKEN
```
**Option 2: Query Parameter**
```bash
?token=YOUR_API_TOKEN
```
#### User API Endpoints
| Endpoint | Description |
|----------|-------------|
| `GET /api/user/models` | List available models from user's own configurations |
| `GET /api/user/providers` | List user's provider configurations |
| `GET /api/user/rotations` | List user's rotation configurations |
| `GET /api/user/autoselects` | List user's autoselect configurations |
| `POST /api/user/chat/completions` | Chat completions using user's own models |
| `GET /api/user/{config_type}/models` | List models for specific config type (provider, rotation, autoselect) |
**Access Control:**
- **Admin Users** have access to both global and user configurations when using user API endpoints
- **Regular Users** can only access their own configurations
- **Global Tokens** (configured in aisbf.json) have full access to all configurations
#### Example: Using User API with cURL
```bash
# List user models
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/models
# Chat using user's own models
curl -X POST -H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"model": "your-rotation/model", "messages": [{"role": "user", "content": "Hello"}]}' \
http://localhost:17765/api/user/chat/completions
```
### MCP (Model Context Protocol)
AISBF provides an MCP server for remote agent configuration and model access:
- **SSE Endpoint**: `GET /mcp` - Server-Sent Events for MCP communication
- **HTTP Endpoint**: `POST /mcp` - Direct HTTP transport for MCP
MCP tools include:
- `list_models` - List available models (user or global depending on auth)
- `chat_completions` - Send chat completion requests
- `get_providers` - Get provider configurations
- `get_rotations` - Get rotation configurations
- `get_autoselects` - Get autoselect configurations
- And more for authenticated users to manage their own configs
User tokens authenticate MCP requests, with admin users getting full access and regular users getting user-only access.
## Provider Support ## Provider Support
AISBF supports the following AI providers: AISBF supports the following AI providers:
......
...@@ -648,6 +648,69 @@ These endpoints are maintained for backward compatibility: ...@@ -648,6 +648,69 @@ These endpoints are maintained for backward compatibility:
- `GET /api/autoselect/models` - List all models across all autoselect configurations - `GET /api/autoselect/models` - List all models across all autoselect configurations
- `GET /api/{provider_id}/models` - List available models for a specific provider, rotation, or autoselect - `GET /api/{provider_id}/models` - List available models for a specific provider, rotation, or autoselect
### User-Specific API Endpoints
Authenticated users can access their own configurations via user-specific API endpoints. These endpoints require either a valid API token (generated in the user dashboard) or session authentication.
#### Authentication
**Option 1: Bearer Token (Recommended for API access)**
```bash
Authorization: Bearer YOUR_API_TOKEN
```
**Option 2: Query Parameter**
```bash
?token=YOUR_API_TOKEN
```
#### User API Endpoints
| Endpoint | Description |
|----------|-------------|
| `GET /api/user/models` | List available models from user's own configurations |
| `GET /api/user/providers` | List user's provider configurations |
| `GET /api/user/rotations` | List user's rotation configurations |
| `GET /api/user/autoselects` | List user's autoselect configurations |
| `POST /api/user/chat/completions` | Chat completions using user's own models |
| `GET /api/user/{config_type}/models` | List models for specific config type (provider, rotation, autoselect) |
**Admin Users** have access to both global and user configurations when using user API endpoints.
**Regular Users** can only access their own configurations.
**Global Tokens** (configured in aisbf.json) have full access to all configurations.
#### Example: Using User API with cURL
```bash
# List user models
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/models
# Chat using user's own models
curl -X POST -H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"model": "your-rotation/model", "messages": [{"role": "user", "content": "Hello"}]}' \
http://localhost:17765/api/user/chat/completions
```
### MCP (Model Context Protocol)
AISBF provides an MCP server for remote agent configuration and model access:
- **SSE Endpoint**: `GET /mcp` - Server-Sent Events for MCP communication
- **HTTP Endpoint**: `POST /mcp` - Direct HTTP transport for MCP
MCP tools include:
- `list_models` - List available models (user or global depending on auth)
- `chat_completions` - Send chat completion requests
- `get_providers` - Get provider configurations
- `get_rotations` - Get rotation configurations
- `get_autoselects` - Get autoselect configurations
- And more for authenticated users to manage their own configs
User tokens authenticate MCP requests, with admin users getting full access and regular users getting user-only access.
### Content Proxy ### Content Proxy
- `GET /api/proxy/{content_id}` - Proxy generated content (images, audio, etc.) - `GET /api/proxy/{content_id}` - Proxy generated content (images, audio, etc.)
......
...@@ -141,6 +141,16 @@ class AutoselectConfig(BaseModel): ...@@ -141,6 +141,16 @@ class AutoselectConfig(BaseModel):
pricing: Optional[Dict] = None pricing: Optional[Dict] = None
supported_parameters: Optional[List[str]] = None supported_parameters: Optional[List[str]] = None
default_parameters: Optional[Dict] = None default_parameters: Optional[Dict] = None
# Default settings for models in this autoselect
default_rate_limit: Optional[float] = None
default_max_request_tokens: Optional[int] = None
default_rate_limit_TPM: Optional[int] = None
default_rate_limit_TPH: Optional[int] = None
default_rate_limit_TPD: Optional[int] = None
default_context_size: Optional[int] = None
default_condense_context: Optional[int] = None
default_condense_method: Optional[Union[str, List[str]]] = None
default_error_cooldown: Optional[int] = None # Default cooldown period in seconds after 3 consecutive failures (default: 300)
# Response caching control # Response caching control
enable_response_cache: Optional[bool] = None # Enable/disable response caching for this autoselect (None = use global default) enable_response_cache: Optional[bool] = None # Enable/disable response caching for this autoselect (None = use global default)
......
...@@ -971,9 +971,10 @@ def get_context_config_for_model( ...@@ -971,9 +971,10 @@ def get_context_config_for_model(
Priority order for each field: Priority order for each field:
1. Rotation model config (if explicitly set) 1. Rotation model config (if explicitly set)
2. Provider model-specific config (if exists) 2. Model-specific config in provider (if exists)
3. Provider default config (fallback) 3. First model in provider (auto-derived from dynamic fetch)
4. System default (0 for condense_context, None for others) 4. Provider default config (fallback)
5. System default (0 for condense_context, None for others)
Args: Args:
model_name: Name of the model model_name: Name of the model
...@@ -1007,12 +1008,23 @@ def get_context_config_for_model( ...@@ -1007,12 +1008,23 @@ def get_context_config_for_model(
model_specific_config = model model_specific_config = model
break break
# Build base config from provider (model-specific > provider defaults) # Build base config from provider (model-specific > first model > provider defaults)
# context_size # context_size
if model_specific_config and model_specific_config.get('context_size') is not None: if model_specific_config and model_specific_config.get('context_size') is not None:
context_config['context_size'] = model_specific_config.get('context_size') context_config['context_size'] = model_specific_config.get('context_size')
elif hasattr(provider_config, 'default_context_size') and provider_config.default_context_size is not None: elif hasattr(provider_config, 'default_context_size') and provider_config.default_context_size is not None:
context_config['context_size'] = provider_config.default_context_size context_config['context_size'] = provider_config.default_context_size
else:
# Auto-derive from first model in provider (has context_size from dynamic fetch)
if provider_config.models and len(provider_config.models) > 0:
first_model = provider_config.models[0]
# Check for context_size in the first model (from dynamic fetch)
if hasattr(first_model, 'context_size') and first_model.context_size:
context_config['context_size'] = first_model.context_size
elif hasattr(first_model, 'context_window') and first_model.context_window:
context_config['context_size'] = first_model.context_window
elif hasattr(first_model, 'context_length') and first_model.context_length:
context_config['context_size'] = first_model.context_length
# condense_context # condense_context
if model_specific_config and model_specific_config.get('condense_context') is not None: if model_specific_config and model_specific_config.get('condense_context') is not None:
...@@ -1033,11 +1045,69 @@ def get_context_config_for_model( ...@@ -1033,11 +1045,69 @@ def get_context_config_for_model(
# Only override if the field is explicitly set in rotation config # Only override if the field is explicitly set in rotation config
if 'context_size' in rotation_model_config and rotation_model_config['context_size'] is not None: if 'context_size' in rotation_model_config and rotation_model_config['context_size'] is not None:
context_config['context_size'] = rotation_model_config['context_size'] context_config['context_size'] = rotation_model_config['context_size']
elif context_config.get('context_size') is None:
# If context_size is still None, check if rotation_model_config has default_context_size
# (This handles the case where rotation-level default should be used)
if 'default_context_size' in rotation_model_config and rotation_model_config['default_context_size'] is not None:
context_config['context_size'] = rotation_model_config['default_context_size']
if 'condense_context' in rotation_model_config and rotation_model_config['condense_context'] is not None: if 'condense_context' in rotation_model_config and rotation_model_config['condense_context'] is not None:
context_config['condense_context'] = rotation_model_config['condense_context'] context_config['condense_context'] = rotation_model_config['condense_context']
elif context_config.get('condense_context') == 0:
# If condense_context is still at default (0), check if rotation has default
if 'default_condense_context' in rotation_model_config and rotation_model_config['default_condense_context'] is not None:
context_config['condense_context'] = rotation_model_config['default_condense_context']
if 'condense_method' in rotation_model_config and rotation_model_config['condense_method'] is not None: if 'condense_method' in rotation_model_config and rotation_model_config['condense_method'] is not None:
context_config['condense_method'] = rotation_model_config['condense_method'] context_config['condense_method'] = rotation_model_config['condense_method']
return context_config # Step 3: Final fallback if context_size is still None
\ No newline at end of file # Use inference based on model name patterns
if context_config.get('context_size') is None:
context_config['context_size'] = _infer_context_size_from_model(model_name)
return context_config
def _infer_context_size_from_model(model_name: str) -> int:
"""
Infer context window size from model name patterns.
Args:
model_name: Name of the model
Returns:
Inferred context size in tokens
"""
model_lower = model_name.lower()
# Known model patterns
if 'gpt-4' in model_lower:
if 'turbo' in model_lower or '1106' in model_lower or '0125' in model_lower:
return 128000
return 8192
elif 'gpt-3.5' in model_lower:
if 'turbo' in model_lower and ('1106' in model_lower or '0125' in model_lower):
return 16385
return 4096
elif 'claude-3' in model_lower:
return 200000
elif 'claude-2' in model_lower:
return 100000
elif 'gemini' in model_lower:
if '1.5' in model_lower:
return 2000000 if 'pro' in model_lower else 1000000
elif '2.0' in model_lower:
return 1000000
return 32000
elif 'llama' in model_lower:
if '3' in model_lower:
return 128000
return 4096
elif 'mistral' in model_lower:
if 'large' in model_lower:
return 32000
return 8192
# Generic default
return 8192
\ No newline at end of file
...@@ -1505,7 +1505,8 @@ class RotationHandler: ...@@ -1505,7 +1505,8 @@ class RotationHandler:
Priority order: Priority order:
1. Model-specific settings (highest priority) 1. Model-specific settings (highest priority)
2. Rotation default settings 2. Rotation default settings
3. Provider default settings (lowest priority) 3. Provider default settings
4. Auto-derived from first model in provider (lowest priority)
Args: Args:
model: The model configuration dict model: The model configuration dict
...@@ -1539,9 +1540,108 @@ class RotationHandler: ...@@ -1539,9 +1540,108 @@ class RotationHandler:
provider_default = getattr(provider_config, f'default_{field}', None) provider_default = getattr(provider_config, f'default_{field}', None)
if provider_default is not None: if provider_default is not None:
model[field] = provider_default model[field] = provider_default
else:
# Auto-derive from first model in provider config if available
if provider_config and provider_config.models and len(provider_config.models) > 0:
first_model = provider_config.models[0]
# For context_size, check multiple field names (from dynamic fetch)
if field == 'context_size':
model_field = getattr(first_model, 'context_size', None)
if model_field is None:
model_field = getattr(first_model, 'context_window', None)
if model_field is None:
model_field = getattr(first_model, 'context_length', None)
else:
model_field = getattr(first_model, field, None)
if model_field is not None:
model[field] = model_field
return model return model
def _apply_defaults_to_autoselect_model(self, model_config: Dict, autoselect_config) -> Dict:
"""
Apply default settings to an autoselect model configuration.
Priority order:
1. Model-specific settings (highest priority)
2. Autoselect default settings
3. Auto-derived from first model in rotation (lowest priority)
Args:
model_config: The model configuration dict (typically a rotation_id from autoselect)
autoselect_config: The autoselect configuration
Returns:
Model dict with defaults applied
"""
import logging
logger = logging.getLogger(__name__)
# List of fields that can have defaults
default_fields = [
'rate_limit',
'max_request_tokens',
'rate_limit_TPM',
'rate_limit_TPH',
'rate_limit_TPD',
'context_size',
'condense_context',
'condense_method'
]
# First, check if the model_config is a rotation ID and get its settings
model_id = model_config.get('model_id') or model_config.get('name') or model_config.get('id', '')
# Try to get defaults from the referenced rotation (first model in the rotation)
if model_id in self.config.rotations:
rotation_config = self.config.rotations[model_id]
# Check each default field
for field in default_fields:
# If field is not set in model, try autoselect defaults, then rotation defaults
if field not in model_config or model_config.get(field) is None:
# Try autoselect defaults first
autoselect_default = getattr(autoselect_config, f'default_{field}', None)
if autoselect_default is not None:
model_config[field] = autoselect_default
logger.debug(f"Applied autoselect default_{field}: {autoselect_default} to model {model_id}")
else:
# Try rotation defaults
rotation_default = getattr(rotation_config, f'default_{field}', None)
if rotation_default is not None:
model_config[field] = rotation_default
logger.debug(f"Applied rotation default_{field}: {rotation_default} to model {model_id}")
else:
# Auto-derive from first provider in rotation, then first model
if rotation_config.providers and len(rotation_config.providers) > 0:
first_provider = rotation_config.providers[0]
provider_id = first_provider.get('provider_id')
provider_config = self.config.get_provider(provider_id)
if provider_config and provider_config.models and len(provider_config.models) > 0:
first_model = provider_config.models[0]
# Check for context_size, context_window, or context_length
if field == 'context_size':
model_field = getattr(first_model, 'context_size', None)
if model_field is None:
model_field = getattr(first_model, 'context_window', None)
if model_field is None:
model_field = getattr(first_model, 'context_length', None)
else:
model_field = getattr(first_model, field, None)
if model_field is not None:
model_config[field] = model_field
logger.debug(f"Auto-derived default_{field}: {model_field} from first model in {provider_id}")
else:
# Not a rotation, apply autoselect defaults directly
for field in default_fields:
if field not in model_config or model_config.get(field) is None:
autoselect_default = getattr(autoselect_config, f'default_{field}', None)
if autoselect_default is not None:
model_config[field] = autoselect_default
return model_config
async def _handle_chunked_rotation_request( async def _handle_chunked_rotation_request(
self, self,
handler, handler,
...@@ -2994,16 +3094,31 @@ class RotationHandler: ...@@ -2994,16 +3094,31 @@ class RotationHandler:
} }
# Add context window information # Add context window information
# Priority: model config in rotation > provider config > first model in provider
if model.get('context_size'): if model.get('context_size'):
model_dict['context_window'] = model['context_size'] model_dict['context_window'] = model['context_size']
elif provider_config: elif provider_config:
# Try to find in provider config # Try to find in provider config
found_in_provider = False
for pm in provider_config.models or []: for pm in provider_config.models or []:
if pm.name == model_name and hasattr(pm, 'context_size'): if pm.name == model_name and hasattr(pm, 'context_size') and pm.context_size:
model_dict['context_window'] = pm.context_size model_dict['context_window'] = pm.context_size
found_in_provider = True
break break
if 'context_window' not in model_dict: if not found_in_provider:
model_dict['context_window'] = self._infer_context_window(model_name, provider_config.type) # Auto-derive from first model in provider (which has context_size from dynamic fetch)
if provider_config.models and len(provider_config.models) > 0:
first_model = provider_config.models[0]
if hasattr(first_model, 'context_size') and first_model.context_size:
model_dict['context_window'] = first_model.context_size
elif hasattr(first_model, 'context_window') and first_model.context_window:
model_dict['context_window'] = first_model.context_window
elif hasattr(first_model, 'context_length') and first_model.context_length:
model_dict['context_window'] = first_model.context_length
else:
model_dict['context_window'] = self._infer_context_window(model_name, provider_config.type)
else:
model_dict['context_window'] = self._infer_context_window(model_name, provider_config.type)
# Add capabilities information # Add capabilities information
if model.get('capabilities'): if model.get('capabilities'):
......
...@@ -141,12 +141,13 @@ class MCPServer: ...@@ -141,12 +141,13 @@ class MCPServer:
def __init__(self): def __init__(self):
self.config = config self.config = config
def get_available_tools(self, auth_level: int) -> List[Dict]: def get_available_tools(self, auth_level: int, user_id: Optional[int] = None) -> List[Dict]:
""" """
Get list of available MCP tools based on auth level. Get list of available MCP tools based on auth level and user.
Args: Args:
auth_level: The authentication level (MCPAuthLevel) auth_level: The authentication level (MCPAuthLevel)
user_id: Optional user ID to include user-specific tools
Returns: Returns:
List of tool definitions List of tool definitions
...@@ -217,6 +218,223 @@ class MCPServer: ...@@ -217,6 +218,223 @@ class MCPServer:
} }
]) ])
# Add user-specific tools if user_id is provided
if user_id:
tools.extend([
# User models
{
"name": "list_user_models",
"description": "List all models from user's own providers, rotations, and autoselects",
"inputSchema": {
"type": "object",
"properties": {},
"required": []
}
},
# User providers
{
"name": "list_user_providers",
"description": "List all user-configured providers",
"inputSchema": {
"type": "object",
"properties": {},
"required": []
}
},
{
"name": "get_user_provider",
"description": "Get a specific user provider configuration",
"inputSchema": {
"type": "object",
"properties": {
"provider_id": {
"type": "string",
"description": "Provider ID to get"
}
},
"required": ["provider_id"]
}
},
{
"name": "set_user_provider",
"description": "Save a user provider configuration",
"inputSchema": {
"type": "object",
"properties": {
"provider_id": {
"type": "string",
"description": "Provider ID to save"
},
"provider_data": {
"type": "object",
"description": "Provider configuration data"
}
},
"required": ["provider_id", "provider_data"]
}
},
{
"name": "delete_user_provider",
"description": "Delete a user provider configuration",
"inputSchema": {
"type": "object",
"properties": {
"provider_id": {
"type": "string",
"description": "Provider ID to delete"
}
},
"required": ["provider_id"]
}
},
# User rotations
{
"name": "list_user_rotations",
"description": "List all user-configured rotations",
"inputSchema": {
"type": "object",
"properties": {},
"required": []
}
},
{
"name": "get_user_rotation",
"description": "Get a specific user rotation configuration",
"inputSchema": {
"type": "object",
"properties": {
"rotation_id": {
"type": "string",
"description": "Rotation ID to get"
}
},
"required": ["rotation_id"]
}
},
{
"name": "set_user_rotation",
"description": "Save a user rotation configuration",
"inputSchema": {
"type": "object",
"properties": {
"rotation_id": {
"type": "string",
"description": "Rotation ID to save"
},
"rotation_data": {
"type": "object",
"description": "Rotation configuration data"
}
},
"required": ["rotation_id", "rotation_data"]
}
},
{
"name": "delete_user_rotation",
"description": "Delete a user rotation configuration",
"inputSchema": {
"type": "object",
"properties": {
"rotation_id": {
"type": "string",
"description": "Rotation ID to delete"
}
},
"required": ["rotation_id"]
}
},
# User autoselects
{
"name": "list_user_autoselects",
"description": "List all user-configured autoselects",
"inputSchema": {
"type": "object",
"properties": {},
"required": []
}
},
{
"name": "get_user_autoselect",
"description": "Get a specific user autoselect configuration",
"inputSchema": {
"type": "object",
"properties": {
"autoselect_id": {
"type": "string",
"description": "Autoselect ID to get"
}
},
"required": ["autoselect_id"]
}
},
{
"name": "set_user_autoselect",
"description": "Save a user autoselect configuration",
"inputSchema": {
"type": "object",
"properties": {
"autoselect_id": {
"type": "string",
"description": "Autoselect ID to save"
},
"autoselect_data": {
"type": "object",
"description": "Autoselect configuration data"
}
},
"required": ["autoselect_id", "autoselect_data"]
}
},
{
"name": "delete_user_autoselect",
"description": "Delete a user autoselect configuration",
"inputSchema": {
"type": "object",
"properties": {
"autoselect_id": {
"type": "string",
"description": "Autoselect ID to delete"
}
},
"required": ["autoselect_id"]
}
},
# User chat completion
{
"name": "user_chat_completion",
"description": "Send a chat completion request using user's own configurations",
"inputSchema": {
"type": "object",
"properties": {
"model": {
"type": "string",
"description": "User model identifier (e.g., 'user-provider/myprovider/mymodel', 'user-rotation/myrotation', 'user-autoselect/myautoselect')"
},
"messages": {
"type": "array",
"description": "List of message objects with role and content"
},
"temperature": {
"type": "number",
"description": "Sampling temperature (0-2)",
"default": 1.0
},
"max_tokens": {
"type": "integer",
"description": "Maximum tokens to generate",
"default": 2048
},
"stream": {
"type": "boolean",
"description": "Enable streaming response",
"default": False
}
},
"required": ["model", "messages"]
}
}
])
# Tools available to AUTOSELECT level and above # Tools available to AUTOSELECT level and above
if auth_level >= MCPAuthLevel.AUTOSELECT: if auth_level >= MCPAuthLevel.AUTOSELECT:
tools.extend([ tools.extend([
...@@ -435,6 +653,7 @@ class MCPServer: ...@@ -435,6 +653,7 @@ class MCPServer:
tool_name: Name of the tool to call tool_name: Name of the tool to call
arguments: Tool arguments arguments: Tool arguments
auth_level: Authentication level auth_level: Authentication level
user_id: Optional user ID for user-specific operations
Returns: Returns:
Tool result Tool result
...@@ -472,6 +691,30 @@ class MCPServer: ...@@ -472,6 +691,30 @@ class MCPServer:
'delete_provider_config': self._delete_provider_config, 'delete_provider_config': self._delete_provider_config,
}) })
# Add user-specific tools (available to all authenticated users with user_id)
if user_id:
handlers.update({
# User models
'list_user_models': self._list_user_models,
# User providers
'list_user_providers': self._list_user_providers,
'get_user_provider': self._get_user_provider,
'set_user_provider': self._set_user_provider,
'delete_user_provider': self._delete_user_provider,
# User rotations
'list_user_rotations': self._list_user_rotations,
'get_user_rotation': self._get_user_rotation,
'set_user_rotation': self._set_user_rotation,
'delete_user_rotation': self._delete_user_rotation,
# User autoselects
'list_user_autoselects': self._list_user_autoselects,
'get_user_autoselect': self._get_user_autoselect,
'set_user_autoselect': self._set_user_autoselect,
'delete_user_autoselect': self._delete_user_autoselect,
# User chat completion
'user_chat_completion': self._user_chat_completion,
})
if tool_name not in handlers: if tool_name not in handlers:
raise HTTPException(status_code=404, detail=f"Tool '{tool_name}' not found") raise HTTPException(status_code=404, detail=f"Tool '{tool_name}' not found")
...@@ -990,5 +1233,380 @@ class MCPServer: ...@@ -990,5 +1233,380 @@ class MCPServer:
return {"status": "success", "message": f"Provider '{provider_id}' deleted. Restart server for changes to take effect."} return {"status": "success", "message": f"Provider '{provider_id}' deleted. Restart server for changes to take effect."}
# ===== User-specific MCP tools =====
async def _list_user_models(self, args: Dict, user_id: Optional[int] = None) -> Dict:
"""List all models from user's own providers, rotations, and autoselects"""
if not user_id:
raise HTTPException(status_code=401, detail="User authentication required")
from .database import get_database
db = get_database()
all_models = []
# Get user providers
user_providers = db.get_user_providers(user_id)
for provider in user_providers:
provider_id = provider['provider_id']
provider_config = provider['config']
models = provider_config.get('models', [])
for model in models:
all_models.append({
'id': f"user-provider/{provider_id}/{model.get('name', '')}",
'object': 'model',
'created': int(time.time()),
'owned_by': provider_id,
'provider': provider_id,
'type': 'user_provider',
'model_name': model.get('name', ''),
'source': 'user_config'
})
# Get user rotations
user_rotations = db.get_user_rotations(user_id)
for rotation in user_rotations:
rotation_id = rotation['rotation_id']
rotation_config = rotation['config']
all_models.append({
'id': f"user-rotation/{rotation_id}",
'object': 'model',
'created': int(time.time()),
'owned_by': 'aisbf-user-rotation',
'type': 'user_rotation',
'rotation_id': rotation_id,
'model_name': rotation_config.get('model_name', rotation_id),
'source': 'user_config'
})
# Get user autoselects
user_autoselects = db.get_user_autoselects(user_id)
for autoselect in user_autoselects:
autoselect_id = autoselect['autoselect_id']
autoselect_config = autoselect['config']
all_models.append({
'id': f"user-autoselect/{autoselect_id}",
'object': 'model',
'created': int(time.time()),
'owned_by': 'aisbf-user-autoselect',
'type': 'user_autoselect',
'autoselect_id': autoselect_id,
'model_name': autoselect_config.get('model_name', autoselect_id),
'description': autoselect_config.get('description'),
'source': 'user_config'
})
return {"models": all_models}
async def _list_user_providers(self, args: Dict, user_id: Optional[int] = None) -> Dict:
"""List all user-configured providers"""
if not user_id:
raise HTTPException(status_code=401, detail="User authentication required")
from .database import get_database
db = get_database()
providers = db.get_user_providers(user_id)
providers_info = {}
for provider in providers:
provider_id = provider['provider_id']
provider_config = provider['config']
# Remove sensitive fields
safe_config = {k: v for k, v in provider_config.items()
if k not in ['api_key', 'password', 'secret', 'token']}
providers_info[provider_id] = {
'name': provider_config.get('name', provider_id),
'type': provider_config.get('type', 'unknown'),
'endpoint': provider_config.get('endpoint'),
'models_count': len(provider_config.get('models', [])),
'config': safe_config
}
return {"providers": providers_info}
async def _get_user_provider(self, args: Dict, user_id: Optional[int] = None) -> Dict:
"""Get a specific user provider configuration"""
if not user_id:
raise HTTPException(status_code=401, detail="User authentication required")
provider_id = args.get('provider_id')
if not provider_id:
raise HTTPException(status_code=400, detail="provider_id is required")
from .database import get_database
db = get_database()
provider = db.get_user_provider(user_id, provider_id)
if not provider:
raise HTTPException(status_code=404, detail=f"Provider '{provider_id}' not found")
# Remove sensitive fields
safe_config = {k: v for k, v in provider['config'].items()
if k not in ['api_key', 'password', 'secret', 'token']}
return {"provider_id": provider_id, "config": safe_config}
async def _set_user_provider(self, args: Dict, user_id: Optional[int] = None) -> Dict:
"""Save a user provider configuration"""
if not user_id:
raise HTTPException(status_code=401, detail="User authentication required")
provider_id = args.get('provider_id')
provider_data = args.get('provider_data')
if not provider_id or not provider_data:
raise HTTPException(status_code=400, detail="provider_id and provider_data are required")
from .database import get_database
db = get_database()
db.save_user_provider(user_id, provider_id, provider_data)
return {"status": "success", "message": f"Provider '{provider_id}' saved successfully."}
async def _delete_user_provider(self, args: Dict, user_id: Optional[int] = None) -> Dict:
"""Delete a user provider configuration"""
if not user_id:
raise HTTPException(status_code=401, detail="User authentication required")
provider_id = args.get('provider_id')
if not provider_id:
raise HTTPException(status_code=400, detail="provider_id is required")
from .database import get_database
db = get_database()
db.delete_user_provider(user_id, provider_id)
return {"status": "success", "message": f"Provider '{provider_id}' deleted successfully."}
async def _list_user_rotations(self, args: Dict, user_id: Optional[int] = None) -> Dict:
"""List all user-configured rotations"""
if not user_id:
raise HTTPException(status_code=401, detail="User authentication required")
from .database import get_database
db = get_database()
rotations = db.get_user_rotations(user_id)
rotations_info = {}
for rotation in rotations:
rotation_id = rotation['rotation_id']
rotation_config = rotation['config']
rotations_info[rotation_id] = {
"model_name": rotation_config.get('model_name', rotation_id),
"providers": rotation_config.get('providers', [])
}
return {"rotations": rotations_info}
async def _get_user_rotation(self, args: Dict, user_id: Optional[int] = None) -> Dict:
"""Get a specific user rotation configuration"""
if not user_id:
raise HTTPException(status_code=401, detail="User authentication required")
rotation_id = args.get('rotation_id')
if not rotation_id:
raise HTTPException(status_code=400, detail="rotation_id is required")
from .database import get_database
db = get_database()
rotation = db.get_user_rotation(user_id, rotation_id)
if not rotation:
raise HTTPException(status_code=404, detail=f"Rotation '{rotation_id}' not found")
return {"rotation_id": rotation_id, "config": rotation['config']}
async def _set_user_rotation(self, args: Dict, user_id: Optional[int] = None) -> Dict:
"""Save a user rotation configuration"""
if not user_id:
raise HTTPException(status_code=401, detail="User authentication required")
rotation_id = args.get('rotation_id')
rotation_data = args.get('rotation_data')
if not rotation_id or not rotation_data:
raise HTTPException(status_code=400, detail="rotation_id and rotation_data are required")
from .database import get_database
db = get_database()
db.save_user_rotation(user_id, rotation_id, rotation_data)
return {"status": "success", "message": f"Rotation '{rotation_id}' saved successfully."}
async def _delete_user_rotation(self, args: Dict, user_id: Optional[int] = None) -> Dict:
"""Delete a user rotation configuration"""
if not user_id:
raise HTTPException(status_code=401, detail="User authentication required")
rotation_id = args.get('rotation_id')
if not rotation_id:
raise HTTPException(status_code=400, detail="rotation_id is required")
from .database import get_database
db = get_database()
db.delete_user_rotation(user_id, rotation_id)
return {"status": "success", "message": f"Rotation '{rotation_id}' deleted successfully."}
async def _list_user_autoselects(self, args: Dict, user_id: Optional[int] = None) -> Dict:
"""List all user-configured autoselects"""
if not user_id:
raise HTTPException(status_code=401, detail="User authentication required")
from .database import get_database
db = get_database()
autoselects = db.get_user_autoselects(user_id)
autoselects_info = {}
for autoselect in autoselects:
autoselect_id = autoselect['autoselect_id']
autoselect_config = autoselect['config']
autoselects_info[autoselect_id] = {
"model_name": autoselect_config.get('model_name', autoselect_id),
"description": autoselect_config.get('description', ''),
"fallback": autoselect_config.get('fallback', ''),
"available_models": autoselect_config.get('available_models', [])
}
return {"autoselects": autoselects_info}
async def _get_user_autoselect(self, args: Dict, user_id: Optional[int] = None) -> Dict:
"""Get a specific user autoselect configuration"""
if not user_id:
raise HTTPException(status_code=401, detail="User authentication required")
autoselect_id = args.get('autoselect_id')
if not autoselect_id:
raise HTTPException(status_code=400, detail="autoselect_id is required")
from .database import get_database
db = get_database()
autoselect = db.get_user_autoselect(user_id, autoselect_id)
if not autoselect:
raise HTTPException(status_code=404, detail=f"Autoselect '{autoselect_id}' not found")
return {"autoselect_id": autoselect_id, "config": autoselect['config']}
async def _set_user_autoselect(self, args: Dict, user_id: Optional[int] = None) -> Dict:
"""Save a user autoselect configuration"""
if not user_id:
raise HTTPException(status_code=401, detail="User authentication required")
autoselect_id = args.get('autoselect_id')
autoselect_data = args.get('autoselect_data')
if not autoselect_id or not autoselect_data:
raise HTTPException(status_code=400, detail="autoselect_id and autoselect_data are required")
from .database import get_database
db = get_database()
db.save_user_autoselect(user_id, autoselect_id, autoselect_data)
return {"status": "success", "message": f"Autoselect '{autoselect_id}' saved successfully."}
async def _delete_user_autoselect(self, args: Dict, user_id: Optional[int] = None) -> Dict:
"""Delete a user autoselect configuration"""
if not user_id:
raise HTTPException(status_code=401, detail="User authentication required")
autoselect_id = args.get('autoselect_id')
if not autoselect_id:
raise HTTPException(status_code=400, detail="autoselect_id is required")
from .database import get_database
db = get_database()
db.delete_user_autoselect(user_id, autoselect_id)
return {"status": "success", "message": f"Autoselect '{autoselect_id}' deleted successfully."}
async def _user_chat_completion(self, args: Dict, user_id: Optional[int] = None) -> Dict:
"""Send a chat completion request using user's own configurations"""
if not user_id:
raise HTTPException(status_code=401, detail="User authentication required")
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)
if not model:
raise HTTPException(status_code=400, detail="model is required")
# Parse model format: user-provider/id/model, user-rotation/id, user-autoselect/id
if '/' in model:
parts = model.split('/', 2)
if len(parts) < 3:
raise HTTPException(status_code=400, detail="Invalid model format. Use 'user-provider/id/model', 'user-rotation/id', or 'user-autoselect/id'")
provider_type = parts[0]
config_id = parts[1]
actual_model = parts[2] if len(parts) > 2 else None
else:
raise HTTPException(status_code=400, detail="Invalid model format. Use 'user-provider/id/model', 'user-rotation/id', or 'user-autoselect/id'")
# Create request data
request_data = {
"model": actual_model or config_id,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
"stream": stream
}
# Route to appropriate handler
from starlette.requests import Request
from main import get_user_handler
scope = {
"type": "http",
"method": "POST",
"headers": [],
"query_string": b"",
"path": "/api/user/chat/completions"
}
dummy_request = Request(scope)
if provider_type == "user-autoselect":
handler = get_user_handler('autoselect', user_id)
if config_id not in handler.user_autoselects:
raise HTTPException(status_code=400, detail=f"User autoselect '{config_id}' not found")
if stream:
return {"error": "Streaming not supported in MCP, use SSE endpoint instead"}
return await handler.handle_autoselect_request(config_id, request_data)
elif provider_type == "user-rotation":
handler = get_user_handler('rotation', user_id)
if config_id not in handler.user_rotations:
raise HTTPException(status_code=400, detail=f"User rotation '{config_id}' not found")
return await handler.handle_rotation_request(config_id, request_data)
elif provider_type == "user-provider":
handler = get_user_handler('request', user_id)
if config_id not in handler.user_providers:
raise HTTPException(status_code=400, detail=f"User provider '{config_id}' not found")
provider_config = handler.user_providers[config_id]
if not validate_kiro_credentials(config_id, provider_config):
raise HTTPException(status_code=403, detail=f"Provider '{config_id}' credentials not available")
if stream:
return {"error": "Streaming not supported in MCP, use SSE endpoint instead"}
return await handler.handle_chat_completion(dummy_request, config_id, request_data)
raise HTTPException(status_code=400, detail="Invalid model type. Use 'user-provider', 'user-rotation', or 'user-autoselect'")
# Global MCP server instance # Global MCP server instance
mcp_server = MCPServer() mcp_server = MCPServer()
...@@ -1034,6 +1034,7 @@ async def auth_middleware(request: Request, call_next): ...@@ -1034,6 +1034,7 @@ async def auth_middleware(request: Request, call_next):
request.state.user_id = None request.state.user_id = None
request.state.token_id = None request.state.token_id = None
request.state.is_global_token = True request.state.is_global_token = True
request.state.is_admin = True # Global tokens have admin access
else: else:
# Check user API tokens # Check user API tokens
from aisbf.database import get_database from aisbf.database import get_database
...@@ -1045,6 +1046,8 @@ async def auth_middleware(request: Request, call_next): ...@@ -1045,6 +1046,8 @@ async def auth_middleware(request: Request, call_next):
request.state.user_id = user_auth['user_id'] request.state.user_id = user_auth['user_id']
request.state.token_id = user_auth['token_id'] request.state.token_id = user_auth['token_id']
request.state.is_global_token = False request.state.is_global_token = False
# Store user role - admin users get full access
request.state.is_admin = (user_auth.get('role') == 'admin')
# Record token usage for analytics # Record token usage for analytics
# We'll do this asynchronously to avoid blocking the request # We'll do this asynchronously to avoid blocking the request
...@@ -4402,7 +4405,8 @@ async def mcp_sse(request: Request): ...@@ -4402,7 +4405,8 @@ async def mcp_sse(request: Request):
elif method == 'tools/list': elif method == 'tools/list':
# Return available tools # Return available tools
tools = mcp_server.get_available_tools(auth_level) user_id = getattr(request.state, 'user_id', None)
tools = mcp_server.get_available_tools(auth_level, user_id)
response = { response = {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": request_id, "id": request_id,
...@@ -4526,7 +4530,8 @@ async def mcp_post(request: Request): ...@@ -4526,7 +4530,8 @@ async def mcp_post(request: Request):
elif method == 'tools/list': elif method == 'tools/list':
# Return available tools # Return available tools
tools = mcp_server.get_available_tools(auth_level) user_id = getattr(request.state, 'user_id', None)
tools = mcp_server.get_available_tools(auth_level, user_id)
return { return {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": request_id, "id": request_id,
...@@ -4593,7 +4598,8 @@ async def mcp_list_tools(request: Request): ...@@ -4593,7 +4598,8 @@ async def mcp_list_tools(request: Request):
content={"error": "Invalid or missing MCP authentication token"} content={"error": "Invalid or missing MCP authentication token"}
) )
tools = mcp_server.get_available_tools(auth_level) user_id = getattr(request.state, 'user_id', None)
tools = mcp_server.get_available_tools(auth_level, user_id)
return {"tools": tools} return {"tools": tools}
...@@ -4646,6 +4652,693 @@ async def mcp_call_tool(request: Request): ...@@ -4646,6 +4652,693 @@ async def mcp_call_tool(request: Request):
) )
# User-specific API endpoints
# These endpoints allow authenticated users to access their own configurations
# Admin users can also access other users' configurations
@app.get("/api/user/models")
async def user_list_models(request: Request):
"""
List all available models for the authenticated user.
This includes the user's own providers, rotations, and autoselects.
Admin users can also access all users' configurations.
Authentication is done via Bearer token in the Authorization header.
Returns models from:
- User-configured providers
- User-configured rotations
- User-configured autoselects
Example:
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/models
"""
user_id = getattr(request.state, 'user_id', None)
is_admin = getattr(request.state, 'is_admin', False)
is_global_token = getattr(request.state, 'is_global_token', False)
# Global tokens and admin users can access all configurations
if is_global_token or is_admin:
# Return global config models plus user-specific models
all_models = []
# Add global provider models
for provider_id, provider_config in config.providers.items():
try:
provider_models = await get_provider_models(provider_id, provider_config)
all_models.extend(provider_models)
except Exception as e:
logger.warning(f"Error listing models for provider {provider_id}: {e}")
# Add global rotations
for rotation_id, rotation_config in config.rotations.items():
try:
all_models.append({
'id': f"rotation/{rotation_id}",
'object': 'model',
'created': int(time.time()),
'owned_by': 'aisbf-rotation',
'type': 'rotation',
'rotation_id': rotation_id,
'model_name': rotation_config.model_name,
'source': 'global'
})
except Exception as e:
logger.warning(f"Error listing rotation {rotation_id}: {e}")
# Add global autoselects
for autoselect_id, autoselect_config in config.autoselect.items():
try:
all_models.append({
'id': f"autoselect/{autoselect_id}",
'object': 'model',
'created': int(time.time()),
'owned_by': 'aisbf-autoselect',
'type': 'autoselect',
'autoselect_id': autoselect_id,
'model_name': autoselect_config.model_name,
'description': autoselect_config.description,
'source': 'global'
})
except Exception as e:
logger.warning(f"Error listing autoselect {autoselect_id}: {e}")
# If not global token, also add user-specific models
if user_id and not is_global_token:
handler = get_user_handler('request', user_id)
for provider_id, provider_config in handler.user_providers.items():
try:
if hasattr(provider_config, 'models') and provider_config.models:
for model in provider_config.models:
model_id = f"{provider_id}/{model.name}"
all_models.append({
'id': model_id,
'object': 'model',
'created': int(time.time()),
'owned_by': provider_id,
'provider': provider_id,
'type': 'user_provider',
'model_name': model.name,
'source': 'user_config'
})
except Exception as e:
logger.warning(f"Error listing models for user provider {provider_id}: {e}")
rotation_handler = get_user_handler('rotation', user_id)
for rotation_id, rotation_config in rotation_handler.user_rotations.items():
try:
all_models.append({
'id': f"user-rotation/{rotation_id}",
'object': 'model',
'created': int(time.time()),
'owned_by': 'aisbf-user-rotation',
'type': 'user_rotation',
'rotation_id': rotation_id,
'source': 'user_config'
})
except Exception as e:
logger.warning(f"Error listing user rotation {rotation_id}: {e}")
autoselect_handler = get_user_handler('autoselect', user_id)
for autoselect_id, autoselect_config in autoselect_handler.user_autoselects.items():
try:
all_models.append({
'id': f"user-autoselect/{autoselect_id}",
'object': 'model',
'created': int(time.time()),
'owned_by': 'aisbf-user-autoselect',
'type': 'user_autoselect',
'autoselect_id': autoselect_id,
'source': 'user_config'
})
except Exception as e:
logger.warning(f"Error listing user autoselect {autoselect_id}: {e}")
return {"object": "list", "data": all_models}
# Regular user - only their own configurations
if not user_id:
return JSONResponse(
status_code=401,
content={"error": "Authentication required. Use a valid API token."}
)
all_models = []
# Get user-specific handler for providers
handler = get_user_handler('request', user_id)
# Add user providers
for provider_id, provider_config in handler.user_providers.items():
try:
if hasattr(provider_config, 'models') and provider_config.models:
for model in provider_config.models:
model_id = f"{provider_id}/{model.name}"
all_models.append({
'id': model_id,
'object': 'model',
'created': int(time.time()),
'owned_by': provider_id,
'provider': provider_id,
'type': 'user_provider',
'model_name': model.name,
'context_size': getattr(model, 'context_size', None),
'capabilities': getattr(model, 'capabilities', []),
'description': getattr(model, 'description', None),
'source': 'user_config'
})
except Exception as e:
logger.warning(f"Error listing models for user provider {provider_id}: {e}")
# Add user rotations
rotation_handler = get_user_handler('rotation', user_id)
for rotation_id, rotation_config in rotation_handler.user_rotations.items():
try:
all_models.append({
'id': f"user-rotation/{rotation_id}",
'object': 'model',
'created': int(time.time()),
'owned_by': 'aisbf-user-rotation',
'type': 'user_rotation',
'rotation_id': rotation_id,
'model_name': rotation_config.get('model_name', rotation_id),
'capabilities': rotation_config.get('capabilities', []),
'source': 'user_config'
})
except Exception as e:
logger.warning(f"Error listing user rotation {rotation_id}: {e}")
# Add user autoselects
autoselect_handler = get_user_handler('autoselect', user_id)
for autoselect_id, autoselect_config in autoselect_handler.user_autoselects.items():
try:
all_models.append({
'id': f"user-autoselect/{autoselect_id}",
'object': 'model',
'created': int(time.time()),
'owned_by': 'aisbf-user-autoselect',
'type': 'user_autoselect',
'autoselect_id': autoselect_id,
'model_name': autoselect_config.get('model_name', autoselect_id),
'description': autoselect_config.get('description'),
'capabilities': autoselect_config.get('capabilities', []),
'source': 'user_config'
})
except Exception as e:
logger.warning(f"Error listing user autoselect {autoselect_id}: {e}")
return {"object": "list", "data": all_models}
@app.get("/api/user/providers")
async def user_list_providers(request: Request):
"""
List all provider configurations for the authenticated user.
Admin users and global tokens can access all configurations.
Authentication is done via Bearer token in the Authorization header.
Example:
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/providers
"""
user_id = getattr(request.state, 'user_id', None)
is_admin = getattr(request.state, 'is_admin', False)
is_global_token = getattr(request.state, 'is_global_token', False)
# Global tokens and admin users can access all configurations
if is_global_token or is_admin:
# Return global providers plus user-specific if user_id exists
providers_info = {}
# Add global providers
for provider_id, provider_config in config.providers.items():
try:
if hasattr(provider_config, 'model_dump'):
config_dict = provider_config.model_dump()
elif hasattr(provider_config, '__dict__'):
config_dict = vars(provider_config)
else:
config_dict = {}
safe_config = {k: v for k, v in config_dict.items()
if k not in ['api_key', 'password', 'secret', 'token']}
providers_info[provider_id] = {
'name': getattr(provider_config, 'name', provider_id),
'type': getattr(provider_config, 'type', 'unknown'),
'endpoint': getattr(provider_config, 'endpoint', None),
'models_count': len(getattr(provider_config, 'models', [])),
'config': safe_config,
'source': 'global'
}
except Exception as e:
logger.warning(f"Error listing global provider {provider_id}: {e}")
# If not global token, also add user-specific providers
if user_id and not is_global_token:
handler = get_user_handler('request', user_id)
for provider_id, provider_config in handler.user_providers.items():
try:
if hasattr(provider_config, 'model_dump'):
config_dict = provider_config.model_dump()
elif hasattr(provider_config, '__dict__'):
config_dict = vars(provider_config)
else:
config_dict = {}
safe_config = {k: v for k, v in config_dict.items()
if k not in ['api_key', 'password', 'secret', 'token']}
providers_info[provider_id] = {
'name': getattr(provider_config, 'name', provider_id),
'type': getattr(provider_config, 'type', 'unknown'),
'endpoint': getattr(provider_config, 'endpoint', None),
'models_count': len(getattr(provider_config, 'models', [])),
'config': safe_config,
'source': 'user_config'
}
except Exception as e:
logger.warning(f"Error listing user provider {provider_id}: {e}")
return {"providers": providers_info}
# Regular user - only their own configurations
if not user_id:
return JSONResponse(
status_code=401,
content={"error": "Authentication required. Use a valid API token."}
)
handler = get_user_handler('request', user_id)
providers_info = {}
for provider_id, provider_config in handler.user_providers.items():
try:
# Convert provider config to dict (excluding sensitive info)
if hasattr(provider_config, 'model_dump'):
config_dict = provider_config.model_dump()
elif hasattr(provider_config, '__dict__'):
config_dict = vars(provider_config)
else:
config_dict = {}
# Remove sensitive fields for display
safe_config = {k: v for k, v in config_dict.items()
if k not in ['api_key', 'password', 'secret', 'token']}
providers_info[provider_id] = {
'name': getattr(provider_config, 'name', provider_id),
'type': getattr(provider_config, 'type', 'unknown'),
'endpoint': getattr(provider_config, 'endpoint', None),
'models_count': len(getattr(provider_config, 'models', [])),
'config': safe_config
}
except Exception as e:
logger.warning(f"Error listing user provider {provider_id}: {e}")
return {"providers": providers_info}
@app.get("/api/user/rotations")
async def user_list_rotations(request: Request):
"""
List all rotation configurations for the authenticated user.
Admin users and global tokens can access all configurations.
Authentication is done via Bearer token in the Authorization header.
Example:
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/rotations
"""
user_id = getattr(request.state, 'user_id', None)
is_admin = getattr(request.state, 'is_admin', False)
is_global_token = getattr(request.state, 'is_global_token', False)
# Global tokens and admin users can access all configurations
if is_global_token or is_admin:
rotations_info = {}
# Add global rotations
for rotation_id, rotation_config in config.rotations.items():
try:
rotations_info[rotation_id] = {
"model_name": rotation_config.model_name,
"providers": rotation_config.providers,
"source": "global"
}
except Exception as e:
logger.warning(f"Error listing global rotation {rotation_id}: {e}")
# If not global token, also add user-specific rotations
if user_id and not is_global_token:
handler = get_user_handler('rotation', user_id)
for rotation_id, rotation_config in handler.user_rotations.items():
try:
rotations_info[rotation_id] = {
"model_name": rotation_config.get('model_name', rotation_id),
"providers": rotation_config.get('providers', []),
"source": "user_config"
}
except Exception as e:
logger.warning(f"Error listing user rotation {rotation_id}: {e}")
return {"rotations": rotations_info}
# Regular user - only their own configurations
if not user_id:
return JSONResponse(
status_code=401,
content={"error": "Authentication required. Use a valid API token."}
)
handler = get_user_handler('rotation', user_id)
rotations_info = {}
for rotation_id, rotation_config in handler.user_rotations.items():
try:
rotations_info[rotation_id] = {
"model_name": rotation_config.get('model_name', rotation_id),
"providers": rotation_config.get('providers', [])
}
except Exception as e:
logger.warning(f"Error listing user rotation {rotation_id}: {e}")
return {"rotations": rotations_info}
@app.get("/api/user/autoselects")
async def user_list_autoselects(request: Request):
"""
List all autoselect configurations for the authenticated user.
Admin users and global tokens can access all configurations.
Authentication is done via Bearer token in the Authorization header.
Example:
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/autoselects
"""
user_id = getattr(request.state, 'user_id', None)
is_admin = getattr(request.state, 'is_admin', False)
is_global_token = getattr(request.state, 'is_global_token', False)
# Global tokens and admin users can access all configurations
if is_global_token or is_admin:
autoselects_info = {}
# Add global autoselects
for autoselect_id, autoselect_config in config.autoselect.items():
try:
autoselects_info[autoselect_id] = {
"model_name": autoselect_config.model_name,
"description": autoselect_config.description,
"fallback": autoselect_config.fallback,
"available_models": [
{"model_id": m.model_id, "description": m.description}
for m in autoselect_config.available_models
],
"source": "global"
}
except Exception as e:
logger.warning(f"Error listing global autoselect {autoselect_id}: {e}")
# If not global token, also add user-specific autoselects
if user_id and not is_global_token:
handler = get_user_handler('autoselect', user_id)
for autoselect_id, autoselect_config in handler.user_autoselects.items():
try:
autoselects_info[autoselect_id] = {
"model_name": autoselect_config.get('model_name', autoselect_id),
"description": autoselect_config.get('description', ''),
"fallback": autoselect_config.get('fallback', ''),
"available_models": autoselect_config.get('available_models', []),
"source": "user_config"
}
except Exception as e:
logger.warning(f"Error listing user autoselect {autoselect_id}: {e}")
return {"autoselects": autoselects_info}
# Regular user - only their own configurations
if not user_id:
return JSONResponse(
status_code=401,
content={"error": "Authentication required. Use a valid API token."}
)
handler = get_user_handler('autoselect', user_id)
autoselects_info = {}
for autoselect_id, autoselect_config in handler.user_autoselects.items():
try:
autoselects_info[autoselect_id] = {
"model_name": autoselect_config.get('model_name', autoselect_id),
"description": autoselect_config.get('description', ''),
"fallback": autoselect_config.get('fallback', ''),
"available_models": autoselect_config.get('available_models', [])
}
except Exception as e:
logger.warning(f"Error listing user autoselect {autoselect_id}: {e}")
return {"autoselects": autoselects_info}
# User-specific API chat completion endpoints
# These endpoints allow authenticated users to use their own configurations
# Admin users and global tokens can also access global configurations
@app.post("/api/user/chat/completions")
async def user_chat_completions(request: Request, body: ChatCompletionRequest):
"""
Handle chat completions using the authenticated user's configurations.
Admin users and global tokens can also use global configurations.
Users can use their own providers, rotations, and autoselects.
Authentication is done via Bearer token in the Authorization header.
Model format:
- 'provider/model' - global provider (admin only)
- 'rotation/name' - global rotation (admin only)
- 'autoselect/name' - global autoselect (admin only)
- 'user-provider/model' - user's provider
- 'user-rotation/name' - user's rotation
- 'user-autoselect/name' - user's autoselect
Example:
curl -X POST -H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"model": "user-rotation/myrotation", "messages": [{"role": "user", "content": "Hello"}]}' \
http://localhost:17765/api/user/chat/completions
"""
user_id = getattr(request.state, 'user_id', None)
is_admin = getattr(request.state, 'is_admin', False)
is_global_token = getattr(request.state, 'is_global_token', False)
# Parse provider from model field
provider_id, actual_model = parse_provider_from_model(body.model)
if not provider_id:
raise HTTPException(
status_code=400,
detail="Model must be in format 'provider/model', 'rotation/name', 'autoselect/name', 'user-provider/model', 'user-rotation/name', or 'user-autoselect/name'"
)
body_dict = body.model_dump()
# Handle user autoselect (format: user-autoselect/{name})
if provider_id == "user-autoselect":
handler = get_user_handler('autoselect', user_id)
if actual_model not in handler.user_autoselects:
raise HTTPException(
status_code=400,
detail=f"User autoselect '{actual_model}' not found. Available: {list(handler.user_autoselects.keys())}"
)
body_dict['model'] = actual_model
if body.stream:
return await handler.handle_autoselect_streaming_request(actual_model, body_dict)
else:
return await handler.handle_autoselect_request(actual_model, body_dict)
# Handle user rotation (format: user-rotation/{name})
if provider_id == "user-rotation":
handler = get_user_handler('rotation', user_id)
if actual_model not in handler.user_rotations:
raise HTTPException(
status_code=400,
detail=f"User rotation '{actual_model}' not found. Available: {list(handler.user_rotations.keys())}"
)
body_dict['model'] = actual_model
return await handler.handle_rotation_request(actual_model, body_dict)
# Handle user provider (format: user-provider/{name})
if provider_id == "user-provider":
handler = get_user_handler('request', user_id)
if actual_model not in handler.user_providers:
raise HTTPException(
status_code=400,
detail=f"User provider '{actual_model}' not found. Available: {list(handler.user_providers.keys())}"
)
provider_config = handler.user_providers[actual_model]
# Validate kiro credentials
if not validate_kiro_credentials(actual_model, provider_config):
raise HTTPException(
status_code=403,
detail=f"Provider '{actual_model}' credentials not available."
)
body_dict['model'] = actual_model
if body.stream:
return await handler.handle_streaming_chat_completion(request, actual_model, body_dict)
else:
return await handler.handle_chat_completion(request, actual_model, body_dict)
# Check for global configurations (admin/global token only)
if is_global_token or is_admin:
# Handle global autoselect
if provider_id == "autoselect":
if actual_model not in config.autoselect:
raise HTTPException(
status_code=400,
detail=f"Autoselect '{actual_model}' not found. Available: {list(config.autoselect.keys())}"
)
handler = get_user_handler('autoselect', None)
body_dict['model'] = actual_model
if body.stream:
return await handler.handle_autoselect_streaming_request(actual_model, body_dict)
else:
return await handler.handle_autoselect_request(actual_model, body_dict)
# Handle global rotation
if provider_id == "rotation":
if actual_model not in config.rotations:
raise HTTPException(
status_code=400,
detail=f"Rotation '{actual_model}' not found. Available: {list(config.rotations.keys())}"
)
handler = get_user_handler('rotation', None)
body_dict['model'] = actual_model
return await handler.handle_rotation_request(actual_model, body_dict)
# Handle global provider
if provider_id in config.providers:
provider_config = config.get_provider(provider_id)
# Validate kiro credentials
if not validate_kiro_credentials(provider_id, provider_config):
raise HTTPException(
status_code=403,
detail=f"Provider '{provider_id}' credentials not available."
)
body_dict['model'] = actual_model
handler = get_user_handler('request', None)
if body.stream:
return await handler.handle_streaming_chat_completion(request, provider_id, body_dict)
else:
return await handler.handle_chat_completion(request, provider_id, body_dict)
raise HTTPException(
status_code=400,
detail="Model must be in format 'user-provider/model', 'user-rotation/name', or 'user-autoselect/name'. Global configurations are only available to admin users."
)
# User-specific model listing endpoint
@app.get("/api/user/{config_type}/models")
async def user_list_config_models(request: Request, config_type: str):
"""
List models for a specific user configuration type.
Args:
config_type: One of 'providers', 'rotations', or 'autoselects'
Authentication is done via Bearer token in the Authorization header.
Example:
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/rotations/models
"""
user_id = getattr(request.state, 'user_id', None)
if not user_id:
return JSONResponse(
status_code=401,
content={"error": "Authentication required. Use a valid API token."}
)
all_models = []
if config_type == "providers":
handler = get_user_handler('request', user_id)
for provider_id, provider_config in handler.user_providers.items():
try:
if hasattr(provider_config, 'models') and provider_config.models:
for model in provider_config.models:
all_models.append({
"id": f"user-provider/{provider_id}/{model.name}",
"name": model.name,
"object": "model",
"created": int(time.time()),
"owned_by": provider_id,
"provider_id": provider_id,
"type": "user_provider"
})
except Exception as e:
logger.warning(f"Error listing models for user provider {provider_id}: {e}")
elif config_type == "rotations":
handler = get_user_handler('rotation', user_id)
for rotation_id, rotation_config in handler.user_rotations.items():
try:
providers = rotation_config.get('providers', [])
for provider in providers:
for model in provider.get('models', []):
all_models.append({
"id": f"user-rotation/{rotation_id}/{model.get('name', '')}",
"name": rotation_id,
"object": "model",
"created": int(time.time()),
"owned_by": provider.get('provider_id', ''),
"rotation_id": rotation_id,
"actual_model": model.get('name', ''),
"provider_id": provider.get('provider_id', ''),
"weight": model.get('weight', 1),
"type": "user_rotation"
})
except Exception as e:
logger.warning(f"Error listing user rotation {rotation_id}: {e}")
elif config_type == "autoselects":
handler = get_user_handler('autoselect', user_id)
for autoselect_id, autoselect_config in handler.user_autoselects.items():
try:
for model_info in autoselect_config.get('available_models', []):
all_models.append({
"id": f"user-autoselect/{autoselect_id}/{model_info.get('model_id', '')}",
"name": autoselect_id,
"object": "model",
"created": int(time.time()),
"owned_by": "user-autoselect",
"autoselect_id": autoselect_id,
"description": model_info.get('description', ''),
"type": "user_autoselect"
})
except Exception as e:
logger.warning(f"Error listing user autoselect {autoselect_id}: {e}")
else:
raise HTTPException(
status_code=400,
detail=f"Invalid config type. Use 'providers', 'rotations', or 'autoselects'"
)
return {"data": all_models}
# Chrome extension download endpoint # Chrome extension download endpoint
@app.get("/dashboard/extension/download") @app.get("/dashboard/extension/download")
async def dashboard_extension_download(request: Request): async def dashboard_extension_download(request: Request):
......
...@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" ...@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "aisbf" name = "aisbf"
version = "0.9.0" version = "0.9.1"
description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations" description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations"
readme = "README.md" readme = "README.md"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
......
...@@ -49,7 +49,7 @@ class InstallCommand(_install): ...@@ -49,7 +49,7 @@ class InstallCommand(_install):
setup( setup(
name="aisbf", name="aisbf",
version="0.9.0", version="0.9.1",
author="AISBF Contributors", author="AISBF Contributors",
author_email="stefy@nexlab.net", author_email="stefy@nexlab.net",
description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations", description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations",
......
...@@ -197,6 +197,50 @@ function renderAutoselectDetails(autoselectKey) { ...@@ -197,6 +197,50 @@ function renderAutoselectDetails(autoselectKey) {
<small style="color: #a0a0a0; font-size: 12px; display: block; margin-top: 5px;">Choose from rotations or provider models</small> <small style="color: #a0a0a0; font-size: 12px; display: block; margin-top: 5px;">Choose from rotations or provider models</small>
</div> </div>
<h4 style="margin-top: 20px; margin-bottom: 10px;">Default Settings</h4>
<p style="color: #a0a0a0; font-size: 14px; margin-bottom: 10px;">Default values for models in this autoselect (optional - auto-derived from first model if not set)</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
<div class="form-group">
<label>Default Rate Limit (seconds)</label>
<input type="number" value="${autoselect.default_rate_limit || ''}" onchange="updateAutoselect('${autoselectKey}', 'default_rate_limit', this.value ? parseFloat(this.value) : null)" step="0.1" placeholder="Optional">
</div>
<div class="form-group">
<label>Default Max Request Tokens</label>
<input type="number" value="${autoselect.default_max_request_tokens || ''}" onchange="updateAutoselect('${autoselectKey}', 'default_max_request_tokens', this.value ? parseInt(this.value) : null)" placeholder="Optional">
</div>
<div class="form-group">
<label>Default Context Size</label>
<input type="number" value="${autoselect.default_context_size || ''}" onchange="updateAutoselect('${autoselectKey}', 'default_context_size', this.value ? parseInt(this.value) : null)" placeholder="Optional">
</div>
<div class="form-group">
<label>Default Rate Limit TPM</label>
<input type="number" value="${autoselect.default_rate_limit_TPM || ''}" onchange="updateAutoselect('${autoselectKey}', 'default_rate_limit_TPM', this.value ? parseInt(this.value) : null)" placeholder="Optional">
<small style="color: #a0a0a0; font-size: 12px; display: block; margin-top: 5px;">Tokens per minute limit</small>
</div>
<div class="form-group">
<label>Default Rate Limit TPH</label>
<input type="number" value="${autoselect.default_rate_limit_TPH || ''}" onchange="updateAutoselect('${autoselectKey}', 'default_rate_limit_TPH', this.value ? parseInt(this.value) : null)" placeholder="Optional">
<small style="color: #a0a0a0; font-size: 12px; display: block; margin-top: 5px;">Tokens per hour limit</small>
</div>
<div class="form-group">
<label>Default Rate Limit TPD</label>
<input type="number" value="${autoselect.default_rate_limit_TPD || ''}" onchange="updateAutoselect('${autoselectKey}', 'default_rate_limit_TPD', this.value ? parseInt(this.value) : null)" placeholder="Optional">
<small style="color: #a0a0a0; font-size: 12px; display: block; margin-top: 5px;">Tokens per day limit</small>
</div>
<div class="form-group">
<label>Default Condense Context</label>
<input type="number" value="${autoselect.default_condense_context || ''}" onchange="updateAutoselect('${autoselectKey}', 'default_condense_context', this.value ? parseInt(this.value) : null)" placeholder="Optional">
<small style="color: #a0a0a0; font-size: 12px; display: block; margin-top: 5px;">Trigger context condensation at this token count</small>
</div>
</div>
<h4 style="margin-top: 20px; margin-bottom: 10px;">Available Models</h4> <h4 style="margin-top: 20px; margin-bottom: 10px;">Available Models</h4>
<p style="color: #a0a0a0; font-size: 14px; margin-bottom: 10px;">Define which models can be selected and their descriptions for AI analysis</p> <p style="color: #a0a0a0; font-size: 14px; margin-bottom: 10px;">Define which models can be selected and their descriptions for AI analysis</p>
<div id="models-${autoselectKey}"></div> <div id="models-${autoselectKey}"></div>
......
...@@ -15,6 +15,83 @@ ...@@ -15,6 +15,83 @@
<div class="alert alert-error">{{ error }}</div> <div class="alert alert-error">{{ error }}</div>
{% endif %} {% endif %}
<!-- API Documentation Section -->
<div class="card">
<h2>🔌 Your API Endpoints</h2>
<p>Use your API token to access your personal configurations. Include the token in the Authorization header:</p>
<pre style="background: #1a1a2e; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>Authorization: Bearer YOUR_API_TOKEN</code></pre>
<div class="api-endpoints">
<h3>Your Models</h3>
<div class="endpoint">
<code class="method GET">GET</code>
<code class="url">/api/user/models</code>
<p>List all models from your providers, rotations, and autoselects</p>
</div>
<h3>Your Providers</h3>
<div class="endpoint">
<code class="method GET">GET</code>
<code class="url">/api/user/providers</code>
<p>List all your configured providers</p>
</div>
<h3>Your Rotations</h3>
<div class="endpoint">
<code class="method GET">GET</code>
<code class="url">/api/user/rotations</code>
<p>List all your configured rotations</p>
</div>
<h3>Your Autoselects</h3>
<div class="endpoint">
<code class="method GET">GET</code>
<code class="url">/api/user/autoselects</code>
<p>List all your configured autoselects</p>
</div>
<h3>Your Chat Completions</h3>
<div class="endpoint">
<code class="method POST">POST</code>
<code class="url">/api/user/chat/completions</code>
<p>Send chat requests using your configurations</p>
<p class="example">Example model formats:</p>
<ul>
<li><code>user-provider/myprovider/mymodel</code></li>
<li><code>user-rotation/myrotation</code></li>
<li><code>user-autoselect/myautoselect</code></li>
</ul>
</div>
<h3>MCP Tools</h3>
<div class="endpoint">
<code class="method GET">GET</code>
<code class="url">/mcp/tools</code>
<p>List available MCP tools for your configurations</p>
</div>
<div class="endpoint">
<code class="method POST">POST</code>
<code class="url">/mcp/tools/call</code>
<p>Call MCP tools to manage your configurations</p>
</div>
</div>
{% if session.role == 'admin' %}
<div class="admin-notice">
<h4>⚡ Admin Access</h4>
<p>As an admin user, you also have access to global configurations (providers, rotations, autoselects configured by the admin) in addition to your own user configurations.</p>
<p class="example">Admin model formats:</p>
<ul>
<li><code>provider/model</code> - global provider</li>
<li><code>rotation/myrotation</code> - global rotation</li>
<li><code>autoselect/myautoselect</code> - global autoselect</li>
</ul>
</div>
{% endif %}
<p class="note">⚠️ Note: Your API token is required for all these endpoints. Manage your tokens in the <a href="{{ url_for(request, '/dashboard/user/tokens') }}">API Tokens</a> section.</p>
</div>
<!-- Usage Statistics --> <!-- Usage Statistics -->
<div class="card"> <div class="card">
<h2>Usage Statistics</h2> <h2>Usage Statistics</h2>
...@@ -136,5 +213,116 @@ ...@@ -136,5 +213,116 @@
.table tbody tr:hover { .table tbody tr:hover {
background: #1a1a2e; background: #1a1a2e;
} }
/* API Endpoints Section */
.api-endpoints {
margin-top: 1rem;
}
.api-endpoints h3 {
margin-top: 1.5rem;
color: #e0e0e0;
font-size: 1.1rem;
}
.endpoint {
background: #0f3460;
padding: 1rem;
margin: 0.5rem 0;
border-radius: 4px;
border-left: 3px solid #667eea;
}
.endpoint .method {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 3px;
font-weight: bold;
font-size: 0.8rem;
margin-right: 0.5rem;
}
.endpoint .method.GET {
background: #28a745;
color: white;
}
.endpoint .method.POST {
background: #007bff;
color: white;
}
.endpoint .url {
color: #e0e0e0;
font-family: monospace;
}
.endpoint p {
margin: 0.5rem 0 0 0;
color: #a0a0a0;
font-size: 0.9rem;
}
.endpoint .example {
color: #667eea;
}
.endpoint ul {
margin: 0.5rem 0 0 0;
padding-left: 1.5rem;
color: #a0a0a0;
}
.endpoint code {
background: #1a1a2e;
padding: 0.2rem 0.4rem;
border-radius: 3px;
font-size: 0.9rem;
}
.note {
margin-top: 1rem;
padding: 1rem;
background: #1a1a2e;
border-radius: 4px;
color: #a0a0a0;
}
.note a {
color: #667eea;
}
.admin-notice {
margin-top: 1.5rem;
padding: 1rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
color: white;
}
.admin-notice h4 {
margin: 0 0 0.5rem 0;
color: white;
}
.admin-notice p {
margin: 0.5rem 0;
color: white;
}
.admin-notice .example {
color: #ffd700;
}
.admin-notice ul {
margin: 0.5rem 0 0 0;
padding-left: 1.5rem;
}
.admin-notice code {
background: rgba(0,0,0,0.3);
padding: 0.2rem 0.4rem;
border-radius: 3px;
}
</style> </style>
{% endblock %} {% endblock %}
...@@ -15,6 +15,74 @@ ...@@ -15,6 +15,74 @@
<div class="alert alert-error">{{ error }}</div> <div class="alert alert-error">{{ error }}</div>
{% endif %} {% endif %}
<!-- API Documentation -->
<div class="card">
<h2>🔌 How to Use Your Token</h2>
<p>Include your API token in the <code>Authorization</code> header when making requests:</p>
<pre style="background: #1a1a2e; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code>Authorization: Bearer YOUR_API_TOKEN</code></pre>
<h3>Available Endpoints:</h3>
<table class="endpoints-table">
<thead>
<tr>
<th>Method</th>
<th>Endpoint</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="method GET">GET</code></td>
<td><code>/api/user/models</code></td>
<td>List your models</td>
</tr>
<tr>
<td><code class="method GET">GET</code></td>
<td><code>/api/user/providers</code></td>
<td>List your providers</td>
</tr>
<tr>
<td><code class="method GET">GET</code></td>
<td><code>/api/user/rotations</code></td>
<td>List your rotations</td>
</tr>
<tr>
<td><code class="method GET">GET</code></td>
<td><code>/api/user/autoselects</code></td>
<td>List your autoselects</td>
</tr>
<tr>
<td><code class="method POST">POST</code></td>
<td><code>/api/user/chat/completions</code></td>
<td>Chat using your configs</td>
</tr>
<tr>
<td><code class="method GET">GET</code></td>
<td><code>/mcp/tools</code></td>
<td>List MCP tools</td>
</tr>
<tr>
<td><code class="method POST">POST</code></td>
<td><code>/mcp/tools/call</code></td>
<td>Call MCP tools</td>
</tr>
</tbody>
</table>
<h3>Example curl commands:</h3>
<pre style="background: #1a1a2e; padding: 1rem; border-radius: 4px; overflow-x: auto;"><code># List your models
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/models
# List your providers
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/providers
# Send a chat request
curl -X POST -H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"model": "user-rotation/myrotation", "messages": [{"role": "user", "content": "Hello"}]}' \
http://localhost:17765/api/user/chat/completions</code></pre>
</div>
<div class="card"> <div class="card">
<h2>API Tokens</h2> <h2>API Tokens</h2>
<div id="tokens-list"> <div id="tokens-list">
...@@ -356,5 +424,42 @@ renderTokens(); ...@@ -356,5 +424,42 @@ renderTokens();
font-family: monospace; font-family: monospace;
word-break: break-all; word-break: break-all;
} }
/* API Endpoints Table */
.endpoints-table {
width: 100%;
margin: 1rem 0;
border-collapse: collapse;
}
.endpoints-table th,
.endpoints-table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #333;
}
.endpoints-table th {
background: #1a1a2e;
font-weight: bold;
}
.endpoints-table .method {
display: inline-block;
padding: 0.2rem 0.5rem;
border-radius: 3px;
font-weight: bold;
font-size: 0.75rem;
}
.endpoints-table .method.GET {
background: #28a745;
color: white;
}
.endpoints-table .method.POST {
background: #007bff;
color: white;
}
</style> </style>
{% endblock %} {% 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