Commit a33c622b authored by Your Name's avatar Your Name

Add comprehensive user API documentation and MCP user tools

- Document user-specific API endpoints: /api/user/models, /api/user/providers,
  /api/user/rotations, /api/user/autoselects, /api/user/chat/completions
- Document user MCP tools: list_user_models, list_user_providers, set_user_provider,
  delete_user_provider, list_user_rotations, set_user_rotation, delete_user_rotation,
  list_user_autoselects, set_user_autoselect, delete_user_autoselect, user_chat_completion
- Update user dashboard with clear endpoint documentation
- Add enhanced analytics for user token usage tracking
- Add database improvements for user token management
parent 76c5318f
......@@ -766,6 +766,262 @@ Default credentials:
- Username: `admin`
- Password: `admin` (SHA256 hashed in config)
## User-Specific API Endpoints
AISBF provides user-specific API endpoints that allow authenticated users to access their own configurations. These endpoints are useful for users who want to manage their own providers, rotations, and autoselects separately from the global configuration.
### Authentication
All user-specific endpoints require authentication via Bearer token:
```bash
curl -H "Authorization: Bearer YOUR_USER_TOKEN" http://localhost:17765/api/user/models
```
Generate a user token from the dashboard: **Dashboard > My Account > API Tokens**
### User API Endpoints
#### List User Models
Returns all models from the user's own providers, rotations, and autoselects:
```bash
# Get all user models
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/models
```
Response includes:
- User provider models (`user-provider/provider_id/model_name`)
- User rotation models (`user-rotation/rotation_name`)
- User autoselect models (`user-autoselect/autoselect_name`)
#### List User Providers
Returns all user-configured providers:
```bash
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/providers
```
#### List User Rotations
Returns all user-configured rotations:
```bash
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/rotations
```
#### List User Autoselects
Returns all user-configured autoselects:
```bash
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/autoselects
```
#### User Chat Completions
Send chat completion requests using user's own configurations:
```bash
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
```
**Model formats for user endpoints:**
- `user-provider/provider_id/model_name` - Use user's provider
- `user-rotation/rotation_name` - Use user's rotation
- `user-autoselect/autoselect_name` - Use user's autoselect
**Admin users** can also access global configurations via these endpoints using the format:
- `provider/model_name` - Global provider
- `rotation/rotation_name` - Global rotation
- `autoselect/autoselect_name` - Global autoselect
#### List Models for Specific Config Type
Get models for a specific user configuration type:
```bash
# Get user provider models
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/providers/models
# Get user rotation models
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/rotations/models
# Get user autoselect models
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/autoselects/models
```
### Python Examples
```python
import requests
BASE_URL = "http://localhost:17765"
TOKEN = "YOUR_USER_TOKEN"
headers = {"Authorization": f"Bearer {TOKEN}"}
# List user models
response = requests.get(f"{BASE_URL}/api/user/models", headers=headers)
print(response.json())
# List user providers
response = requests.get(f"{BASE_URL}/api/user/providers", headers=headers)
print(response.json())
# Send chat completion using user rotation
response = requests.post(
f"{BASE_URL}/api/user/chat/completions",
headers=headers,
json={
"model": "user-rotation/myrotation",
"messages": [{"role": "user", "content": "Hello"}]
}
)
print(response.json())
```
### Using with OpenAI SDK
```python
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:17765/api/v1",
api_key="YOUR_USER_TOKEN" # Use user token as API key
)
# Use user's rotation
response = client.chat.completions.create(
model="user-rotation/myrotation",
messages=[{"role": "user", "content": "Hello"}]
)
print(response.choices[0].message.content)
```
## MCP User Tools
The MCP server includes user-specific tools that allow authenticated users to configure their own models, providers, rotations, and autoselects. These tools are available when a user_id is associated with the authenticated token.
### Available User Tools
**User Models:**
- `list_user_models` - List all models from user's own configurations
**User Providers:**
- `list_user_providers` - List all user-configured providers
- `get_user_provider` - Get a specific user provider
- `set_user_provider` - Save a user provider configuration
- `delete_user_provider` - Delete a user provider
**User Rotations:**
- `list_user_rotations` - List all user-configured rotations
- `get_user_rotation` - Get a specific user rotation
- `set_user_rotation` - Save a user rotation configuration
- `delete_user_rotation` - Delete a user rotation
**User Autoselects:**
- `list_user_autoselects` - List all user-configured autoselects
- `get_user_autoselect` - Get a specific user autoselect
- `set_user_autoselect` - Save a user autoselect configuration
- `delete_user_autoselect` - Delete a user autoselect
**User Chat:**
- `user_chat_completion` - Send chat completion using user's configurations
### MCP User Tool Examples
```bash
# List user models
curl -X POST http://localhost:17765/mcp \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "list_user_models",
"arguments": {}
}
}'
# Set a user provider
curl -X POST http://localhost:17765/mcp \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "set_user_provider",
"arguments": {
"provider_id": "myprovider",
"provider_data": {
"name": "My Provider",
"type": "openai",
"endpoint": "https://api.openai.com/v1",
"api_key": "sk-...",
"models": [
{"name": "gpt-4"}
]
}
}
}
}'
# Send chat using user's rotation
curl -X POST http://localhost:17765/mcp \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "user_chat_completion",
"arguments": {
"model": "user-rotation/myrotation",
"messages": [{"role": "user", "content": "Hello"}]
}
}
}'
```
### Direct Tool Call Examples
```bash
# List user providers
curl -X POST http://localhost:17765/mcp/tools/call \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "list_user_providers",
"arguments": {}
}'
# Get user rotation
curl -X POST http://localhost:17765/mcp/tools/call \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "get_user_rotation",
"arguments": {
"rotation_id": "myrotation"
}
}'
```
## License
Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
......
......@@ -764,6 +764,157 @@ class Analytics:
self._latencies = {}
self._error_types = {}
logger.info("Analytics stats reset")
# User-specific analytics methods
def get_user_stats(self, user_id: int) -> Dict[str, Any]:
"""
Get token usage statistics for a specific user.
Args:
user_id: The user ID
Returns:
Dictionary with user token statistics
"""
return self.db.get_user_token_usage_stats(user_id)
def get_all_users_stats(self) -> List[Dict[str, Any]]:
"""
Get token usage statistics for all users (admin only).
Returns:
List of user statistics
"""
return self.db.get_all_users_token_usage()
def get_global_stats(self) -> Dict[str, Any]:
"""
Get global token usage statistics (across all users and non-user requests).
Returns:
Dictionary with global token statistics
"""
# Get total tokens in last hour and day from the token_usage table
with self.db._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db.db_type == 'sqlite' else '%s'
# Last hour
cursor.execute(f'''
SELECT COALESCE(SUM(tokens_used), 0)
FROM token_usage
WHERE timestamp >= {placeholder}
''', ((datetime.now() - timedelta(hours=1)).isoformat(),))
tokens_1h = cursor.fetchone()[0] or 0
# Last day
cursor.execute(f'''
SELECT COALESCE(SUM(tokens_used), 0)
FROM token_usage
WHERE timestamp >= {placeholder}
''', ((datetime.now() - timedelta(days=1)).isoformat(),))
tokens_1d = cursor.fetchone()[0] or 0
# Last week
cursor.execute(f'''
SELECT COALESCE(SUM(tokens_used), 0)
FROM token_usage
WHERE timestamp >= {placeholder}
''', ((datetime.now() - timedelta(days=7)).isoformat(),))
tokens_7d = cursor.fetchone()[0] or 0
# Total by provider
cursor.execute(f'''
SELECT provider_id, COALESCE(SUM(tokens_used), 0) as total
FROM token_usage
GROUP BY provider_id
''')
provider_totals = {row[0]: row[1] for row in cursor.fetchall()}
return {
'tokens_1h': tokens_1h,
'tokens_1d': tokens_1d,
'tokens_7d': tokens_7d,
'provider_totals': provider_totals,
'estimated_cost_1h': self.estimate_cost('global', tokens_1h),
'estimated_cost_1d': self.estimate_cost('global', tokens_1d),
'estimated_cost_7d': self.estimate_cost('global', tokens_7d)
}
def get_user_token_usage_over_time(
self,
user_id: int,
time_range: str = '24h',
from_datetime: Optional[datetime] = None,
to_datetime: Optional[datetime] = None
) -> List[Dict[str, Any]]:
"""
Get token usage over time for a specific user.
Args:
user_id: The user ID
time_range: Time range ('1h', '6h', '24h', '7d', '30d', '90d', 'custom')
from_datetime: Optional custom start datetime
to_datetime: Optional custom end datetime
Returns:
List of time-series data points
"""
# Determine time range
if time_range == 'custom' and from_datetime and to_datetime:
cutoff = from_datetime
end_time = to_datetime
elif time_range == '1h':
cutoff = datetime.now() - timedelta(hours=1)
end_time = datetime.now()
elif time_range == '6h':
cutoff = datetime.now() - timedelta(hours=6)
end_time = datetime.now()
elif time_range == '24h':
cutoff = datetime.now() - timedelta(hours=24)
end_time = datetime.now()
elif time_range == '7d':
cutoff = datetime.now() - timedelta(days=7)
end_time = datetime.now()
elif time_range == '30d':
cutoff = datetime.now() - timedelta(days=30)
end_time = datetime.now()
elif time_range == '90d':
cutoff = datetime.now() - timedelta(days=90)
end_time = datetime.now()
else: # Default 24h
cutoff = datetime.now() - timedelta(hours=24)
end_time = datetime.now()
with self.db._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db.db_type == 'sqlite' else '%s'
if self.db.db_type == 'sqlite':
date_format = "%Y-%m-%d %H:%M"
else:
date_format = "%Y-%m-%d %H:%i"
cursor.execute(f'''
SELECT
strftime('{date_format}', timestamp) as time_bucket,
SUM(tokens_used) as tokens,
provider_id
FROM token_usage
WHERE user_id = {placeholder} AND timestamp >= {placeholder} AND timestamp <= {placeholder}
GROUP BY time_bucket, provider_id
ORDER BY time_bucket
''', (user_id, cutoff.isoformat(), end_time.isoformat()))
results = []
for row in cursor.fetchall():
results.append({
'timestamp': row[0],
'tokens': row[1],
'provider_id': row[2]
})
return results
# Global analytics instance
......
......@@ -133,6 +133,7 @@ class DatabaseManager:
cursor.execute(f'''
CREATE TABLE IF NOT EXISTS token_usage (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER,
provider_id VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
tokens_used INTEGER NOT NULL,
......@@ -140,6 +141,15 @@ class DatabaseManager:
)
''')
# Create indexes for token_usage
try:
cursor.execute('''
CREATE INDEX idx_token_user
ON token_usage(user_id)
''')
except:
pass
# Create indexes for better query performance
try:
cursor.execute('''
......@@ -408,7 +418,8 @@ class DatabaseManager:
self,
provider_id: str,
model_name: str,
tokens_used: int
tokens_used: int,
user_id: Optional[int] = None
):
"""
Record token usage for rate limiting.
......@@ -417,23 +428,25 @@ class DatabaseManager:
provider_id: The provider identifier
model_name: The model name
tokens_used: Number of tokens used in the request
user_id: Optional user ID for user-specific tracking
"""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
cursor.execute(f'''
INSERT INTO token_usage (provider_id, model_name, tokens_used, timestamp)
VALUES ({placeholder}, {placeholder}, {placeholder}, CURRENT_TIMESTAMP)
''', (provider_id, model_name, tokens_used))
INSERT INTO token_usage (user_id, provider_id, model_name, tokens_used, timestamp)
VALUES ({placeholder}, {placeholder}, {placeholder}, {placeholder}, CURRENT_TIMESTAMP)
''', (user_id, provider_id, model_name, tokens_used))
conn.commit()
logger.debug(f"Recorded token usage for {provider_id}/{model_name}: {tokens_used}")
logger.debug(f"Recorded token usage for {provider_id}/{model_name}: {tokens_used} (user_id={user_id})")
def get_token_usage(
self,
provider_id: str,
model_name: str,
time_window: str = '1m' # 1m, 1h, 1d
time_window: str = '1m', # 1m, 1h, 1d
user_id: Optional[int] = None
) -> int:
"""
Get total token usage for a model within a time window.
......@@ -442,6 +455,7 @@ class DatabaseManager:
provider_id: The provider identifier
model_name: The model name
time_window: Time window ('1m' for minute, '1h' for hour, '1d' for day)
user_id: Optional user ID to filter by
Returns:
Total tokens used within the time window
......@@ -460,21 +474,132 @@ class DatabaseManager:
cutoff = datetime.now() - timedelta(minutes=1)
placeholder = '?' if self.db_type == 'sqlite' else '%s'
if self.db_type == 'sqlite':
if user_id is not None:
if self.db_type == 'sqlite':
cursor.execute(f'''
SELECT COALESCE(SUM(tokens_used), 0)
FROM token_usage
WHERE user_id = {placeholder} AND provider_id = {placeholder} AND model_name = {placeholder} AND timestamp >= {placeholder}
''', (user_id, provider_id, model_name, cutoff.isoformat()))
else: # mysql
cursor.execute(f'''
SELECT COALESCE(SUM(tokens_used), 0)
FROM token_usage
WHERE user_id = {placeholder} AND provider_id = {placeholder} AND model_name = {placeholder} AND timestamp >= {placeholder}
''', (user_id, provider_id, model_name, cutoff.isoformat()))
else:
if self.db_type == 'sqlite':
cursor.execute(f'''
SELECT COALESCE(SUM(tokens_used), 0)
FROM token_usage
WHERE provider_id = {placeholder} AND model_name = {placeholder} AND timestamp >= {placeholder}
''', (provider_id, model_name, cutoff.isoformat()))
else: # mysql
cursor.execute(f'''
SELECT COALESCE(SUM(tokens_used), 0)
FROM token_usage
WHERE provider_id = {placeholder} AND model_name = {placeholder} AND timestamp >= {placeholder}
''', (provider_id, model_name, cutoff.isoformat()))
result = cursor.fetchone()
return result[0] if result else 0
def get_user_token_usage_stats(self, user_id: int) -> Dict[str, int]:
"""
Get aggregated token usage statistics for a user across all providers.
Args:
user_id: The user ID
Returns:
Dictionary with TPM, TPH, TPD statistics
"""
return {
'TPM': self.get_user_token_usage(user_id, '1m'),
'TPH': self.get_user_token_usage(user_id, '1h'),
'TPD': self.get_user_token_usage(user_id, '1d')
}
def get_user_token_usage(self, user_id: int, time_window: str = '1m') -> int:
"""
Get total token usage for a user within a time window.
Args:
user_id: The user ID
time_window: Time window ('1m', '1h', '1d')
Returns:
Total tokens used within the time window
"""
with self._get_connection() as conn:
cursor = conn.cursor()
if time_window == '1m':
cutoff = datetime.now() - timedelta(minutes=1)
elif time_window == '1h':
cutoff = datetime.now() - timedelta(hours=1)
elif time_window == '1d':
cutoff = datetime.now() - timedelta(days=1)
else:
cutoff = datetime.now() - timedelta(minutes=1)
placeholder = '?' if self.db_type == 'sqlite' else '%s'
cursor.execute(f'''
SELECT COALESCE(SUM(tokens_used), 0)
FROM token_usage
WHERE user_id = {placeholder} AND timestamp >= {placeholder}
''', (user_id, cutoff.isoformat()))
result = cursor.fetchone()
return result[0] if result else 0
def get_all_users_token_usage(self) -> List[Dict]:
"""
Get aggregated token usage for all users.
Returns:
List of user statistics with token usage
"""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
# Get all users
cursor.execute(f'''
SELECT u.id, u.username, u.role
FROM users u
WHERE u.is_active = 1
''')
users = []
for row in cursor.fetchall():
user_id = row[0]
# Get token usage for this user in last hour and day
cursor.execute(f'''
SELECT COALESCE(SUM(tokens_used), 0)
FROM token_usage
WHERE provider_id = {placeholder} AND model_name = {placeholder} AND timestamp >= {placeholder}
''', (provider_id, model_name, cutoff.isoformat()))
else: # mysql
WHERE user_id = {placeholder} AND timestamp >= {placeholder}
''', (user_id, (datetime.now() - timedelta(hours=1)).isoformat()))
tokens_1h = cursor.fetchone()[0] or 0
cursor.execute(f'''
SELECT COALESCE(SUM(tokens_used), 0)
FROM token_usage
WHERE provider_id = {placeholder} AND model_name = {placeholder} AND timestamp >= {placeholder}
''', (provider_id, model_name, cutoff.isoformat()))
result = cursor.fetchone()
return result[0] if result else 0
WHERE user_id = {placeholder} AND timestamp >= {placeholder}
''', (user_id, (datetime.now() - timedelta(days=1)).isoformat()))
tokens_1d = cursor.fetchone()[0] or 0
users.append({
'user_id': user_id,
'username': row[1],
'role': row[2],
'tokens_1h': tokens_1h,
'tokens_1d': tokens_1d
})
return users
def cleanup_old_token_usage(self, days_to_keep: int = 7):
"""
......
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