Fix Claude CLI streaming: pass-through SSE strings, handle assistant/tool_use...

Fix Claude CLI streaming: pass-through SSE strings, handle assistant/tool_use events, non-streaming via --output-format json
parent 23f7362e
...@@ -3651,20 +3651,6 @@ def DatabaseManager__create_cache_tables(self, cursor, auto_increment, timestamp ...@@ -3651,20 +3651,6 @@ def DatabaseManager__create_cache_tables(self, cursor, auto_increment, timestamp
) )
''') ''')
cursor.execute(f'''
CREATE TABLE IF NOT EXISTS context_dimensions (
id INTEGER PRIMARY KEY {auto_increment},
provider_id VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
context_size INTEGER,
condense_context INTEGER,
condense_method TEXT,
effective_context INTEGER DEFAULT 0,
last_updated TIMESTAMP DEFAULT {timestamp_default},
UNIQUE(provider_id, model_name)
)
''')
logger.info("⚠️ CACHE DATABASE: Only minimal cache tables created - NO USER TABLES") logger.info("⚠️ CACHE DATABASE: Only minimal cache tables created - NO USER TABLES")
...@@ -4216,6 +4202,48 @@ def DatabaseManager__run_config_migrations(self, cursor, auto_increment, timesta ...@@ -4216,6 +4202,48 @@ def DatabaseManager__run_config_migrations(self, cursor, auto_increment, timesta
except Exception as e: except Exception as e:
logger.warning(f"Migration check for user_notifications table: {e}") logger.warning(f"Migration check for user_notifications table: {e}")
# Migration: Create context_dimensions table if missing
try:
if self.db_type == 'sqlite':
cursor.execute("PRAGMA table_info(context_dimensions)")
if not cursor.fetchall():
cursor.execute(f'''
CREATE TABLE context_dimensions (
id INTEGER PRIMARY KEY {auto_increment},
provider_id VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
context_size INTEGER,
condense_context INTEGER,
condense_method TEXT,
effective_context INTEGER DEFAULT 0,
last_updated TIMESTAMP DEFAULT {timestamp_default},
UNIQUE(provider_id, model_name)
)
''')
logger.info("✅ Migration: Created context_dimensions table")
else:
cursor.execute("""
SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'context_dimensions'
""")
if not cursor.fetchone():
cursor.execute(f'''
CREATE TABLE context_dimensions (
id INTEGER PRIMARY KEY {auto_increment},
provider_id VARCHAR(255) NOT NULL,
model_name VARCHAR(255) NOT NULL,
context_size INTEGER,
condense_context INTEGER,
condense_method TEXT,
effective_context INTEGER DEFAULT 0,
last_updated TIMESTAMP DEFAULT {timestamp_default},
UNIQUE(provider_id, model_name)
)
''')
logger.info("✅ Migration: Created context_dimensions table")
except Exception as e:
logger.warning(f"Migration check for context_dimensions table: {e}")
logger.info("✅ All database migrations completed") logger.info("✅ All database migrations completed")
# Patch the methods # Patch the methods
......
...@@ -595,7 +595,7 @@ class RequestHandler: ...@@ -595,7 +595,7 @@ class RequestHandler:
}], model_name) }], model_name)
else: else:
# Fallback to estimation if no content # Fallback to estimation if no content
max_tokens = request_data.get('max_tokens', 0) max_tokens = request_data.get('max_tokens') or 0
if max_tokens > 0: if max_tokens > 0:
completion_tokens = min(max_tokens, estimated_prompt_tokens * 2) completion_tokens = min(max_tokens, estimated_prompt_tokens * 2)
else: else:
...@@ -1182,10 +1182,13 @@ class RequestHandler: ...@@ -1182,10 +1182,13 @@ class RequestHandler:
logger.debug(f"Async chunk type: {type(chunk)}") logger.debug(f"Async chunk type: {type(chunk)}")
logger.debug(f"Async chunk: {chunk}") logger.debug(f"Async chunk: {chunk}")
# For async generators, chunks might be bytes (SSE format) # For async generators, chunks might be bytes or pre-formatted SSE strings
if isinstance(chunk, bytes): if isinstance(chunk, bytes):
logger.debug(f"Yielding raw bytes chunk: {len(chunk)} bytes") logger.debug(f"Yielding raw bytes chunk: {len(chunk)} bytes")
yield chunk yield chunk
elif isinstance(chunk, str):
# Already SSE-formatted (e.g. "data: {...}\n\n") — pass through directly
yield chunk.encode('utf-8')
else: else:
# Fallback: treat as dict and serialize # Fallback: treat as dict and serialize
chunk_dict = chunk.model_dump() if hasattr(chunk, 'model_dump') else chunk chunk_dict = chunk.model_dump() if hasattr(chunk, 'model_dump') else chunk
...@@ -2967,12 +2970,12 @@ class RotationHandler: ...@@ -2967,12 +2970,12 @@ class RotationHandler:
estimated_prompt_tokens = count_messages_tokens(messages, model_name) estimated_prompt_tokens = count_messages_tokens(messages, model_name)
# More realistic completion estimate # More realistic completion estimate
max_tokens = request_data.get('max_tokens', 0) max_tokens = request_data.get('max_tokens') or 0
if max_tokens > 0: if max_tokens > 0:
estimated_completion = min(max_tokens, estimated_prompt_tokens * 2) estimated_completion = min(max_tokens, estimated_prompt_tokens * 2)
else: else:
estimated_completion = max(estimated_prompt_tokens, 50) estimated_completion = max(estimated_prompt_tokens, 50)
total_tokens = estimated_prompt_tokens + estimated_completion total_tokens = estimated_prompt_tokens + estimated_completion
prompt_tokens = estimated_prompt_tokens prompt_tokens = estimated_prompt_tokens
completion_tokens = estimated_completion completion_tokens = estimated_completion
...@@ -3636,11 +3639,12 @@ class RotationHandler: ...@@ -3636,11 +3639,12 @@ class RotationHandler:
logger.debug(f"Async chunk type: {type(chunk)}") logger.debug(f"Async chunk type: {type(chunk)}")
logger.debug(f"Async chunk: {chunk}") logger.debug(f"Async chunk: {chunk}")
# For Kiro, chunks are already properly formatted SSE bytes # For Kiro/Claude CLI, chunks may be pre-formatted SSE bytes or strings
# Just pass them through directly
if isinstance(chunk, bytes): if isinstance(chunk, bytes):
logger.debug(f"Yielding raw bytes chunk: {len(chunk)} bytes") logger.debug(f"Yielding raw bytes chunk: {len(chunk)} bytes")
yield chunk yield chunk
elif isinstance(chunk, str):
yield chunk.encode('utf-8')
else: else:
# Fallback: treat as dict and serialize # Fallback: treat as dict and serialize
chunk_dict = chunk.model_dump() if hasattr(chunk, 'model_dump') else chunk chunk_dict = chunk.model_dump() if hasattr(chunk, 'model_dump') else chunk
...@@ -4464,12 +4468,12 @@ class AutoselectHandler: ...@@ -4464,12 +4468,12 @@ class AutoselectHandler:
estimated_prompt_tokens = count_messages_tokens(messages, model_name) estimated_prompt_tokens = count_messages_tokens(messages, model_name)
# More realistic completion estimate # More realistic completion estimate
max_tokens = request_data.get('max_tokens', 0) max_tokens = request_data.get('max_tokens') or 0
if max_tokens > 0: if max_tokens > 0:
estimated_completion = min(max_tokens, estimated_prompt_tokens * 2) estimated_completion = min(max_tokens, estimated_prompt_tokens * 2)
else: else:
estimated_completion = max(estimated_prompt_tokens, 50) estimated_completion = max(estimated_prompt_tokens, 50)
total_tokens = estimated_prompt_tokens + estimated_completion total_tokens = estimated_prompt_tokens + estimated_completion
prompt_tokens = estimated_prompt_tokens prompt_tokens = estimated_prompt_tokens
completion_tokens = estimated_completion completion_tokens = estimated_completion
......
This diff is collapsed.
...@@ -578,6 +578,11 @@ _MUST_CHANGE_PASSWORD_WHITELIST = ( ...@@ -578,6 +578,11 @@ _MUST_CHANGE_PASSWORD_WHITELIST = (
'/dashboard/settings', '/dashboard/settings',
'/dashboard/logout', '/dashboard/logout',
'/api/admin/settings/', '/api/admin/settings/',
'/dashboard/tor/status',
'/dashboard/response-cache/stats',
'/dashboard/response-cache/clear',
'/dashboard/test-smtp',
'/dashboard/restart',
) )
# --- Login rate limiter --- # --- Login rate limiter ---
...@@ -1546,10 +1551,14 @@ async def api_token_authorization_middleware(request: Request, call_next): ...@@ -1546,10 +1551,14 @@ async def api_token_authorization_middleware(request: Request, call_next):
if request.method == "GET" and path in ["/api/models", "/api/v1/models"]: if request.method == "GET" and path in ["/api/models", "/api/v1/models"]:
return await call_next(request) return await call_next(request)
# If authentication is globally disabled, skip all token scope checks
if not (server_config and server_config.get('auth_enabled', False)):
return await call_next(request)
is_global_token = getattr(request.state, 'is_global_token', False) is_global_token = getattr(request.state, 'is_global_token', False)
user_id = getattr(request.state, 'user_id', None) user_id = getattr(request.state, 'user_id', None)
is_admin = getattr(request.state, 'is_admin', False) is_admin = getattr(request.state, 'is_admin', False)
# Debug logging # Debug logging
logger.info(f"API Token Auth: path={path}, is_global_token={is_global_token}, user_id={user_id}") logger.info(f"API Token Auth: path={path}, is_global_token={is_global_token}, user_id={user_id}")
...@@ -5578,6 +5587,7 @@ async def dashboard_settings(request: Request): ...@@ -5578,6 +5587,7 @@ async def dashboard_settings(request: Request):
'fullconfig_tokens': [] 'fullconfig_tokens': []
} }
warning = request.query_params.get('warning')
return templates.TemplateResponse( return templates.TemplateResponse(
request=request, request=request,
name="dashboard/settings.html", name="dashboard/settings.html",
...@@ -5586,7 +5596,8 @@ async def dashboard_settings(request: Request): ...@@ -5586,7 +5596,8 @@ async def dashboard_settings(request: Request):
"session": request.session, "session": request.session,
"__version__": __version__, "__version__": __version__,
"config": aisbf_config, "config": aisbf_config,
"os": os "os": os,
"warning": warning,
} }
) )
...@@ -5599,7 +5610,6 @@ async def dashboard_settings_save( ...@@ -5599,7 +5610,6 @@ async def dashboard_settings_save(
auth_enabled: bool = Form(False), auth_enabled: bool = Form(False),
auth_tokens: str = Form(""), auth_tokens: str = Form(""),
dashboard_username: str = Form(...), dashboard_username: str = Form(...),
dashboard_password: str = Form(""),
condensation_model_id: str = Form(...), condensation_model_id: str = Form(...),
autoselect_model_id: str = Form(...), autoselect_model_id: str = Form(...),
database_type: str = Form("sqlite"), database_type: str = Form("sqlite"),
...@@ -5691,8 +5701,6 @@ async def dashboard_settings_save( ...@@ -5691,8 +5701,6 @@ async def dashboard_settings_save(
aisbf_config['auth']['enabled'] = auth_enabled aisbf_config['auth']['enabled'] = auth_enabled
aisbf_config['auth']['tokens'] = [t.strip() for t in auth_tokens.split('\n') if t.strip()] aisbf_config['auth']['tokens'] = [t.strip() for t in auth_tokens.split('\n') if t.strip()]
aisbf_config['dashboard']['username'] = dashboard_username aisbf_config['dashboard']['username'] = dashboard_username
if dashboard_password: # Only update if provided - hash the password
aisbf_config['dashboard']['password'] = _db_hash_password(dashboard_password)
aisbf_config['internal_model']['condensation_model_id'] = condensation_model_id aisbf_config['internal_model']['condensation_model_id'] = condensation_model_id
aisbf_config['internal_model']['autoselect_model_id'] = autoselect_model_id aisbf_config['internal_model']['autoselect_model_id'] = autoselect_model_id
...@@ -5840,12 +5848,10 @@ async def dashboard_settings_save( ...@@ -5840,12 +5848,10 @@ async def dashboard_settings_save(
aisbf_config['dashboard']['notifications']['wallet_topup'] = admin_notify_wallet_topup aisbf_config['dashboard']['notifications']['wallet_topup'] = admin_notify_wallet_topup
aisbf_config['dashboard']['notifications']['user_deleted_account'] = admin_notify_user_deleted_account aisbf_config['dashboard']['notifications']['user_deleted_account'] = admin_notify_user_deleted_account
# Handle new_admin_password from the Admin tab (distinct from dashboard_password in Dashboard tab)
if new_admin_password: if new_admin_password:
if new_admin_password == confirm_admin_password: if new_admin_password == confirm_admin_password:
aisbf_config['dashboard']['password'] = _db_hash_password(new_admin_password) aisbf_config['dashboard']['password'] = _db_hash_password(new_admin_password)
request.session.pop('must_change_password', None) request.session.pop('must_change_password', None)
# silently ignore mismatch — UI should validate
# Save config # Save config
config_path = Path.home() / '.aisbf' / 'aisbf.json' config_path = Path.home() / '.aisbf' / 'aisbf.json'
...@@ -5853,9 +5859,9 @@ async def dashboard_settings_save( ...@@ -5853,9 +5859,9 @@ async def dashboard_settings_save(
with open(config_path, 'w') as f: with open(config_path, 'w') as f:
json.dump(aisbf_config, f, indent=2) json.dump(aisbf_config, f, indent=2)
# If a new dashboard password was submitted, clear the forced-change flag # Reload dashboard credentials in memory so the new username/password takes effect immediately
if dashboard_password: if server_config is not None:
request.session.pop('must_change_password', None) server_config['dashboard_config'] = aisbf_config.get('dashboard', {})
return templates.TemplateResponse( return templates.TemplateResponse(
request=request, request=request,
...@@ -10055,10 +10061,10 @@ async def v1_chat_completions(request: Request, body: ChatCompletionRequest): ...@@ -10055,10 +10061,10 @@ async def v1_chat_completions(request: Request, body: ChatCompletionRequest):
# PATH 1: Direct provider model (format: {provider}/{model}) # PATH 1: Direct provider model (format: {provider}/{model})
if provider_id not in config.providers: if provider_id not in config.providers:
raise HTTPException( raise HTTPException(
status_code=404, status_code=404,
detail=f"User autoselect '{actual_model}' not found. Available: {list(handler.user_autoselects.keys())}" detail=f"Provider '{provider_id}' not found. Available: {list(config.providers.keys())}"
) )
# Validate kiro credentials before processing request # Validate kiro credentials before processing request
provider_config = config.get_provider(provider_id) provider_config = config.get_provider(provider_id)
......
...@@ -551,10 +551,18 @@ function renderProviderDetails(key) { ...@@ -551,10 +551,18 @@ function renderProviderDetails(key) {
${CLAUDE_CLI_MODE ? ` ${CLAUDE_CLI_MODE ? `
<div style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #1e3a5f;"> <div style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #1e3a5f;">
<h5 style="margin: 0 0 8px 0; color: #4ade80;">Claude CLI Mode Active</h5> <h5 style="margin: 0 0 8px 0; color: #4ade80;">Claude CLI Mode Active</h5>
<small style="color: #4ade80; display: block; margin-bottom: 14px;"> <small style="color: #4ade80; display: block; margin-bottom: 8px;">
The claude CLI was detected at startup. When enabled, requests are piped The claude CLI was detected at startup. When enabled, requests are piped
through the local claude binary instead of the HTTP API. through the local claude binary instead of the HTTP API.
</small> </small>
<div style="background: #3a2a00; border: 1px solid #f59e0b; border-radius: 6px; padding: 10px 14px; margin-bottom: 14px;">
<span style="color: #f59e0b; font-weight: 600;">⚠ Experimental:</span>
<span style="color: #fcd34d; font-size: 0.85em;">
CLI mode is experimental. Tool calling (function calling) does not yet work reliably —
the CLI subprocess may refuse or mishandle tool definitions. Use with simple
(non-tool) requests only until this is resolved.
</span>
</div>
<div class="form-group" style="margin-bottom: 16px;"> <div class="form-group" style="margin-bottom: 16px;">
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;"> <label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
......
...@@ -41,10 +41,20 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -41,10 +41,20 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div class="alert alert-error">{{ error }}</div> <div class="alert alert-error">{{ error }}</div>
{% endif %} {% endif %}
{% if warning == 'default_password' %}
<div style="background:#b71c1c; border:2px solid #e53935; color:#fff; padding:16px 20px; border-radius:6px; margin-bottom:20px; display:flex; align-items:flex-start; gap:12px;">
<span style="font-size:1.4em; line-height:1;">&#9888;</span>
<div>
<strong>Security Warning: Default password in use.</strong><br>
You are logged in with the factory-default <code style="background:rgba(0,0,0,.3);padding:1px 5px;border-radius:3px;">admin / admin</code> credentials.
Please change your password immediately using the <strong>Admin</strong> tab below before using AISBF.
</div>
</div>
{% endif %}
<div class="settings-tabs"> <div class="settings-tabs">
<div class="settings-tab active" onclick="switchTab('server')"><i class="fas fa-server"></i> Server</div> <div class="settings-tab active" onclick="switchTab('server')"><i class="fas fa-server"></i> Server</div>
<div class="settings-tab" onclick="switchTab('auth')"><i class="fas fa-key"></i> Auth &amp; MCP</div> <div class="settings-tab" onclick="switchTab('auth')"><i class="fas fa-key"></i> Auth &amp; MCP</div>
<div class="settings-tab" onclick="switchTab('dashboard')"><i class="fas fa-tachometer-alt"></i> Dashboard</div>
<div class="settings-tab" onclick="switchTab('models')"><i class="fas fa-brain"></i> Models</div> <div class="settings-tab" onclick="switchTab('models')"><i class="fas fa-brain"></i> Models</div>
<div class="settings-tab" onclick="switchTab('database')"><i class="fas fa-database"></i> Database</div> <div class="settings-tab" onclick="switchTab('database')"><i class="fas fa-database"></i> Database</div>
<div class="settings-tab" onclick="switchTab('cache')"><i class="fas fa-bolt"></i> Cache</div> <div class="settings-tab" onclick="switchTab('cache')"><i class="fas fa-bolt"></i> Cache</div>
...@@ -139,20 +149,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -139,20 +149,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div> </div>
</div><!-- /tab-auth --> </div><!-- /tab-auth -->
<div class="settings-section" id="tab-dashboard">
<div class="section-title"><i class="fas fa-tachometer-alt"></i> Dashboard</div>
<div class="form-group">
<label for="dashboard_username">Dashboard Username</label>
<input type="text" id="dashboard_username" name="dashboard_username" value="{{ config.dashboard.username }}" required>
</div>
<div class="form-group">
<label for="dashboard_password">Dashboard Password</label>
<input type="password" id="dashboard_password" name="dashboard_password" placeholder="Leave blank to keep current">
</div>
</div><!-- /tab-dashboard -->
<div class="settings-section" id="tab-models"> <div class="settings-section" id="tab-models">
<div class="section-title"><i class="fas fa-brain"></i> Internal Models</div> <div class="section-title"><i class="fas fa-brain"></i> Internal Models</div>
...@@ -879,15 +875,18 @@ brew services restart tor # macOS</code></pre> ...@@ -879,15 +875,18 @@ brew services restart tor # macOS</code></pre>
<div class="section-title"><i class="fas fa-shield-alt"></i> Admin Account &amp; Notifications</div> <div class="section-title"><i class="fas fa-shield-alt"></i> Admin Account &amp; Notifications</div>
<div class="form-group"> <div class="form-group">
<label for="new_admin_password">New Admin Password</label> <label for="dashboard_username">Admin Username</label>
<input type="text" id="dashboard_username" name="dashboard_username" value="{{ config.dashboard.username }}" required>
</div>
<div class="form-group">
<label for="new_admin_password">New Password</label>
<input type="password" id="new_admin_password" name="new_admin_password" placeholder="Leave blank to keep current password"> <input type="password" id="new_admin_password" name="new_admin_password" placeholder="Leave blank to keep current password">
<small style="color: #666; display: block; margin-top: 5px;">Enter a new password to change the admin dashboard password</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="confirm_admin_password">Confirm New Admin Password</label> <label for="confirm_admin_password">Confirm New Password</label>
<input type="password" id="confirm_admin_password" name="confirm_admin_password" placeholder="Confirm new password"> <input type="password" id="confirm_admin_password" name="confirm_admin_password" placeholder="Confirm new password">
<small style="color: #666; display: block; margin-top: 5px;">Re-enter the new password to confirm</small>
</div> </div>
<div class="form-group"> <div class="form-group">
...@@ -1194,13 +1193,12 @@ async function checkTorStatus() { ...@@ -1194,13 +1193,12 @@ async function checkTorStatus() {
// Check TOR status on page load // Check TOR status on page load
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
checkTorStatus(); checkTorStatus();
// Refresh status every 30 seconds
setInterval(checkTorStatus, 30000); setInterval(checkTorStatus, 30000);
// Load cache statistics
refreshCacheStats(); refreshCacheStats();
// Refresh cache stats every 10 seconds
setInterval(refreshCacheStats, 10000); setInterval(refreshCacheStats, 10000);
{% if warning == 'default_password' %}
switchTab('admin');
{% endif %}
}); });
async function refreshCacheStats() { async function refreshCacheStats() {
......
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