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"""
......
{% extends "base.html" %}
{% block title %}Prompt Cache Settings{% endblock %}
{% block content %}
<div class="container-fluid" style="padding: 20px; background: #0f3460; min-height: 100vh;">
<div style="max-width: 1400px; margin: 0 auto;">
<h2 style="color: #4a9eff; margin-bottom: 30px;">
<i class="fas fa-memory me-2"></i>Prompt Cache Settings
</h2>
<!-- Global Cache Setting -->
<div style="background: #16213e; border: 2px solid #f39c12; border-radius: 8px; padding: 20px; margin-bottom: 20px;">
<h3 style="margin: 0 0 20px 0; color: #f39c12;">
<i class="fas fa-globe me-2"></i>Global Cache Setting
</h3>
<div style="background: #1a1a2e; padding: 15px; border-radius: 8px;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div>
<h5 style="margin: 0 0 5px 0; color: #e0e0e0;">Enable Prompt Caching</h5>
<p style="color: #888; margin: 0; font-size: 14px;">Enable/disable prompt caching for all providers and models</p>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="globalCacheToggle" onchange="setCacheSetting(null, null, this.checked)">
<label class="form-check-label" for="globalCacheToggle"></label>
</div>
</div>
</div>
</div>
<!-- Provider-specific Settings -->
<div style="background: #16213e; border: 2px solid #4a9eff; border-radius: 8px; padding: 20px; margin-bottom: 20px;">
<h3 style="margin: 0 0 20px 0; color: #4a9eff;">
<i class="fas fa-server me-2"></i>Provider-specific Settings
</h3>
<div style="background: #1a1a2e; padding: 15px; border-radius: 8px; margin-bottom: 15px;">
<h5 style="margin: 0 0 15px 0; color: #e0e0e0;">Add Provider Setting</h5>
<div style="display: flex; gap: 15px; align-items: center; flex-wrap: wrap;">
<div style="flex: 1; min-width: 200px;">
<label style="color: #888; font-size: 14px;">Provider</label>
<select id="providerSelect" class="form-control" style="background: #16213e; border: 1px solid #0f3460; color: #e0e0e0;">
<option value="">Select provider...</option>
</select>
</div>
<div style="flex: 1; min-width: 200px;">
<label style="color: #888; font-size: 14px;">Model (optional)</label>
<input type="text" id="modelInput" class="form-control" style="background: #16213e; border: 1px solid #0f3460; color: #e0e0e0;" placeholder="Leave empty for all models">
</div>
<div style="flex: 0 0 auto;">
<label style="color: #888; font-size: 14px;">Enable Cache</label>
<div style="margin-top: 5px;">
<label class="form-check-label" style="margin-right: 10px; color: #888;">No</label>
<input type="checkbox" id="addProviderCacheToggle" checked>
<label class="form-check-label" style="margin-left: 10px; color: #888;">Yes</label>
</div>
</div>
<div style="flex: 0 0 auto; align-self: flex-end;">
<button type="button" class="btn" style="background: #4a9eff; color: white;" onclick="addProviderSetting()">
<i class="fas fa-plus me-2"></i>Add
</button>
</div>
</div>
</div>
<table class="table table-dark" style="margin: 0;">
<thead>
<tr>
<th>Provider</th>
<th>Model</th>
<th>Cache Enabled</th>
<th>Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="cacheSettingsTable">
<tr>
<td colspan="5" style="text-align: center; color: #888;">Loading...</td>
</tr>
</tbody>
</table>
</div>
<!-- Back Button -->
<div style="margin-top: 30px;">
<a href="{{ url_for(request, '/dashboard') }}" class="btn" style="background: #0f3460; color: #4a9eff; border: 1px solid #4a9eff;">
<i class="fas fa-arrow-left me-2"></i>Back to Dashboard
</a>
</div>
</div>
</div>
<script>
let cacheSettings = [];
let userProviders = [];
document.addEventListener('DOMContentLoaded', function() {
loadCacheSettings();
loadUserProviders();
});
async function loadCacheSettings() {
try {
const response = await fetch('{{ url_for(request, "/api/user/cache-settings") }}');
const data = await response.json();
cacheSettings = data.settings || [];
renderCacheSettings();
} catch (error) {
console.error('Error loading cache settings:', error);
document.getElementById('cacheSettingsTable').innerHTML = '<tr><td colspan="5" style="text-align: center; color: #e74c3c;">Error loading settings</td></tr>';
}
}
async function loadUserProviders() {
try {
const response = await fetch('{{ url_for(request, "/api/user/providers") }}');
const data = await response.json();
userProviders = data.providers || [];
const select = document.getElementById('providerSelect');
select.innerHTML = '<option value="">Select provider...</option>';
userProviders.forEach(provider => {
const option = document.createElement('option');
option.value = provider.id || provider.key;
option.textContent = provider.name || provider.id || provider.key;
select.appendChild(option);
});
} catch (error) {
console.error('Error loading providers:', error);
}
}
function renderCacheSettings() {
const tbody = document.getElementById('cacheSettingsTable');
if (cacheSettings.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; color: #888;">No custom settings defined</td></tr>';
return;
}
tbody.innerHTML = cacheSettings.map(setting => `
<tr>
<td>${setting.provider_id || '<em>All</em>'}</td>
<td>${setting.model_name || '<em>All</em>'}</td>
<td>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" ${setting.cache_enabled ? 'checked' : ''}
onchange="setCacheSetting('${setting.provider_id || ''}', '${setting.model_name || ''}', this.checked)">
</div>
</td>
<td>${setting.updated_at || '-'}</td>
<td>
<button type="button" class="btn btn-sm btn-danger" onclick="deleteCacheSetting('${setting.provider_id || ''}', '${setting.model_name || ''}')">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
`).join('');
// Update global toggle
const globalSetting = cacheSettings.find(s => !s.provider_id && !s.model_name);
document.getElementById('globalCacheToggle').checked = globalSetting ? globalSetting.cache_enabled : true;
}
async function setCacheSetting(provider_id, model_name, enabled) {
try {
const response = await fetch('{{ url_for(request, "/api/user/cache-settings") }}', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
provider_id: provider_id || null,
model_name: model_name || null,
cache_enabled: enabled
})
});
if (response.ok) {
showToast('Cache setting updated', 'success');
await loadCacheSettings();
} else {
showToast('Failed to update setting', 'danger');
}
} catch (error) {
console.error('Error updating cache setting:', error);
showToast('Error updating setting', 'danger');
}
}
async function addProviderSetting() {
const provider_id = document.getElementById('providerSelect').value;
const model_name = document.getElementById('modelInput').value.trim();
const enabled = document.getElementById('addProviderCacheToggle').checked;
if (!provider_id) {
showToast('Please select a provider', 'warning');
return;
}
await setCacheSetting(provider_id, model_name, enabled);
// Clear inputs
document.getElementById('providerSelect').value = '';
document.getElementById('modelInput').value = '';
}
async function deleteCacheSetting(provider_id, model_name) {
if (!confirm('Are you sure you want to delete this setting?')) {
return;
}
try {
const url = new URL('{{ url_for(request, "/api/user/cache-settings") }}', window.location.origin);
if (provider_id) url.searchParams.append('provider_id', provider_id);
if (model_name) url.searchParams.append('model_name', model_name);
const response = await fetch(url, { method: 'DELETE' });
if (response.ok) {
showToast('Cache setting deleted', 'success');
await loadCacheSettings();
} else {
showToast('Failed to delete setting', 'danger');
}
} catch (error) {
console.error('Error deleting cache setting:', error);
showToast('Error deleting setting', 'danger');
}
}
function showToast(message, type) {
const alertDiv = document.createElement('div');
alertDiv.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 10000;
min-width: 400px;
padding: 20px 30px;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
font-size: 16px;
font-weight: 500;
text-align: center;
animation: slideDown 0.3s ease-out;
${type === 'success' ? 'background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%); color: white; border: 2px solid #27ae60;' : ''}
${type === 'danger' ? 'background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); color: white; border: 2px solid #e74c3c;' : ''}
${type === 'warning' ? 'background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%); color: white; border: 2px solid #f39c12;' : ''}
`;
const icon = type === 'success' ? 'fa-check-circle' : type === 'danger' ? 'fa-times-circle' : 'fa-exclamation-triangle';
alertDiv.innerHTML = `<i class="fas ${icon}" style="font-size: 24px; margin-right: 10px; vertical-align: middle;"></i><span style="vertical-align: middle;">${message}</span>`;
if (!document.getElementById('alertAnimations')) {
const style = document.createElement('style');
style.id = 'alertAnimations';
style.textContent = `
@keyframes slideDown {
from {
opacity: 0;
transform: translateX(-50%) translateY(-20px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
@keyframes slideUp {
from {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
to {
opacity: 0;
transform: translateX(-50%) translateY(-20px);
}
}
`;
document.head.appendChild(style);
}
document.body.appendChild(alertDiv);
setTimeout(() => {
alertDiv.style.animation = 'slideUp 0.3s ease-out';
setTimeout(() => alertDiv.remove(), 300);
}, 4000);
}
</script>
{% 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