{% extends "base.html" %} {% block title %}My Providers - AISBF Dashboard{% endblock %} {% block content %}
No providers found.
'; } else { pageKeys.forEach(key => { const provider = providersData[key]; const providerItem = document.createElement('div'); providerItem.className = 'provider-item'; providerItem.style.cssText = 'border: 1px solid #0f3460; margin-bottom: 10px; border-radius: 5px; background: #1a1a2e;'; const isExpanded = expandedProviders.has(key); const safeKey = key.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); providerItem.innerHTML = `.credentials.json
below to override.
~/.claude/.credentials.json
to use a specific account instead of your OAuth2 tokens.
Claude.ai policies state that unofficial clients are not allowed. By using AISBF in HTTP API / OAuth2 mode, you acknowledge that:
• While we do our best to mimic the claude-code CLI, using an unofficial client can lead to account suspension or cancellation
• Claude.ai detects prompt patterns to identify unofficial client usage, so it may not work with complex agents and prompts
• You use this software at your own risk and responsibility
• We are not affiliated with Anthropic and cannot guarantee compatibility or continued functionality
✅ CLI mode is different: when "Use Claude CLI mode" is enabled, AISBF proxies requests through the official claude binary using claude -p, which is the intended programmatic use of the official Anthropic CLI and is permitted by Claude's terms of service.
As of April 2026, Qwen has completely disabled OAuth2 subscriptions for Qwen Code.
OAuth2 tokens are no longer accepted by the DashScope API.
Please use API Key authentication instead.
OAuth2 support is maintained in the code for potential future re-enablement, but it is currently non-functional.
No models configured
'; return; } container.innerHTML = ''; provider.models.forEach((model, index) => { const modelDiv = document.createElement('div'); modelDiv.style.cssText = 'border: 1px solid #0f3460; padding: 15px; margin-bottom: 10px; border-radius: 3px; background: #1a1a2e;'; modelDiv.innerHTML = `🔄 Starting OAuth2 authentication flow...
'; try { const response = await fetch('/dashboard/claude/auth/start', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ provider_key: key }) }); const data = await response.json(); if (!data.success || !data.auth_url) { statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = `✗ Failed to start authentication: ${data.error || 'Unknown error'}
`; return; } statusEl.innerHTML = '🔄 Opening authentication window...
'; // Open OAuth2 URL in popup window const authWindow = window.open(data.auth_url, 'claude-auth', 'width=600,height=700'); statusEl.innerHTML = '🔄 Please complete authentication in the popup window...
'; // Poll for completion let pollCount = 0; const maxPolls = 60; let isCompleting = false; await new Promise(resolve => setTimeout(resolve, 8000)); const pollInterval = setInterval(async () => { pollCount++; try { const statusResponse = await fetch('/dashboard/claude/auth/callback-status'); const statusData = await statusResponse.json(); if (statusData.received) { clearInterval(pollInterval); if (authWindow && !authWindow.closed) { authWindow.close(); } if (statusData.error) { statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = `✗ OAuth2 error: ${statusData.error}
`; return; } if (isCompleting) return; isCompleting = true; await new Promise(resolve => setTimeout(resolve, 1000)); try { statusEl.innerHTML = '🔄 Completing authentication...
'; const completeResponse = await fetch('/dashboard/claude/auth/complete', { method: 'POST', headers: { 'Content-Type': 'application/json', } }); const completeData = await completeResponse.json(); if (completeData.success) { statusEl.style.background = '#0f4020'; statusEl.style.border = '1px solid #4eff9e'; statusEl.innerHTML = '✓ Authentication successful! Credentials saved.
'; } else { statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = `✗ Authentication incomplete. ${completeData.error || 'Please try again.'}
`; } } catch (error) { statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = `✗ Error completing authentication: ${error.message}
`; } } } catch (error) { console.error('Error checking callback status:', error); } if (pollCount >= maxPolls) { clearInterval(pollInterval); if (authWindow && !authWindow.closed) { authWindow.close(); } statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = '✗ Authentication timeout. Please try again.
'; } }, 2000); } catch (error) { statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = `✗ Error: ${error.message}
`; } } async function authenticateQwen(key) { const statusEl = document.getElementById(`qwen-auth-status-${key}`); statusEl.style.display = 'block'; statusEl.style.background = '#0f2840'; statusEl.style.border = '1px solid #4a9eff'; statusEl.innerHTML = '🔄 Starting Qwen OAuth2 Device Authorization flow...
'; try { const response = await fetch('/dashboard/qwen/auth/start', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ provider_key: key }) }); const data = await response.json(); if (!data.success) { statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = `✗ Failed to start authentication: ${data.error || 'Unknown error'}
`; return; } statusEl.style.background = '#0f2840'; statusEl.style.border = '1px solid #4a9eff'; statusEl.innerHTML = `🔐 Qwen Device Authorization
Please visit: ${data.verification_uri}
Enter code: ${data.user_code}
Waiting for authorization... (expires in ${Math.floor(data.expires_in / 60)} minutes)
✓ Qwen authentication successful! Credentials saved.
'; } else if (pollData.status === 'denied') { clearInterval(pollInterval); statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = '✗ Authorization denied by user.
'; } else if (pollData.status === 'expired') { clearInterval(pollInterval); statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = '✗ Authorization code expired. Please try again.
'; } } catch (error) { console.error('Error polling Qwen auth:', error); } if (pollCount >= maxPolls) { clearInterval(pollInterval); statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = '✗ Authentication timeout. Please try again.
'; } }, data.interval * 1000); } catch (error) { statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = `✗ Error: ${error.message}
`; } } async function pollQwenAuth(providerKey, deviceCode) { // This function is no longer needed as polling is handled in authenticateQwen } async function authenticateCodex(key) { const statusEl = document.getElementById(`codex-auth-status-${key}`); statusEl.style.display = 'block'; statusEl.style.background = '#0f2840'; statusEl.style.border = '1px solid #4a9eff'; statusEl.innerHTML = '🔄 Starting Codex OAuth2 Device Authorization flow...
'; try { const response = await fetch('/dashboard/codex/auth/start', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ provider_key: key, issuer: providersData[key].codex_config?.issuer || 'https://auth.openai.com' }) }); const data = await response.json(); if (!data.success) { statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = `✗ Failed to start authentication: ${data.error || 'Unknown error'}
`; return; } statusEl.style.background = '#0f2840'; statusEl.style.border = '1px solid #4a9eff'; statusEl.innerHTML = `🔐 Codex Device Authorization
Please visit: ${data.verification_uri}
Enter code: ${data.user_code}
Waiting for authorization... (expires in ${Math.floor(data.expires_in / 60)} minutes)
✓ Codex authentication successful! Credentials saved.
'; } else if (pollData.status === 'denied') { clearInterval(pollInterval); statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = '✗ Authorization denied by user.
'; } else if (pollData.status === 'expired') { clearInterval(pollInterval); statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = '✗ Authorization code expired. Please try again.
'; } } catch (error) { console.error('Error polling Codex auth:', error); } if (pollCount >= maxPolls) { clearInterval(pollInterval); statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = '✗ Authentication timeout. Please try again.
'; } }, data.interval * 1000); } catch (error) { statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = `✗ Error: ${error.message}
`; } } async function pollCodexAuth(providerKey, deviceCode) { // This function is no longer needed as polling is handled in authenticateCodex } async function authenticateKilo(key) { const statusEl = document.getElementById(`kilo-auth-status-${key}`); statusEl.style.display = 'block'; statusEl.style.background = '#0f2840'; statusEl.style.border = '1px solid #4a9eff'; statusEl.innerHTML = '🔄 Starting Kilocode OAuth2 Device Authorization flow...
'; try { const response = await fetch('/dashboard/kilo/auth/start', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ provider_key: key }) }); const data = await response.json(); if (!data.success) { statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = `✗ Failed to start authentication: ${data.error || 'Unknown error'}
`; return; } statusEl.style.background = '#0f2840'; statusEl.style.border = '1px solid #4a9eff'; statusEl.innerHTML = `🔐 Kilocode Device Authorization
Please visit: ${data.verification_uri}
Enter code: ${data.user_code}
Waiting for authorization... (expires in ${Math.floor(data.expires_in / 60)} minutes)
✓ Kilocode authentication successful! Credentials saved.
'; } else if (pollData.status === 'denied') { clearInterval(pollInterval); statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = '✗ Authorization denied by user.
'; } else if (pollData.status === 'expired') { clearInterval(pollInterval); statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = '✗ Authorization code expired. Please try again.
'; } } catch (error) { console.error('Error polling Kilo auth:', error); } if (pollCount >= maxPolls) { clearInterval(pollInterval); statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = '✗ Authentication timeout. Please try again.
'; } }, data.interval * 1000); } catch (error) { statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = `✗ Error: ${error.message}
`; } } async function pollKiloAuth(providerKey, deviceCode) { // This function is no longer needed as polling is handled in authenticateKilo } async function saveProvider(key) { const statusEl = document.getElementById(`save-status-${key}`); if (statusEl) statusEl.textContent = 'Saving…'; try { const result = await apiCall('POST', '/dashboard/api/provider', { provider_id: key, config: providersData[key] }); if (result.success) { if (statusEl) { statusEl.textContent = 'Saved!'; statusEl.style.color = '#4ade80'; setTimeout(() => { statusEl.textContent = ''; }, 3000); } } else { if (statusEl) { statusEl.textContent = 'Error: ' + (result.error || 'Unknown'); statusEl.style.color = '#f87171'; } } } catch (e) { if (statusEl) { statusEl.textContent = 'Error: ' + e.message; statusEl.style.color = '#f87171'; } } } async function removeProvider(key) { if (await showDangerConfirm(`Remove provider "${key}"?`, 'Remove Provider')) { try { const result = await apiCall('DELETE', '/dashboard/api/provider/' + encodeURIComponent(key)); if (!result.success) { showAlert('Error removing provider: ' + (result.error || 'Unknown'), 'Error', '❌', 'danger'); return; } } catch (e) { showAlert('Error removing provider: ' + e.message, 'Error', '❌', 'danger'); return; } delete providersData[key]; expandedProviders.delete(key); renderProvidersList(); } } function updateProvider(key, field, value) { providersData[key][field] = value; } function updateKiroConfig(key, field, value) { if (!providersData[key].kiro_config) { providersData[key].kiro_config = {}; } providersData[key].kiro_config[field] = value; } function updateClaudeConfig(key, field, value) { if (!providersData[key].claude_config) { providersData[key].claude_config = {}; } providersData[key].claude_config[field] = value; } function updateKiloConfig(key, field, value) { if (!providersData[key].kilo_config) { providersData[key].kilo_config = {}; } providersData[key].kilo_config[field] = value; } function updateQwenConfig(key, field, value) { if (!providersData[key].qwen_config) { providersData[key].qwen_config = {}; } providersData[key].qwen_config[field] = value; } function updateCodexConfig(key, field, value) { if (!providersData[key].codex_config) { providersData[key].codex_config = {}; } providersData[key].codex_config[field] = value; } function updateProviderType(key, value) { providersData[key].type = value; // Re-render to update the configuration fields renderProvidersList(); } function updateProviderCondenseMethod(key, value) { const trimmed = value.trim(); if (!trimmed) { providersData[key].default_condense_method = null; return; } // Check if it's a comma-separated list if (trimmed.includes(',')) { providersData[key].default_condense_method = trimmed.split(',').map(s => s.trim()).filter(s => s); } else { providersData[key].default_condense_method = trimmed; } } function addModel(key) { if (!providersData[key].models) { providersData[key].models = []; } providersData[key].models.push({ name: '', rate_limit: 0 }); renderModels(key); } async function removeModel(providerKey, index) { if (await showDangerConfirm('Remove this model?', 'Remove Model')) { providersData[providerKey].models.splice(index, 1); renderModels(providerKey); } } async function confirmAddProvider() { const key = document.getElementById('new-provider-key').value.trim(); const type = document.getElementById('new-provider-type').value; if (!key) { showAlert('Please enter a provider key', 'Missing Key', '⚠️', 'warn'); return; } if (providersData[key]) { showAlert('Provider key already exists', 'Duplicate Key', '⚠️', 'warn'); return; } // Create new provider with defaults providersData[key] = { id: key, name: key.charAt(0).toUpperCase() + key.slice(1), endpoint: getDefaultEndpoint(type), type: type, api_key_required: type !== 'ollama' && type !== 'kiro' && type !== 'claude' && type !== 'kilocode' && type !== 'qwen' && type !== 'codex', rate_limit: 0, models: [] }; // Add type-specific config if (type === 'kiro') { providersData[key].kiro_config = { region: 'us-east-1', refresh_token: '', profile_arn: '', client_id: '', client_secret: '' }; } else if (type === 'claude') { providersData[key].claude_config = {}; } else if (type === 'kilocode') { providersData[key].kilo_config = { api_base: 'https://api.kilo.ai/api/gateway' }; } else if (type === 'qwen') { providersData[key].qwen_config = { api_key: '', region: 'china-beijing', workspace_id: 'Default Workspace' }; } else if (type === 'codex') { providersData[key].codex_config = { issuer: 'https://auth.openai.com' }; } try { const result = await apiCall('POST', '/dashboard/api/provider', { provider_id: key, config: providersData[key] }); if (!result.success) { showAlert('Error creating provider: ' + (result.error || 'Unknown'), 'Error', '❌', 'danger'); delete providersData[key]; return; } } catch (e) { showAlert('Error creating provider: ' + e.message, 'Error', '❌', 'danger'); delete providersData[key]; return; } expandedProviders.add(key); cancelAddProvider(); renderProvidersList(); } function getDefaultEndpoint(type) { const defaults = { 'openai': 'https://api.openai.com/v1', 'google': 'https://generativelanguage.googleapis.com/v1beta', 'anthropic': 'https://api.anthropic.com/v1', 'ollama': 'http://localhost:11434/api', 'kiro': 'https://q.us-east-1.amazonaws.com', 'claude': 'https://api.anthropic.com/v1', 'kilocode': 'https://api.kilo.ai/api/gateway', 'qwen': 'https://dashscope.aliyuncs.com/compatible-mode/v1', 'codex': 'https://api.openai.com/v1' }; return defaults[type] || ''; } async function getModelsFromProvider(key) { const statusDiv = document.getElementById(`get-models-status-${key}`); if (!statusDiv) return; statusDiv.innerHTML = 'Fetching models...'; try { const response = await fetch('/dashboard/providers/get-models', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ provider_key: key, provider: providersData[key] }) }); const result = await response.json(); if (response.ok && result.models) { providersData[key].models = result.models; renderModels(key); statusDiv.innerHTML = `✅ Found ${result.models.length} models`; } else { statusDiv.innerHTML = `❌ Error: ${result.error || 'Failed to fetch models'}`; } } catch (error) { statusDiv.innerHTML = `❌ Error: ${error.message}`; } } function updateModel(providerKey, index, field, value) { providersData[providerKey].models[index][field] = value; } function updateModelCondenseMethod(providerKey, index, value) { const trimmed = value.trim(); if (!trimmed) { providersData[providerKey].models[index].condense_method = null; return; } // Check if it's a comma-separated list if (trimmed.includes(',')) { providersData[providerKey].models[index].condense_method = trimmed.split(',').map(s => s.trim()).filter(s => s); } else { providersData[providerKey].models[index].condense_method = trimmed; } } async function saveProviders() { try { const response = await fetch('/dashboard/providers', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: 'config=' + encodeURIComponent(JSON.stringify(providersData, null, 2)) }); if (response.ok) { window.location.href = '/dashboard/providers?success=1'; } else { showAlert('Error saving configuration', 'Error', '❌', 'danger'); } } catch (error) { showAlert('Error: ' + error.message, 'Error', '❌', 'danger'); } } async function initProviders() { await loadCacheSettings(); renderProvidersList(); } initProviders(); {% endblock %}