{% extends "base.html" %} {% block title %}My Providers - AISBF Dashboard{% endblock %} {% block content %}
Usage data unavailable.
'; let html = ''; if (usage.plan_type) { html += `No usage data available.
'; } async function fetchProviderUsage(key) { try { const resp = await fetch(`${BASE_PATH}/dashboard/api/provider/${encodeURIComponent(key)}/usage`); if (!resp.ok) return null; const data = await resp.json(); if (data.supported === false) return {supported: false}; _usageCache[key] = {data: data.usage, ts: Date.now()}; return data.usage; } catch(e) { return null; } } function updateUsageBadge(key) { const badge = document.getElementById(`usage-badge-${key}`); if (badge) { const c = _usageCache[key]; if (c && c.data) badge.innerHTML = _renderUsageCompact(c.data); } const full = document.getElementById(`usage-full-${key}`); if (full) { const c = _usageCache[key]; if (c && c.data) full.innerHTML = _renderUsageFull(c.data); } } async function refreshAllUsage() { for (const key of Object.keys(providersData)) { if (!_providerSupportsUsage(key)) continue; const c = _usageCache[key]; if (c && (Date.now() - c.ts) < USAGE_POLL_INTERVAL - 1000) continue; const data = await fetchProviderUsage(key); if (data && data !== null) updateUsageBadge(key); } } setInterval(refreshAllUsage, USAGE_POLL_INTERVAL); // Generic chunked upload handler for all providers async function uploadFileChunked(providerKey, fileType, file, configObject) { if (!file) return; // Ensure provider is expanded so status element exists if (!expandedProviders.has(providerKey)) { toggleProvider(providerKey); } // Map config object names to status element prefixes const statusPrefixMap = { 'kiro_config': 'kiro', 'kilo_config': 'kilo', 'claude_config': 'claude', 'qwen_config': 'qwen', 'codex_config': 'codex' }; const statusPrefix = statusPrefixMap[configObject] || configObject.replace('_config', ''); const statusEl = document.getElementById(`${statusPrefix}-upload-status-${providerKey}`); statusEl.innerHTML = `${savedPath}${window.i18n.t('providers.no_providers')}
`; } else { pageKeys.forEach(key => { const provider = providersData[key]; const providerItem = document.createElement('div'); providerItem.className = 'provider-item'; providerItem.dataset.sortKey = key; providerItem.style.cssText = 'border: 1px solid var(--color-border); margin-bottom: 10px; border-radius: 5px; background: var(--bg-page);'; const isExpanded = expandedProviders.has(key); const safeKey = key.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); const supportsUsage = _providerSupportsUsage(key); const cachedUsage = _usageCache[key]; const compactBadge = supportsUsage ? `
Claude's OAuth2 flow redirects your browser to http://localhost:54545/callback
to deliver the auth token. When you access AISBF remotely, that localhost URL goes to
your machine, not the server — so the token never reaches AISBF.
The AISBF Chrome Extension intercepts that localhost callback in your browser and
forwards it to this server, completing the OAuth2 flow transparently.
Once installed, it self-configures automatically — no setup needed.
Reload this page after installing and the Authenticate button will appear.
.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.
Loading usage data…
${window.i18n.t('providers.no_models')}
`; return; } container.innerHTML = ''; provider.models.forEach((model, index) => { const modelDiv = document.createElement('div'); modelDiv.style.cssText = 'border: 1px solid var(--color-border); padding: 15px; margin-bottom: 10px; border-radius: 3px; background: var(--bg-page);'; modelDiv.innerHTML = `🔄 Starting OAuth2 authentication flow...
'; try { const response = await fetch(BASE_PATH + '/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 = `${window.i18n.interpolate(window.i18n.t('providers.auth_start_failed'), {error: data.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(BASE_PATH + '/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(BASE_PATH + '/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 = `${window.i18n.interpolate(window.i18n.t('providers.auth_error_completing'), {error: 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 = `${window.i18n.t('providers.auth_timeout')}
`; } }, 2000); } catch (error) { statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = `${window.i18n.interpolate(window.i18n.t('providers.auth_generic_error'), {error: error.message})}
`; } } async function authenticateQwen(key) { const statusEl = document.getElementById(`qwen-auth-status-${key}`); statusEl.style.display = 'block'; statusEl.style.background = 'var(--bg-panel)'; statusEl.style.border = '1px solid #4a9eff'; statusEl.innerHTML = '🔄 Starting Qwen OAuth2 Device Authorization flow...
'; try { const response = await fetch(BASE_PATH + '/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 = `${window.i18n.interpolate(window.i18n.t('providers.auth_start_failed'), {error: data.error || ''})}
`; return; } statusEl.style.background = 'var(--bg-panel)'; 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)
${window.i18n.interpolate(window.i18n.t('providers.auth_success'), {provider: 'Qwen'})}
`; } else if (pollData.status === 'denied') { clearInterval(pollInterval); statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = `${window.i18n.t('providers.auth_denied')}
`; } else if (pollData.status === 'expired') { clearInterval(pollInterval); statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = `${window.i18n.t('providers.auth_expired')}
`; } } 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 = `${window.i18n.t('providers.auth_timeout')}
`; } }, data.interval * 1000); } catch (error) { statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = `${window.i18n.interpolate(window.i18n.t('providers.auth_generic_error'), {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 = 'var(--bg-panel)'; statusEl.style.border = '1px solid #4a9eff'; statusEl.innerHTML = '🔄 Starting Codex OAuth2 Device Authorization flow...
'; try { const response = await fetch(BASE_PATH + '/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 = `${window.i18n.interpolate(window.i18n.t('providers.auth_start_failed'), {error: data.error || ''})}
`; return; } statusEl.style.background = 'var(--bg-panel)'; 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)
${window.i18n.interpolate(window.i18n.t('providers.auth_success'), {provider: 'Codex'})}
`; } else if (pollData.status === 'denied') { clearInterval(pollInterval); statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = `${window.i18n.t('providers.auth_denied')}
`; } else if (pollData.status === 'expired') { clearInterval(pollInterval); statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = `${window.i18n.t('providers.auth_expired')}
`; } } 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 = `${window.i18n.t('providers.auth_timeout')}
`; } }, data.interval * 1000); } catch (error) { statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = `${window.i18n.interpolate(window.i18n.t('providers.auth_generic_error'), {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 = 'var(--bg-panel)'; statusEl.style.border = '1px solid #4a9eff'; statusEl.innerHTML = '🔄 Starting Kilocode OAuth2 Device Authorization flow...
'; try { const response = await fetch(BASE_PATH + '/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 = `${window.i18n.interpolate(window.i18n.t('providers.auth_start_failed'), {error: data.error || ''})}
`; return; } statusEl.style.background = 'var(--bg-panel)'; 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)
${window.i18n.interpolate(window.i18n.t('providers.auth_success'), {provider: 'Kilocode'})}
`; } else if (pollData.status === 'denied') { clearInterval(pollInterval); statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = `${window.i18n.t('providers.auth_denied')}
`; } else if (pollData.status === 'expired') { clearInterval(pollInterval); statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = `${window.i18n.t('providers.auth_expired')}
`; } } 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 = `${window.i18n.t('providers.auth_timeout')}
`; } }, data.interval * 1000); } catch (error) { statusEl.style.background = '#402010'; statusEl.style.border = '1px solid #ff4a4a'; statusEl.innerHTML = `${window.i18n.interpolate(window.i18n.t('providers.auth_generic_error'), {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 = window.i18n.t('providers.saving'); try { const result = await apiCall('POST', '/dashboard/api/provider', { provider_id: key, config: providersData[key] }); if (result.success) { if (statusEl) { statusEl.textContent = window.i18n.t('providers.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(window.i18n.interpolate(window.i18n.t('providers.remove_provider_confirm'), {key}), window.i18n.t('providers.remove_provider_title'))) { 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]; providerMasterOrder = providerMasterOrder.filter(k => k !== 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; const defaultEndpoint = getDefaultEndpoint(value); if (defaultEndpoint) { providersData[key].endpoint = defaultEndpoint; } // 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(window.i18n.t('providers.remove_model_confirm'), window.i18n.t('providers.remove_model_title'))) { 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(window.i18n.t('providers.missing_key'), window.i18n.t('providers.missing_key_title'), '⚠️', 'warn'); return; } if (providersData[key]) { showAlert(window.i18n.t('providers.duplicate_key'), window.i18n.t('providers.duplicate_key_title'), '⚠️', '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; } if (!providerMasterOrder.includes(key)) providerMasterOrder.push(key); 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 = window.i18n.t('providers.fetching_models'); try { const response = await fetch(BASE_PATH + '/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 = window.i18n.interpolate(window.i18n.t('providers.models_found'), {n: result.models.length}); } else { statusDiv.innerHTML = window.i18n.interpolate(window.i18n.t('providers.models_fetch_error'), {error: result.error || ''}); } } 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(BASE_PATH + '/dashboard/providers', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: (function() { const orderedData = {}; providerMasterOrder.forEach(k => { if (providersData[k]) orderedData[k] = providersData[k]; }); return 'config=' + encodeURIComponent(JSON.stringify(orderedData, null, 2)); })() }); if (response.ok) { window.location.href = BASE_PATH + '/dashboard/providers?success=1'; } else { showAlert(window.i18n.t('providers.error_saving'), 'Error', '❌', 'danger'); } } catch (error) { showAlert('Error: ' + error.message, 'Error', '❌', 'danger'); } } async function initProviders() { await loadCacheSettings(); renderProvidersList(); refreshAllUsage().then(() => renderProvidersList()); } // Defer until i18n is ready so translated strings are available function _doInitialRender() { initProviders(); } if (window.i18n) { _doInitialRender(); } else { document.addEventListener('i18n:ready', _doInitialRender, {once: true}); } {% endblock %}