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
)
''')
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")
......@@ -4216,6 +4202,48 @@ def DatabaseManager__run_config_migrations(self, cursor, auto_increment, timesta
except Exception as 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")
# Patch the methods
......
......@@ -595,7 +595,7 @@ class RequestHandler:
}], model_name)
else:
# 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:
completion_tokens = min(max_tokens, estimated_prompt_tokens * 2)
else:
......@@ -1182,10 +1182,13 @@ class RequestHandler:
logger.debug(f"Async chunk type: {type(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):
logger.debug(f"Yielding raw bytes chunk: {len(chunk)} bytes")
yield chunk
elif isinstance(chunk, str):
# Already SSE-formatted (e.g. "data: {...}\n\n") — pass through directly
yield chunk.encode('utf-8')
else:
# Fallback: treat as dict and serialize
chunk_dict = chunk.model_dump() if hasattr(chunk, 'model_dump') else chunk
......@@ -2967,12 +2970,12 @@ class RotationHandler:
estimated_prompt_tokens = count_messages_tokens(messages, model_name)
# 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:
estimated_completion = min(max_tokens, estimated_prompt_tokens * 2)
else:
estimated_completion = max(estimated_prompt_tokens, 50)
total_tokens = estimated_prompt_tokens + estimated_completion
prompt_tokens = estimated_prompt_tokens
completion_tokens = estimated_completion
......@@ -3636,11 +3639,12 @@ class RotationHandler:
logger.debug(f"Async chunk type: {type(chunk)}")
logger.debug(f"Async chunk: {chunk}")
# For Kiro, chunks are already properly formatted SSE bytes
# Just pass them through directly
# For Kiro/Claude CLI, chunks may be pre-formatted SSE bytes or strings
if isinstance(chunk, bytes):
logger.debug(f"Yielding raw bytes chunk: {len(chunk)} bytes")
yield chunk
elif isinstance(chunk, str):
yield chunk.encode('utf-8')
else:
# Fallback: treat as dict and serialize
chunk_dict = chunk.model_dump() if hasattr(chunk, 'model_dump') else chunk
......@@ -4464,12 +4468,12 @@ class AutoselectHandler:
estimated_prompt_tokens = count_messages_tokens(messages, model_name)
# 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:
estimated_completion = min(max_tokens, estimated_prompt_tokens * 2)
else:
estimated_completion = max(estimated_prompt_tokens, 50)
total_tokens = estimated_prompt_tokens + estimated_completion
prompt_tokens = estimated_prompt_tokens
completion_tokens = estimated_completion
......
This diff is collapsed.
......@@ -578,6 +578,11 @@ _MUST_CHANGE_PASSWORD_WHITELIST = (
'/dashboard/settings',
'/dashboard/logout',
'/api/admin/settings/',
'/dashboard/tor/status',
'/dashboard/response-cache/stats',
'/dashboard/response-cache/clear',
'/dashboard/test-smtp',
'/dashboard/restart',
)
# --- Login rate limiter ---
......@@ -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"]:
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)
user_id = getattr(request.state, 'user_id', None)
is_admin = getattr(request.state, 'is_admin', False)
# Debug logging
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):
'fullconfig_tokens': []
}
warning = request.query_params.get('warning')
return templates.TemplateResponse(
request=request,
name="dashboard/settings.html",
......@@ -5586,7 +5596,8 @@ async def dashboard_settings(request: Request):
"session": request.session,
"__version__": __version__,
"config": aisbf_config,
"os": os
"os": os,
"warning": warning,
}
)
......@@ -5599,7 +5610,6 @@ async def dashboard_settings_save(
auth_enabled: bool = Form(False),
auth_tokens: str = Form(""),
dashboard_username: str = Form(...),
dashboard_password: str = Form(""),
condensation_model_id: str = Form(...),
autoselect_model_id: str = Form(...),
database_type: str = Form("sqlite"),
......@@ -5691,8 +5701,6 @@ async def dashboard_settings_save(
aisbf_config['auth']['enabled'] = auth_enabled
aisbf_config['auth']['tokens'] = [t.strip() for t in auth_tokens.split('\n') if t.strip()]
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']['autoselect_model_id'] = autoselect_model_id
......@@ -5840,12 +5848,10 @@ async def dashboard_settings_save(
aisbf_config['dashboard']['notifications']['wallet_topup'] = admin_notify_wallet_topup
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 == confirm_admin_password:
aisbf_config['dashboard']['password'] = _db_hash_password(new_admin_password)
request.session.pop('must_change_password', None)
# silently ignore mismatch — UI should validate
# Save config
config_path = Path.home() / '.aisbf' / 'aisbf.json'
......@@ -5853,9 +5859,9 @@ async def dashboard_settings_save(
with open(config_path, 'w') as f:
json.dump(aisbf_config, f, indent=2)
# If a new dashboard password was submitted, clear the forced-change flag
if dashboard_password:
request.session.pop('must_change_password', None)
# Reload dashboard credentials in memory so the new username/password takes effect immediately
if server_config is not None:
server_config['dashboard_config'] = aisbf_config.get('dashboard', {})
return templates.TemplateResponse(
request=request,
......@@ -10055,10 +10061,10 @@ async def v1_chat_completions(request: Request, body: ChatCompletionRequest):
# PATH 1: Direct provider model (format: {provider}/{model})
if provider_id not in config.providers:
raise HTTPException(
status_code=404,
detail=f"User autoselect '{actual_model}' not found. Available: {list(handler.user_autoselects.keys())}"
)
raise HTTPException(
status_code=404,
detail=f"Provider '{provider_id}' not found. Available: {list(config.providers.keys())}"
)
# Validate kiro credentials before processing request
provider_config = config.get_provider(provider_id)
......
......@@ -551,10 +551,18 @@ function renderProviderDetails(key) {
${CLAUDE_CLI_MODE ? `
<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>
<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
through the local claude binary instead of the HTTP API.
</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;">
<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/>.
<div class="alert alert-error">{{ error }}</div>
{% 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-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('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('database')"><i class="fas fa-database"></i> Database</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/>.
</div>
</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="section-title"><i class="fas fa-brain"></i> Internal Models</div>
......@@ -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="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">
<small style="color: #666; display: block; margin-top: 5px;">Enter a new password to change the admin dashboard password</small>
</div>
<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">
<small style="color: #666; display: block; margin-top: 5px;">Re-enter the new password to confirm</small>
</div>
<div class="form-group">
......@@ -1194,13 +1193,12 @@ async function checkTorStatus() {
// Check TOR status on page load
document.addEventListener('DOMContentLoaded', function() {
checkTorStatus();
// Refresh status every 30 seconds
setInterval(checkTorStatus, 30000);
// Load cache statistics
refreshCacheStats();
// Refresh cache stats every 10 seconds
setInterval(refreshCacheStats, 10000);
{% if warning == 'default_password' %}
switchTab('admin');
{% endif %}
});
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