{% 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 = ''; // Chunk size: 512KB chunks for maximum compatibility with restrictive proxies const CHUNK_SIZE = 512 * 1024; // 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 = `
Uploading file: 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 = `
Uploading file: ${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('/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) { statusEl.innerHTML = '
File uploaded successfully!
'; // 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; renderProvidersList(); return; } } } catch (e) { statusEl.innerHTML = `
Upload failed: ${e.message}
`; } } // 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 = '
Uploading CLI credentials: 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 = `
Uploading CLI credentials: ${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('/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 = '
CLI credentials saved to server!
'; return; } } } catch (e) { statusEl.innerHTML = `
Upload failed: ${e.message}
`; } } // 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 renderProvidersList() { const container = document.getElementById('providers-list'); if (!container) return; const allKeys = Object.keys(providersData); const filteredKeys = providerSearchFilter ? allKeys.filter(k => k.toLowerCase().includes(providerSearchFilter.toLowerCase()) || (providersData[k].name || '').toLowerCase().includes(providerSearchFilter.toLowerCase())) : allKeys; 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 = `${total} provider${total !== 1 ? 's' : ''}`; container.innerHTML = ''; if (pageKeys.length === 0) { container.innerHTML = '

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 = `
${isExpanded ? '▼' : '▶'} ${escHtmlAttr(key)} (${escHtmlAttr(provider.name || key)})
`; container.appendChild(providerItem); if (isExpanded) { renderProviderDetails(key); } }); } // 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}
`; } } } 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); // Ensure the key's page is visible const allKeys = Object.keys(providersData); const filteredKeys = providerSearchFilter ? allKeys.filter(k => k.toLowerCase().includes(providerSearchFilter.toLowerCase()) || (providersData[k].name || '').toLowerCase().includes(providerSearchFilter.toLowerCase())) : allKeys; 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: '~/.claude_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: '~/.kilo_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_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_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 = `

Kiro Authentication

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)
Option 1: Kiro IDE Credentials
Path to Kiro IDE credentials JSON file
Option 2: kiro-cli Database
Path to kiro-cli SQLite database
Option 3: Direct Credentials
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
Option 4: Upload Files
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 = `

Kilocode Authentication

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
Option 2: OAuth2 Authentication
If no API key is provided, OAuth2 will be used automatically.
Kilocode API base URL (fixed)
Path where OAuth2 credentials will be stored
Or Upload OAuth2 Credentials File
Upload Kilocode OAuth2 credentials JSON file
`; } else if (isClaudeProvider) { // Claude OAuth2 authentication fields authFieldsHtml = `

Claude OAuth2 Authentication

Claude uses OAuth2 authentication. Click "Authenticate" to start the OAuth2 flow in your browser.
Path where OAuth2 credentials will be stored
Or Upload Credentials File
Upload Claude OAuth2 credentials JSON file
${CLAUDE_CLI_MODE ? `
Claude 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.
Option 1: API Key (Recommended)
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")
Option 2: OAuth2 Authentication (DISCONTINUED - NOT WORKING)
⚠️ OAuth2 authentication is no longer functional. Qwen has discontinued this service. Use API Key instead.
Path where OAuth2 credentials will be stored
Or Upload OAuth2 Credentials File
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)
Path where OAuth2 credentials will be stored
Or Upload Credentials File
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}

Pricing Configuration

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

Native Caching

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

Models

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); } function renderModels(providerKey) { const container = document.getElementById(`models-${providerKey}`); const provider = providersData[providerKey]; if (!provider.models || provider.models.length === 0) { container.innerHTML = '

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 = `
Model ${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] || 'Standard provider configuration.'; // Show/hide Claude warning const claudeWarningEl = document.getElementById('new-provider-claude-warning'); if (providerType === 'claude') { claudeWarningEl.style.display = 'block'; } else { claudeWarningEl.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 = 'Checking Claude authentication status...'; try { const response = await fetch(`/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 = `✅ Claude authentication is valid. Expires in: ${expirationText}`; } else { statusDiv.style.background = '#f8d7da'; statusDiv.style.color = '#721c24'; statusDiv.innerHTML = `❌ Claude authentication failed: ${result.error || 'Not authenticated'}`; } } catch (error) { statusDiv.style.background = '#f8d7da'; statusDiv.style.color = '#721c24'; statusDiv.innerHTML = `❌ Error checking Claude auth: ${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 = 'Checking Qwen authentication status...'; try { const response = await fetch(`/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 = `✅ Qwen authentication is valid. Expires in: ${expirationText}`; } else { statusDiv.style.background = '#f8d7da'; statusDiv.style.color = '#721c24'; statusDiv.innerHTML = `❌ Qwen authentication failed: ${result.error || 'Not authenticated'}`; } } catch (error) { statusDiv.style.background = '#f8d7da'; statusDiv.style.color = '#721c24'; statusDiv.innerHTML = `❌ Error checking Qwen auth: ${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 = 'Checking Codex authentication status...'; try { const response = await fetch(`/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 = `✅ Codex authentication is valid. Expires in: ${expirationText}`; } else { statusDiv.style.background = '#f8d7da'; statusDiv.style.color = '#721c24'; statusDiv.innerHTML = `❌ Codex authentication failed: ${result.error || 'Not authenticated'}`; } } catch (error) { statusDiv.style.background = '#f8d7da'; statusDiv.style.color = '#721c24'; statusDiv.innerHTML = `❌ Error checking Codex auth: ${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 = 'Checking Kilocode authentication status...'; try { const response = await fetch(`/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 = `✅ Kilocode authentication is valid. Expires in: ${expirationText}`; } else { statusDiv.style.background = '#f8d7da'; statusDiv.style.color = '#721c24'; statusDiv.innerHTML = `❌ Kilocode authentication failed: ${result.error || 'Not authenticated'}`; } } catch (error) { statusDiv.style.background = '#f8d7da'; statusDiv.style.color = '#721c24'; statusDiv.innerHTML = `❌ Error checking Kilocode auth: ${error.message}`; } } // OAuth authentication functions async function authenticateClaude(key) { const statusEl = document.getElementById(`claude-auth-status-${key}`); statusEl.style.display = 'block'; statusEl.style.background = '#0f2840'; statusEl.style.border = '1px solid #4a9eff'; statusEl.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, credentials_file: providersData[key].claude_config?.credentials_file || '~/.claude_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 = `

✗ 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, credentials_file: providersData[key].qwen_config?.credentials_file || '~/.aisbf/qwen_credentials.json' }) }); 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)

`; 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('/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 = '

✓ 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, credentials_file: providersData[key].codex_config?.credentials_file || '~/.aisbf/codex_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 = `

✗ 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)

`; 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('/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 = '

✓ 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, credentials_file: providersData[key].kilo_config?.credentials_file || '~/.kilo_credentials.json' }) }); 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)

`; 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('/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 = '

✓ 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', creds_file: '', sqlite_db: '', refresh_token: '', profile_arn: '', client_id: '', client_secret: '' }; } else if (type === 'claude') { providersData[key].claude_config = { credentials_file: '~/.claude_credentials.json' }; } else if (type === 'kilocode') { providersData[key].kilo_config = { credentials_file: '~/.kilo_credentials.json', api_base: 'https://api.kilo.ai/api/gateway' }; } else if (type === 'qwen') { providersData[key].qwen_config = { credentials_file: '~/.aisbf/qwen_credentials.json', api_key: '', region: 'china-beijing', workspace_id: 'Default Workspace' }; } else if (type === 'codex') { providersData[key].codex_config = { credentials_file: '~/.aisbf/codex_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; } 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'); } } renderProvidersList(); {% endblock %}