{% extends "base.html" %} {% block title %}Providers - AISBF Dashboard{% endblock %} {% block content %}

Providers Configuration

{% if success %}
{{ success }}
{% endif %} {% if error %}
{{ error }}
{% endif %}
Cancel
{% endblock %} {% block extra_js %} ", "<\\/script>") | safe }}; let expandedProviders = new Set(); let currentProviderPage = 0; const PROVIDERS_PAGE_SIZE = 10; let providerSearchFilter = ''; let providerMasterOrder = Object.keys(providersData); let _providerDS = null; // Chunk size: 512KB chunks for maximum compatibility with restrictive proxies const CHUNK_SIZE = 512 * 1024; // ===== Provider Usage Indicators ===== const _usageCache = {}; // provider_id -> {data, ts} const USAGE_POLL_INTERVAL = 15000; // 15 seconds function _providerSupportsUsage(key) { const p = providersData[key]; return p && p.type === 'codex'; } function _fmtSeconds(s) { if (!s || s <= 0) return '0s'; const h = Math.floor(s/3600), m = Math.floor((s%3600)/60), sec = s%60; if (h > 0) return `${h}h${m > 0 ? ' '+m+'m' : ''}`; if (m > 0) return `${m}m${sec > 0 ? ' '+sec+'s' : ''}`; return `${sec}s`; } function _windowLabel(seconds) { if (!seconds || seconds <= 0) return 'Window'; if (seconds < 120) return 'Session'; const MIN = 60, HOUR = 3600, DAY = 86400, WEEK = 604800, MONTH = 2592000; if (seconds % MONTH === 0) return seconds === MONTH ? 'Monthly' : `${seconds/MONTH}mo`; if (seconds % WEEK === 0) return seconds === WEEK ? 'Weekly' : `${seconds/WEEK}wk`; if (seconds % DAY === 0) return seconds === DAY ? 'Daily' : `${seconds/DAY}d`; if (seconds % HOUR === 0) return seconds === HOUR ? 'Hourly' : `${seconds/HOUR}h`; if (seconds % MIN === 0) return `${seconds/MIN}min`; if (seconds >= MONTH * 0.9) return `${Math.round(seconds/MONTH)}mo`; if (seconds >= WEEK * 0.9) return 'Weekly'; if (seconds >= DAY) return `${Math.round(seconds/DAY)}d`; if (seconds >= HOUR) return `${Math.round(seconds/HOUR)}h`; return `${Math.round(seconds/MIN)}min`; } function _fmtResetAt(ts) { if (!ts) return ''; const diff = Math.round((new Date(ts * 1000) - Date.now()) / 1000); if (diff <= 0) return 'now'; return 'in ' + _fmtSeconds(diff); } function _usageBarHtml(pct, color) { const c = color || (pct >= 90 ? '#ef4444' : pct >= 70 ? '#f59e0b' : '#22c55e'); return `
`; } function _allWindows(rl) { const windows = []; if (rl.primary_window) windows.push(rl.primary_window); if (rl.secondary_window) windows.push(rl.secondary_window); if (Array.isArray(rl.additional_rate_limits)) windows.push(...rl.additional_rate_limits); return windows; } function _renderUsageCompact(usage) { if (!usage) return 'No data'; const parts = []; const plan = usage.plan_type ? `${escHtmlAttr(usage.plan_type)}` : ''; const rl = usage.rate_limit; if (rl) { _allWindows(rl).forEach(w => { const pct = w.used_percent || 0; let countStr = ''; if (w.num_requests_used != null && w.num_requests_limit != null) { countStr = ` (${w.num_requests_used.toLocaleString()}/${w.num_requests_limit.toLocaleString()})`; } parts.push(`
${_windowLabel(w.limit_window_seconds)}: ${_usageBarHtml(pct)} ${pct}%${countStr}
`); }); if (rl.limit_reached) { parts.push('LIMIT REACHED'); } } return `
${plan}${parts.join('')}
`; } function _renderUsageFull(usage) { if (!usage) return '

Usage data unavailable.

'; let html = ''; if (usage.plan_type) { html += `
${escHtmlAttr(usage.plan_type)}
`; } const rl = usage.rate_limit; if (rl) { if (rl.limit_reached) { html += `
Rate limit reached${usage.rate_limit_reached_type ? ': ' + escHtmlAttr(usage.rate_limit_reached_type) : ''}
`; } const renderWindow = (w) => { if (!w) return ''; const pct = w.used_percent || 0; const color = pct >= 90 ? '#ef4444' : pct >= 70 ? '#f59e0b' : '#22c55e'; const label = _windowLabel(w.limit_window_seconds); let countsHtml = ''; if (w.num_requests_used != null && w.num_requests_limit != null) { countsHtml += `Requests: ${w.num_requests_used.toLocaleString()} / ${w.num_requests_limit.toLocaleString()}`; } if (w.num_tokens_used != null && w.num_tokens_limit != null) { countsHtml += `Tokens: ${w.num_tokens_used.toLocaleString()} / ${w.num_tokens_limit.toLocaleString()}`; } return `
${label} ${pct}%
Window: ${_fmtSeconds(w.limit_window_seconds)} Resets ${_fmtResetAt(w.reset_at)} ${countsHtml}
`; }; _allWindows(rl).forEach(w => { html += renderWindow(w); }); } const cr = usage.credits; if (cr) { const bal = cr.balance != null ? escHtmlAttr(String(cr.balance)) : (cr.unlimited ? 'Unlimited' : 'None'); html += `
Credits: ${bal}
`; } return 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) { // Update collapsed badge const badge = document.getElementById(`usage-badge-${key}`); if (badge) { const cached = _usageCache[key]; if (cached && cached.data) badge.innerHTML = _renderUsageCompact(cached.data); } // Update expanded panel const full = document.getElementById(`usage-full-${key}`); if (full) { const cached = _usageCache[key]; if (cached && cached.data) full.innerHTML = _renderUsageFull(cached.data); } } async function refreshAllUsage() { for (const key of Object.keys(providersData)) { if (!_providerSupportsUsage(key)) continue; const cached = _usageCache[key]; if (cached && (Date.now() - cached.ts) < USAGE_POLL_INTERVAL - 1000) continue; const data = await fetchProviderUsage(key); if (data && data !== null) updateUsageBadge(key); } } // Start polling setInterval(refreshAllUsage, USAGE_POLL_INTERVAL); 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; max-width: calc(100vw - 16px); padding: 20px 30px; border-radius: 6px; box-shadow: 0 4px 16px rgba(0,0,0,0.4); ${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 = `${message}`; 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); } alertDiv.style.animation = 'slideDown 0.3s ease-out'; document.body.appendChild(alertDiv); setTimeout(() => { alertDiv.style.animation = 'slideUp 0.3s ease-out'; setTimeout(() => alertDiv.remove(), 300); }, 4000); } // 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 = `
${window.i18n.interpolate(window.i18n.t('providers.uploading_file'), {pct: 0})}
`; try { const totalChunks = Math.ceil(file.size / CHUNK_SIZE); for (let chunkNumber = 1; chunkNumber <= totalChunks; chunkNumber++) { const start = (chunkNumber - 1) * CHUNK_SIZE; const end = Math.min(start + CHUNK_SIZE, file.size); const chunk = file.slice(start, end); const progress = Math.round((chunkNumber / totalChunks) * 100); statusEl.innerHTML = `
${window.i18n.interpolate(window.i18n.t('providers.uploading_file'), {pct: progress})}
`; const formData = new FormData(); formData.append('provider_key', providerKey); formData.append('file_type', fileType); formData.append('file_name', file.name); formData.append('total_size', file.size); formData.append('chunk_number', chunkNumber); formData.append('total_chunks', totalChunks); formData.append('file', chunk); const response = await fetch(BASE_PATH + '/dashboard/providers/upload-auth-file/chunk', { method: 'POST', body: formData }); const result = await response.json(); if (!result.success) { throw new Error(result.error || 'Upload failed'); } // When upload is complete if (result.complete) { const savedPath = result.file_path || result.stored_filename || file.name; statusEl.innerHTML = `
Saved: ${savedPath}
`; showToast(`File uploaded successfully: ${savedPath}`, 'success'); // Update the config with the full file path (not just filename) if (!providersData[providerKey][configObject]) { providersData[providerKey][configObject] = {}; } // Use file_path from server response (tilde format: ~/.aisbf/...) providersData[providerKey][configObject][fileType] = result.file_path || result.stored_filename; setTimeout(() => renderProvidersList(), 2500); return; } } } catch (e) { statusEl.innerHTML = `
${window.i18n.interpolate(window.i18n.t('providers.upload_failed'), {error: e.message})}
`; showToast(`Upload failed: ${e.message}`, 'danger'); } } // Upload handler for Kiro credential files async function uploadKiroFile(providerKey, fileType, file) { await uploadFileChunked(providerKey, fileType, file, 'kiro_config'); } // Upload handler for Kilo credential files async function uploadKiloFile(providerKey, file) { await uploadFileChunked(providerKey, 'credentials_file', file, 'kilo_config'); } // Upload handler for Claude credential files async function uploadClaudeFile(providerKey, file) { await uploadFileChunked(providerKey, 'credentials_file', file, 'claude_config'); } // Upload handler for Claude CLI credentials (.credentials.json from ~/.claude/) async function uploadClaudeCliFile(providerKey, file) { if (!file) return; if (!expandedProviders.has(providerKey)) toggleProvider(providerKey); const statusEl = document.getElementById(`claude-cli-upload-status-${providerKey}`); if (!statusEl) return; statusEl.innerHTML = `
${window.i18n.interpolate(window.i18n.t('providers.uploading_cli'), {pct: 0})}
`; try { const totalChunks = Math.ceil(file.size / CHUNK_SIZE); for (let chunkNumber = 1; chunkNumber <= totalChunks; chunkNumber++) { const start = (chunkNumber - 1) * CHUNK_SIZE; const chunk = file.slice(start, Math.min(start + CHUNK_SIZE, file.size)); const progress = Math.round((chunkNumber / totalChunks) * 100); statusEl.innerHTML = `
${window.i18n.interpolate(window.i18n.t('providers.uploading_cli'), {pct: progress})}
`; const formData = new FormData(); formData.append('provider_key', providerKey); formData.append('file_type', 'cli_credentials'); formData.append('file_name', file.name); formData.append('total_size', file.size); formData.append('chunk_number', chunkNumber); formData.append('total_chunks', totalChunks); formData.append('file', chunk); const response = await fetch(BASE_PATH + '/dashboard/providers/upload-auth-file/chunk', { method: 'POST', body: formData }); const result = await response.json(); if (!result.success) throw new Error(result.error || 'Upload failed'); if (result.complete) { statusEl.innerHTML = `
${window.i18n.interpolate(window.i18n.t('providers.cli_creds_saved'), {name: file.name})}
`; showToast(window.i18n.interpolate(window.i18n.t('providers.cli_creds_saved'), {name: file.name}), 'success'); return; } } } catch (e) { statusEl.innerHTML = `
${window.i18n.interpolate(window.i18n.t('providers.upload_failed'), {error: e.message})}
`; showToast(`Upload failed: ${e.message}`, 'danger'); } } // Upload handler for Qwen credential files async function uploadQwenFile(providerKey, file) { await uploadFileChunked(providerKey, 'credentials_file', file, 'qwen_config'); } // Upload handler for Codex credential files async function uploadCodexFile(providerKey, file) { await uploadFileChunked(providerKey, 'credentials_file', file, 'codex_config'); } function _providerFilteredKeys() { return providerSearchFilter ? providerMasterOrder.filter(k => k.toLowerCase().includes(providerSearchFilter.toLowerCase()) || (providersData[k] && (providersData[k].name || '').toLowerCase().includes(providerSearchFilter.toLowerCase()))) : providerMasterOrder.filter(k => k in providersData); } function renderProvidersList() { const container = document.getElementById('providers-list'); if (!container) return; const filteredKeys = _providerFilteredKeys(); const total = filteredKeys.length; const totalPages = Math.max(1, Math.ceil(total / PROVIDERS_PAGE_SIZE)); if (currentProviderPage >= totalPages) currentProviderPage = totalPages - 1; const pageKeys = filteredKeys.slice(currentProviderPage * PROVIDERS_PAGE_SIZE, (currentProviderPage + 1) * PROVIDERS_PAGE_SIZE); const countEl = document.getElementById('providers-count'); if (countEl) countEl.textContent = window.i18n.interpolate(window.i18n.t(total !== 1 ? 'providers.provider_count_plural' : 'providers.provider_count_singular'), {n: total}); // Cross-page sentinel zones (before + after list) const hasPrev = totalPages > 1 && currentProviderPage > 0; const hasNext = totalPages > 1 && currentProviderPage < totalPages - 1; container.innerHTML = `
⬆ Drop here to move to previous page
`; if (pageKeys.length === 0) { container.innerHTML += `

${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 ? `
${cachedUsage && cachedUsage.data ? _renderUsageCompact(cachedUsage.data) : 'loading…'}
` : ''; providerItem.innerHTML = `
${isExpanded ? '▼' : '▶'} ${escHtmlAttr(key)} (${escHtmlAttr(provider.name || key)}) ${compactBadge}
`; container.appendChild(providerItem); if (isExpanded) { renderProviderDetails(key); } }); } const sentinelBot = document.createElement('div'); sentinelBot.id = 'providers-list-page-next'; sentinelBot.className = 'ds-sentinel' + (hasNext ? ' ds-visible' : ''); sentinelBot.textContent = '⬇ Drop here to move to next page'; container.appendChild(sentinelBot); // Pagination controls const paginationEl = document.getElementById('providers-pagination'); if (paginationEl) { if (totalPages <= 1) { paginationEl.innerHTML = ''; } else { let btns = ''; btns += ``; const start = Math.max(0, currentProviderPage - 2); const end = Math.min(totalPages - 1, currentProviderPage + 2); for (let p = start; p <= end; p++) { btns += ``; } btns += ``; paginationEl.innerHTML = `
${btns} Page ${currentProviderPage + 1} of ${totalPages}
`; } } // Init / re-attach DragSort if (!_providerDS) { _providerDS = new DragSort({ containerId: 'providers-list', masterOrder: { value: providerMasterOrder }, onReorder: function(newOrder) { providerMasterOrder = newOrder; renderProvidersList(); fetch(BASE_PATH + '/dashboard/api/provider/reorder', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({order: newOrder}) }).catch(function(){}); }, pagination: { getCurrentPage: function() { return currentProviderPage; }, getTotalPages: function() { return Math.max(1, Math.ceil(_providerFilteredKeys().length / PROVIDERS_PAGE_SIZE)); }, goToPage: function(p) { goToProviderPage(p); }, pageSize: PROVIDERS_PAGE_SIZE, getFilteredKeys: _providerFilteredKeys } }); } else { _providerDS._opts.masterOrder.value = providerMasterOrder; _providerDS.attach(); } } function goToProviderPage(page) { currentProviderPage = page; expandedProviders.clear(); renderProvidersList(); window.scrollTo(0, 0); } function toggleProvider(key) { if (expandedProviders.has(key)) { expandedProviders.delete(key); } else { expandedProviders.add(key); const filteredKeys = _providerFilteredKeys(); const idx = filteredKeys.indexOf(key); if (idx >= 0) currentProviderPage = Math.floor(idx / PROVIDERS_PAGE_SIZE); } renderProvidersList(); } function renderProviderDetails(key) { const container = document.getElementById(`provider-details-${key}`); const provider = providersData[key]; const safeKey = key.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); const isKiroProvider = provider.type === 'kiro'; const isClaudeProvider = provider.type === 'claude'; const isKiloProvider = provider.type === 'kilocode'; const isQwenProvider = provider.type === 'qwen'; const isCodexProvider = provider.type === 'codex'; // Initialize kiro_config if this is a kiro provider and doesn't have it if (isKiroProvider && !provider.kiro_config) { provider.kiro_config = { region: 'us-east-1', creds_file: '', sqlite_db: '', refresh_token: '', profile_arn: '', client_id: '', client_secret: '' }; } // Initialize claude_config if this is a claude provider and doesn't have it if (isClaudeProvider && !provider.claude_config) { provider.claude_config = { credentials_file: `~/.aisbf/claude_${key}_credentials.json` }; } // Initialize kilo_config if this is a kilocode provider and doesn't have it if (isKiloProvider && !provider.kilo_config) { provider.kilo_config = { credentials_file: `~/.aisbf/kilo_${key}_credentials.json`, api_base: 'https://api.kilo.ai/api/gateway' }; } // Initialize qwen_config if this is a qwen provider and doesn't have it if (isQwenProvider && !provider.qwen_config) { provider.qwen_config = { credentials_file: `~/.aisbf/qwen_${key}_credentials.json`, api_key: '', region: 'china-beijing', workspace_id: 'Default Workspace' }; } // Initialize codex_config if this is a codex provider and doesn't have it if (isCodexProvider && !provider.codex_config) { provider.codex_config = { credentials_file: `~/.aisbf/codex_${key}_credentials.json`, issuer: 'https://auth.openai.com' }; } const kiroConfig = provider.kiro_config || {}; const claudeConfig = provider.claude_config || {}; const kiloConfig = provider.kilo_config || {}; const qwenConfig = provider.qwen_config || {}; const codexConfig = provider.codex_config || {}; // Build authentication fields based on provider type let authFieldsHtml = ''; if (isKiroProvider) { // Kiro-specific authentication fields authFieldsHtml = `

${window.i18n.t('providers.kiro_auth_section')}

Choose one authentication method: Kiro IDE credentials (creds_file), kiro-cli database (sqlite_db), or direct credentials (refresh_token + client_id/secret).
AWS region for Kiro API (default: us-east-1)
${window.i18n.t('providers.kiro_opt1')}
Path to Kiro IDE credentials JSON file
${window.i18n.t('providers.kiro_opt2')}
Path to kiro-cli SQLite database
${window.i18n.t('providers.kiro_opt3')}
Kiro refresh token for direct authentication
AWS CodeWhisperer profile ARN (optional)
OAuth client ID for AWS SSO OIDC authentication
OAuth client secret for AWS SSO OIDC authentication
${window.i18n.t('providers.kiro_opt4')}
Upload Kiro IDE credentials JSON file
Upload kiro-cli SQLite database file
`; } else if (isKiloProvider) { // Kilocode authentication fields - supports both API key and OAuth2 authFieldsHtml = `

${window.i18n.t('providers.kilo_auth_section')}

Choose your authentication method: API Key (recommended for simplicity) or OAuth2 Device Authorization Grant.
Option 1: API Key / Auth Token
If provided, API key authentication will be used instead of OAuth2
${window.i18n.t('providers.kilo_opt2')}
If no API key is provided, OAuth2 will be used automatically.
Kilocode API base URL (fixed)
${window.i18n.t('providers.credentials_file_desc')}
${window.i18n.t('providers.upload_credentials_title')}
Upload Kilocode OAuth2 credentials JSON file
`; } else if (isClaudeProvider) { // Claude OAuth2 authentication fields // Determine extension state: local clients don't need it; remote clients need it installed const extensionInstalled = !!window.aisbfOAuth2Extension; const oauth2Available = IS_LOCAL_CLIENT || extensionInstalled; authFieldsHtml = `

Claude OAuth2 Authentication

Claude uses OAuth2 authentication. Click "Authenticate" to start the OAuth2 flow in your browser. ${!IS_LOCAL_CLIENT && !extensionInstalled ? `
🧩 Chrome Extension Required

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.

⬇️ Download Chrome Extension (.zip) In Chrome: go to chrome://extensions, enable Developer Mode, click "Load unpacked", and select the extracted folder.
` : ''} ${oauth2Available ? `
${window.i18n.t('providers.credentials_file_desc')}
${window.i18n.t('providers.upload_credentials_title')}
${window.i18n.t('providers.upload_credentials_desc')}
` : ` `} ${CLAUDE_CLI_MODE ? `
${window.i18n.t('providers.cli_mode_active')}
The claude CLI was detected at startup. When enabled, requests are piped through the local claude binary instead of the HTTP API.
⚠ Experimental: 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.
When checked and you have already authenticated via OAuth2 above, your existing tokens are used automatically — no separate file upload needed. You can also upload an explicit .credentials.json below to override.
Optional — upload a ~/.claude/.credentials.json to use a specific account. Path saved in claude_config.cli_credentials_file.
` : ''}

⚠️ Important Notice

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.

`; } else if (isQwenProvider) { // Qwen authentication fields - supports both API key and OAuth2 authFieldsHtml = `

⚠️ Qwen OAuth2 Service Discontinued

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.

Qwen Authentication

Choose your authentication method: API Key (recommended for simplicity) or OAuth2 Device Authorization Grant.
${window.i18n.t('providers.kilo_opt1')}
If provided, API key authentication will be used instead of OAuth2
Region Configuration (API Key Mode)
Select your preferred region for API key authentication. Different regions have different endpoints.
Select the region for your Qwen API endpoint
Workspace ID for Germany region (default: "Default Workspace")
${window.i18n.t('providers.qwen_opt2_discontinued')}
⚠️ OAuth2 authentication is no longer functional. Qwen has discontinued this service. Use API Key instead.
${window.i18n.t('providers.credentials_file_desc')}
${window.i18n.t('providers.upload_credentials_title')}
Upload Qwen OAuth2 credentials JSON file
`; } else if (isCodexProvider) { // Codex OAuth2 authentication fields authFieldsHtml = `

Codex OAuth2 Authentication

Codex uses OAuth2 Device Authorization Grant (same protocol as OpenAI). Click "Authenticate" to start the OAuth2 flow.
If provided, API key authentication will be used instead of OAuth2
OAuth2 issuer URL (default: https://auth.openai.com)
${window.i18n.t('providers.credentials_file_desc')}
Usage & Quota

Loading usage data…

${window.i18n.t('providers.upload_credentials_title')}
Upload Codex OAuth2 credentials JSON file
`; } else { // Standard API key authentication fields authFieldsHtml = `
`; } container.innerHTML = `
${isKiroProvider ? 'Typically: https://q.us-east-1.amazonaws.com' : ''} ${provider.type === 'kilocode' ? 'Fixed endpoint for Kilocode OAuth2 provider' : ''} ${isQwenProvider ? 'Fixed endpoint for Qwen OAuth2 provider (https://dashscope.aliyuncs.com/compatible-mode/v1)' : ''} ${isClaudeProvider ? 'Fixed endpoint for Claude OAuth2 provider (https://api.anthropic.com/v1)' : ''} ${isCodexProvider ? 'Fixed endpoint for Codex OAuth2 provider (https://api.openai.com/v1)' : ''}
${authFieldsHtml}

${window.i18n.t('providers.pricing_section')}

If checked, this provider is subscription-based and costs will be calculated as $0. Usage is still tracked for analytics.
Leave empty to use default pricing. Examples: OpenAI GPT-4: $10, Anthropic Claude: $15, Google Gemini: $1.25
Leave empty to use default pricing. Examples: OpenAI GPT-4: $30, Anthropic Claude: $75, Google Gemini: $5.00
Time delay between requests to this provider
Default token limit per minute for models in this provider
Default token limit per hour for models in this provider
Default token limit per day for models in this provider

${window.i18n.t('providers.native_caching_section')}

Provider-native caching features (Anthropic cache_control, Google Context Caching, OpenAI and Kilo-compatible APIs) for cost reduction.
Enable provider-native caching for cost reduction (50-70% savings for supported providers)
Cache time-to-live in seconds (Google Context Caching only)
Minimum token count for content to be cacheable (default: 1000)
Optional cache key for OpenAI/Kilo load balancer routing optimization

${window.i18n.t('providers.models_section')}

Configure specific models for this provider, or leave empty to automatically fetch all available models from the provider's API.
When no models are manually configured, only expose models whose ID contains this filter word (case-insensitive wildcard matching).
Example: "free" will match models like "model-name-free", "free-model", "gpt-4-free-tier", etc.
`; renderModels(key); // Fetch full usage data for codex providers when panel is expanded if (isCodexProvider) { const cached = _usageCache[key]; if (cached && cached.data) { const fullEl = document.getElementById(`usage-full-${key}`); if (fullEl) fullEl.innerHTML = _renderUsageFull(cached.data); } else { fetchProviderUsage(key).then(data => { const fullEl = document.getElementById(`usage-full-${key}`); if (fullEl) fullEl.innerHTML = _renderUsageFull(data); updateUsageBadge(key); }); } } } function renderModels(providerKey) { const container = document.getElementById(`models-${providerKey}`); const provider = providersData[providerKey]; if (!provider.models || provider.models.length === 0) { container.innerHTML = `

${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 = `
${window.i18n.t('providers.model_label')} ${index + 1}
`; container.appendChild(modelDiv); }); } function showAddProviderForm() { document.getElementById('new-provider-form').style.display = 'block'; document.getElementById('add-provider-btn').style.display = 'none'; document.getElementById('new-provider-key').value = ''; document.getElementById('new-provider-type').value = 'openai'; updateNewProviderDefaults(); document.getElementById('new-provider-key').focus(); } function cancelAddProvider() { document.getElementById('new-provider-form').style.display = 'none'; document.getElementById('add-provider-btn').style.display = 'inline-block'; } function updateNewProviderDefaults() { const providerType = document.getElementById('new-provider-type').value; const descriptionEl = document.getElementById('new-provider-type-description'); const descriptions = { 'openai': 'OpenAI-compatible provider. Uses standard API key authentication. Endpoint: https://api.openai.com/v1', 'google': 'Google AI provider (Gemini). Uses API key authentication. Endpoint: https://generativelanguage.googleapis.com/v1beta', 'anthropic': 'Anthropic provider (Claude). Uses API key authentication. Endpoint: https://api.anthropic.com/v1', 'ollama': 'Ollama local provider. No API key required by default. Endpoint: http://localhost:11434/api', 'kiro': 'Kiro (Amazon Q Developer) provider. Uses Kiro credentials (IDE, CLI, or direct tokens). Endpoint: https://q.us-east-1.amazonaws.com', 'claude': 'Claude Code provider. Uses OAuth2 authentication (browser-based login). Endpoint: https://api.anthropic.com/v1', 'kilocode': 'Kilocode provider. Uses OAuth2 Device Authorization Grant. Endpoint: https://api.kilo.ai/api/gateway', 'qwen': 'Qwen provider. Uses OAuth2 Device Authorization Grant or API key. Endpoint: https://dashscope.aliyuncs.com/compatible-mode/v1', 'codex': 'Codex provider. Uses OAuth2 Device Authorization Grant (same protocol as OpenAI). Endpoint: https://api.openai.com/v1' }; descriptionEl.textContent = descriptions[providerType] || window.i18n.t('providers.standard_config'); // Show/hide Claude warning const claudeWarningEl = document.getElementById('new-provider-claude-warning'); if (providerType === 'claude') { claudeWarningEl.style.display = 'block'; } else { claudeWarningEl.style.display = 'none'; } // Show extension download prompt when Claude is selected on a non-local client without the extension const extensionPromptEl = document.getElementById('new-provider-extension-prompt'); if (extensionPromptEl) { if (providerType === 'claude' && !IS_LOCAL_CLIENT && !window.aisbfOAuth2Extension) { const dlLink = document.getElementById('new-provider-extension-download'); if (dlLink) dlLink.href = BASE_PATH + '/dashboard/extension/download'; extensionPromptEl.style.display = 'block'; } else { extensionPromptEl.style.display = 'none'; } } } function togglePricingFields(key) { const provider = providersData[key]; const pricingFields = document.getElementById(`pricing-fields-${key}`); if (pricingFields) { pricingFields.style.display = provider.is_subscription ? 'none' : 'block'; } } // Helper function to format expiration time function formatExpirationTime(expiresAt) { if (!expiresAt) return 'Unknown'; const now = Math.floor(Date.now() / 1000); const secondsRemaining = expiresAt - now; if (secondsRemaining <= 0) { return 'Expired'; } const days = Math.floor(secondsRemaining / 86400); const hours = Math.floor((secondsRemaining % 86400) / 3600); const minutes = Math.floor((secondsRemaining % 3600) / 60); const seconds = secondsRemaining % 60; if (days > 0) { return `${days} day${days > 1 ? 's' : ''} ${hours} hour${hours !== 1 ? 's' : ''}`; } else if (hours > 0) { return `${hours} hour${hours > 1 ? 's' : ''} ${minutes} minute${minutes !== 1 ? 's' : ''}`; } else if (minutes > 0) { return `${minutes} minute${minutes > 1 ? 's' : ''} ${seconds} second${seconds !== 1 ? 's' : ''}`; } else { return `${seconds} second${seconds > 1 ? 's' : ''}`; } } // OAuth authentication check functions async function checkClaudeAuth(providerKey) { const statusDiv = document.getElementById(`claude-auth-status-${providerKey}`); if (!statusDiv) return; statusDiv.style.display = 'block'; statusDiv.style.background = '#f0f0f0'; statusDiv.style.color = '#333'; statusDiv.innerHTML = window.i18n.interpolate(window.i18n.t('providers.checking_auth'), {provider: 'Claude'}); try { const response = await fetch(`${BASE_PATH}/dashboard/providers/${providerKey}/auth/check`, { method: 'GET', headers: { 'Content-Type': 'application/json', } }); const result = await response.json(); if (response.ok && result.authenticated) { const expirationText = result.expires_at ? formatExpirationTime(result.expires_at) : 'Unknown'; statusDiv.style.background = '#d4edda'; statusDiv.style.color = '#155724'; statusDiv.innerHTML = window.i18n.interpolate(window.i18n.t('providers.auth_valid'), {provider: 'Claude', expiry: expirationText}); } else { statusDiv.style.background = '#f8d7da'; statusDiv.style.color = '#721c24'; statusDiv.innerHTML = window.i18n.interpolate(window.i18n.t('providers.auth_failed'), {provider: 'Claude', error: result.error || window.i18n.t('providers.not_authenticated')}); } } catch (error) { statusDiv.style.background = '#f8d7da'; statusDiv.style.color = '#721c24'; statusDiv.innerHTML = window.i18n.interpolate(window.i18n.t('providers.auth_error'), {provider: 'Claude', error: error.message}); } } async function checkQwenAuth(providerKey) { const statusDiv = document.getElementById(`qwen-auth-status-${providerKey}`); if (!statusDiv) return; statusDiv.style.display = 'block'; statusDiv.style.background = '#f0f0f0'; statusDiv.style.color = '#333'; statusDiv.innerHTML = window.i18n.interpolate(window.i18n.t('providers.checking_auth'), {provider: 'Qwen'}); try { const response = await fetch(`${BASE_PATH}/dashboard/providers/${providerKey}/auth/check`, { method: 'GET', headers: { 'Content-Type': 'application/json', } }); const result = await response.json(); if (response.ok && result.authenticated) { const expirationText = result.expires_at ? formatExpirationTime(result.expires_at) : 'Unknown'; statusDiv.style.background = '#d4edda'; statusDiv.style.color = '#155724'; statusDiv.innerHTML = window.i18n.interpolate(window.i18n.t('providers.auth_valid'), {provider: 'Qwen', expiry: expirationText}); } else { statusDiv.style.background = '#f8d7da'; statusDiv.style.color = '#721c24'; statusDiv.innerHTML = window.i18n.interpolate(window.i18n.t('providers.auth_failed'), {provider: 'Qwen', error: result.error || window.i18n.t('providers.not_authenticated')}); } } catch (error) { statusDiv.style.background = '#f8d7da'; statusDiv.style.color = '#721c24'; statusDiv.innerHTML = window.i18n.interpolate(window.i18n.t('providers.auth_error'), {provider: 'Qwen', error: error.message}); } } async function checkCodexAuth(providerKey) { const statusDiv = document.getElementById(`codex-auth-status-${providerKey}`); if (!statusDiv) return; statusDiv.style.display = 'block'; statusDiv.style.background = '#f0f0f0'; statusDiv.style.color = '#333'; statusDiv.innerHTML = window.i18n.interpolate(window.i18n.t('providers.checking_auth'), {provider: 'Codex'}); try { const response = await fetch(`${BASE_PATH}/dashboard/providers/${providerKey}/auth/check`, { method: 'GET', headers: { 'Content-Type': 'application/json', } }); const result = await response.json(); if (response.ok && result.authenticated) { const expirationText = result.expires_at ? formatExpirationTime(result.expires_at) : 'Unknown'; statusDiv.style.background = '#d4edda'; statusDiv.style.color = '#155724'; statusDiv.innerHTML = window.i18n.interpolate(window.i18n.t('providers.auth_valid'), {provider: 'Codex', expiry: expirationText}); } else { statusDiv.style.background = '#f8d7da'; statusDiv.style.color = '#721c24'; statusDiv.innerHTML = window.i18n.interpolate(window.i18n.t('providers.auth_failed'), {provider: 'Codex', error: result.error || window.i18n.t('providers.not_authenticated')}); } } catch (error) { statusDiv.style.background = '#f8d7da'; statusDiv.style.color = '#721c24'; statusDiv.innerHTML = window.i18n.interpolate(window.i18n.t('providers.auth_error'), {provider: 'Codex', error: error.message}); } } async function checkKiloAuth(providerKey) { const statusDiv = document.getElementById(`kilo-auth-status-${providerKey}`); if (!statusDiv) return; statusDiv.style.display = 'block'; statusDiv.style.background = '#f0f0f0'; statusDiv.style.color = '#333'; statusDiv.innerHTML = window.i18n.interpolate(window.i18n.t('providers.checking_auth'), {provider: 'Kilocode'}); try { const response = await fetch(`${BASE_PATH}/dashboard/providers/${providerKey}/auth/check`, { method: 'GET', headers: { 'Content-Type': 'application/json', } }); const result = await response.json(); if (response.ok && result.authenticated) { const expirationText = result.expires_at ? formatExpirationTime(result.expires_at) : 'Unknown'; statusDiv.style.background = '#d4edda'; statusDiv.style.color = '#155724'; statusDiv.innerHTML = window.i18n.interpolate(window.i18n.t('providers.auth_valid'), {provider: 'Kilocode', expiry: expirationText}); } else { statusDiv.style.background = '#f8d7da'; statusDiv.style.color = '#721c24'; statusDiv.innerHTML = window.i18n.interpolate(window.i18n.t('providers.auth_failed'), {provider: 'Kilocode', error: result.error || window.i18n.t('providers.not_authenticated')}); } } catch (error) { statusDiv.style.background = '#f8d7da'; statusDiv.style.color = '#721c24'; statusDiv.innerHTML = window.i18n.interpolate(window.i18n.t('providers.auth_error'), {provider: 'Kilocode', error: error.message}); } } // OAuth authentication functions async function authenticateClaude(key) { const statusEl = document.getElementById(`claude-auth-status-${key}`); statusEl.style.display = 'block'; statusEl.style.background = 'var(--bg-panel)'; statusEl.style.border = '1px solid #4a9eff'; statusEl.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, credentials_file: providersData[key].claude_config?.credentials_file || `~/.aisbf/claude_${key}_credentials.json` }) }); 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, credentials_file: providersData[key].qwen_config?.credentials_file || `~/.aisbf/qwen_${key}_credentials.json` }) }); 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)

`; try { window.open(data.verification_uri, 'qwen-auth', 'width=600,height=700'); } catch (e) { console.error('Could not open auth window:', e); } let pollCount = 0; const maxPolls = Math.floor(data.expires_in / data.interval); const pollInterval = setInterval(async () => { pollCount++; try { const pollResponse = await fetch(BASE_PATH + '/dashboard/qwen/auth/poll', { method: 'POST', headers: { 'Content-Type': 'application/json', } }); const pollData = await pollResponse.json(); if (pollData.status === 'completed') { clearInterval(pollInterval); statusEl.style.background = '#0f4020'; statusEl.style.border = '1px solid #4eff9e'; statusEl.innerHTML = `

${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, credentials_file: providersData[key].codex_config?.credentials_file || `~/.aisbf/codex_${key}_credentials.json`, 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)

`; try { window.open(data.verification_uri, 'codex-auth', 'width=600,height=700'); } catch (e) { console.error('Could not open auth window:', e); } let pollCount = 0; const maxPolls = Math.floor(data.expires_in / data.interval); const pollInterval = setInterval(async () => { pollCount++; try { const pollResponse = await fetch(BASE_PATH + '/dashboard/codex/auth/poll', { method: 'POST', headers: { 'Content-Type': 'application/json', } }); const pollData = await pollResponse.json(); if (pollData.status === 'approved') { clearInterval(pollInterval); statusEl.style.background = '#0f4020'; statusEl.style.border = '1px solid #4eff9e'; statusEl.innerHTML = `

${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, credentials_file: providersData[key].kilo_config?.credentials_file || `~/.aisbf/kilo_${key}_credentials.json` }) }); 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)

`; try { window.open(data.verification_uri, 'kilo-auth', 'width=600,height=700'); } catch (e) { console.error('Could not open auth window:', e); } let pollCount = 0; const maxPolls = Math.floor(data.expires_in / data.interval); const pollInterval = setInterval(async () => { pollCount++; try { const pollResponse = await fetch(BASE_PATH + '/dashboard/kilo/auth/poll', { method: 'POST', headers: { 'Content-Type': 'application/json', } }); const pollData = await pollResponse.json(); if (pollData.status === 'completed') { clearInterval(pollInterval); statusEl.style.background = '#0f4020'; statusEl.style.border = '1px solid #4eff9e'; statusEl.innerHTML = `

${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', creds_file: '', sqlite_db: '', refresh_token: '', profile_arn: '', client_id: '', client_secret: '' }; } else if (type === 'claude') { providersData[key].claude_config = { credentials_file: `~/.aisbf/claude_${key}_credentials.json` }; } else if (type === 'kilocode') { providersData[key].kilo_config = { credentials_file: `~/.aisbf/kilo_${key}_credentials.json`, api_base: 'https://api.kilo.ai/api/gateway' }; } else if (type === 'qwen') { providersData[key].qwen_config = { credentials_file: `~/.aisbf/qwen_${key}_credentials.json`, api_key: '', region: 'china-beijing', workspace_id: 'Default Workspace' }; } else if (type === 'codex') { providersData[key].codex_config = { credentials_file: `~/.aisbf/codex_${key}_credentials.json`, issuer: 'https://auth.openai.com' }; } // Immediately persist the new provider 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) { sessionStorage.setItem('providers_page', currentProviderPage); 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'); } } // Defer until i18n is ready so translated strings are available function _doInitialRender() { const savedPage = sessionStorage.getItem('providers_page'); if (savedPage !== null) { currentProviderPage = parseInt(savedPage) || 0; sessionStorage.removeItem('providers_page'); } renderProvidersList(); // Kick off initial usage fetch for all usage-supporting providers refreshAllUsage().then(() => renderProvidersList()); } if (window.i18n) { _doInitialRender(); } else { document.addEventListener('i18n:ready', _doInitialRender, {once: true}); } {% endblock %}