Commit 55032fc7 authored by Your Name's avatar Your Name

Add user prompt cache settings feature

- Add user_cache_settings database table
- Add database methods to get/set cache settings
- Add API endpoints for cache management
- Add /dashboard/user/cache-settings page
- Integrate with Claude provider to respect user cache settings
- Allow disabling cache per provider, per model, or globally
parent af2a4187
......@@ -2854,6 +2854,156 @@ class DatabaseManager:
logger.error(f"Traceback: {traceback.format_exc()}")
return False
def get_user_cache_settings(self, user_id: int, provider_id: str = None, model_name: str = None) -> Dict:
"""
Get user's prompt cache settings.
Args:
user_id: User ID
provider_id: Optional provider ID to filter by
model_name: Optional model name to filter by
Returns:
Dict with cache settings. If specific provider/model not found, returns default enabled.
"""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
# Build query based on filters
if provider_id and model_name:
# Check for specific provider+model setting
cursor.execute(f'''
SELECT cache_enabled FROM user_cache_settings
WHERE user_id = {placeholder} AND provider_id = {placeholder} AND model_name = {placeholder}
''', (user_id, provider_id, model_name))
row = cursor.fetchone()
if row:
return {'cache_enabled': bool(row[0]), 'level': 'model'}
# Fall back to provider-level setting
cursor.execute(f'''
SELECT cache_enabled FROM user_cache_settings
WHERE user_id = {placeholder} AND provider_id = {placeholder} AND model_name IS NULL
''', (user_id, provider_id))
row = cursor.fetchone()
if row:
return {'cache_enabled': bool(row[0]), 'level': 'provider'}
elif provider_id:
# Check for provider-level setting
cursor.execute(f'''
SELECT cache_enabled FROM user_cache_settings
WHERE user_id = {placeholder} AND provider_id = {placeholder} AND model_name IS NULL
''', (user_id, provider_id))
row = cursor.fetchone()
if row:
return {'cache_enabled': bool(row[0]), 'level': 'provider'}
# Check for global user setting (NULL provider and model)
cursor.execute(f'''
SELECT cache_enabled FROM user_cache_settings
WHERE user_id = {placeholder} AND provider_id IS NULL AND model_name IS NULL
''', (user_id,))
row = cursor.fetchone()
if row:
return {'cache_enabled': bool(row[0]), 'level': 'global'}
# Default: cache enabled
return {'cache_enabled': True, 'level': 'default'}
def set_user_cache_setting(self, user_id: int, cache_enabled: bool, provider_id: str = None, model_name: str = None) -> bool:
"""
Set user's prompt cache setting.
Args:
user_id: User ID
cache_enabled: Whether to enable cache
provider_id: Optional provider ID (None = global setting)
model_name: Optional model name (requires provider_id)
Returns:
True if successful
"""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
try:
if self.db_type == 'sqlite':
cursor.execute(f'''
INSERT OR REPLACE INTO user_cache_settings
(user_id, provider_id, model_name, cache_enabled, updated_at)
VALUES ({placeholder}, {placeholder}, {placeholder}, {placeholder}, CURRENT_TIMESTAMP)
''', (user_id, provider_id, model_name, cache_enabled))
else:
cursor.execute(f'''
INSERT INTO user_cache_settings
(user_id, provider_id, model_name, cache_enabled, updated_at)
VALUES ({placeholder}, {placeholder}, {placeholder}, {placeholder}, CURRENT_TIMESTAMP)
ON DUPLICATE KEY UPDATE
cache_enabled = VALUES(cache_enabled),
updated_at = CURRENT_TIMESTAMP
''', (user_id, provider_id, model_name, cache_enabled))
conn.commit()
logger.info(f"Set cache setting for user {user_id}, provider={provider_id}, model={model_name}, enabled={cache_enabled}")
return True
except Exception as e:
logger.error(f"Error setting cache setting: {e}")
return False
def get_all_user_cache_settings(self, user_id: int) -> list:
"""Get all cache settings for a user."""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
cursor.execute(f'''
SELECT provider_id, model_name, cache_enabled, created_at, updated_at
FROM user_cache_settings
WHERE user_id = {placeholder}
ORDER BY provider_id, model_name
''', (user_id,))
rows = cursor.fetchall()
return [{
'provider_id': row[0],
'model_name': row[1],
'cache_enabled': bool(row[2]),
'created_at': row[3],
'updated_at': row[4]
} for row in rows]
def delete_user_cache_setting(self, user_id: int, provider_id: str = None, model_name: str = None) -> bool:
"""Delete a user's cache setting."""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
try:
if provider_id and model_name:
cursor.execute(f'''
DELETE FROM user_cache_settings
WHERE user_id = {placeholder} AND provider_id = {placeholder} AND model_name = {placeholder}
''', (user_id, provider_id, model_name))
elif provider_id:
cursor.execute(f'''
DELETE FROM user_cache_settings
WHERE user_id = {placeholder} AND provider_id = {placeholder} AND model_name IS NULL
''', (user_id, provider_id))
else:
cursor.execute(f'''
DELETE FROM user_cache_settings
WHERE user_id = {placeholder} AND provider_id IS NULL AND model_name IS NULL
''', (user_id,))
conn.commit()
return True
except Exception as e:
logger.error(f"Error deleting cache setting: {e}")
return False
def get_currency_settings(self) -> Dict:
"""Get currency settings from admin_settings table."""
with self._get_connection() as conn:
......@@ -3997,6 +4147,19 @@ def DatabaseManager__run_config_migrations(self, cursor, auto_increment, timesta
FOREIGN KEY (subscription_id) REFERENCES user_subscriptions(id),
FOREIGN KEY (payment_method_id) REFERENCES payment_methods(id)
)
'''),
('user_cache_settings', f'''
CREATE TABLE user_cache_settings (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
provider_id VARCHAR(255),
model_name VARCHAR(255),
cache_enabled {boolean_type} DEFAULT 1,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, provider_id, model_name)
)
''')
]:
try:
......
......@@ -501,8 +501,8 @@ class ClaudeProviderHandler(BaseProviderHandler):
logger.warning(f"ClaudeProviderHandler: Tool result truncated from {len(content)} to {max_chars} characters")
return truncated, True
def _get_cache_config(self) -> Dict:
"""Get prompt caching configuration from provider config."""
def _get_cache_config(self, user_id: int = None, provider_id: str = None, model_name: str = None) -> Dict:
"""Get prompt caching configuration from provider config and user settings."""
cache_config = {
'enabled': False,
'min_messages': 4,
......@@ -518,6 +518,20 @@ class ClaudeProviderHandler(BaseProviderHandler):
cache_config['enabled'] = claude_config.get('enable_prompt_caching', False)
cache_config['min_messages'] = claude_config.get('cache_min_messages', 4)
# Check user's cache settings (overrides provider config)
if user_id and cache_config['enabled']:
try:
from aisbf.database import DatabaseRegistry
db = DatabaseRegistry.get_config_database()
user_setting = db.get_user_cache_settings(user_id, provider_id, model_name)
if not user_setting['cache_enabled']:
cache_config['enabled'] = False
import logging
logging.getLogger(__name__).info(f"User {user_id} disabled cache for provider={provider_id}, model={model_name}")
except Exception as e:
import logging
logging.getLogger(__name__).warning(f"Error checking user cache settings: {e}")
return cache_config
def _get_fallback_models(self) -> List[str]:
......@@ -837,6 +851,16 @@ class ClaudeProviderHandler(BaseProviderHandler):
system_message, anthropic_messages = self._convert_messages_to_anthropic(validated_messages)
# Apply prompt caching based on user and provider settings
cache_config = self._get_cache_config(
user_id=getattr(request, 'user_id', None),
provider_id=getattr(request, 'provider_id', None),
model_name=model
)
if cache_config['enabled']:
anthropic_messages = self._apply_cache_control(anthropic_messages)
# Sanitize system message to avoid Claude's unofficial client detection
# Replace "You are Kilo," or "You are Kiro," with "You are" to prevent
# contradiction with "You are Claude Code" that triggers detection
......
......@@ -6661,6 +6661,124 @@ async def dashboard_user_tokens_delete(request: Request, token_id: int):
except Exception as e:
return JSONResponse(status_code=500, content={"error": str(e)})
@app.get("/dashboard/user/cache-settings", response_class=HTMLResponse)
async def dashboard_user_cache_settings(request: Request):
"""User prompt cache settings page"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return RedirectResponse(url=url_for(request, "/dashboard/login"), status_code=303)
db = DatabaseRegistry.get_config_database()
# Get all cache settings for user
cache_settings = db.get_all_user_cache_settings(user_id)
# Convert datetime objects to strings
for setting in cache_settings:
if 'created_at' in setting and setting['created_at']:
setting['created_at'] = setting['created_at'].isoformat() if hasattr(setting['created_at'], 'isoformat') else str(setting['created_at'])
if 'updated_at' in setting and setting['updated_at']:
setting['updated_at'] = setting['updated_at'].isoformat() if hasattr(setting['updated_at'], 'isoformat') else str(setting['updated_at'])
# Get user's providers for dropdown
user_providers = db.get_user_providers(user_id)
return templates.TemplateResponse(
request=request,
name="dashboard/user_cache_settings.html",
context={
"request": request,
"session": request.session,
"__version__": __version__,
"cache_settings": cache_settings,
"user_providers": user_providers,
"user_id": user_id
}
)
@app.get("/api/user/cache-settings")
async def api_get_user_cache_settings(request: Request):
"""Get user's cache settings"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return JSONResponse(status_code=401, content={"error": "Not authenticated"})
db = DatabaseRegistry.get_config_database()
provider_id = request.query_params.get('provider_id')
model_name = request.query_params.get('model_name')
if provider_id or model_name:
# Get specific setting
setting = db.get_user_cache_settings(user_id, provider_id, model_name)
return JSONResponse(setting)
else:
# Get all settings
settings = db.get_all_user_cache_settings(user_id)
return JSONResponse({"settings": settings})
@app.post("/api/user/cache-settings")
async def api_set_user_cache_setting(request: Request):
"""Set user's cache setting"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return JSONResponse(status_code=401, content={"error": "Not authenticated"})
try:
body = await request.json()
provider_id = body.get('provider_id')
model_name = body.get('model_name')
cache_enabled = body.get('cache_enabled', True)
db = DatabaseRegistry.get_config_database()
success = db.set_user_cache_setting(user_id, cache_enabled, provider_id, model_name)
if success:
return JSONResponse({"success": True, "message": "Cache setting updated"})
else:
return JSONResponse(status_code=500, content={"error": "Failed to update setting"})
except Exception as e:
logger.error(f"Error setting cache setting: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
@app.delete("/api/user/cache-settings")
async def api_delete_user_cache_setting(request: Request):
"""Delete user's cache setting"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
user_id = request.session.get('user_id')
if not user_id:
return JSONResponse(status_code=401, content={"error": "Not authenticated"})
try:
provider_id = request.query_params.get('provider_id')
model_name = request.query_params.get('model_name')
db = DatabaseRegistry.get_config_database()
success = db.delete_user_cache_setting(user_id, provider_id, model_name)
if success:
return JSONResponse({"success": True, "message": "Cache setting deleted"})
else:
return JSONResponse(status_code=500, content={"error": "Failed to delete setting"})
except Exception as e:
logger.error(f"Error deleting cache setting: {e}")
return JSONResponse(status_code=500, content={"error": str(e)})
@app.get("/dashboard/response-cache/stats")
async def dashboard_response_cache_stats(request: Request):
"""Get response cache statistics"""
......
This diff is collapsed.
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