// ========================================== // API Functions // ========================================== async function fetchModels(showInModal = true) { const btn = showInModal ? document.getElementById('fetchModelsBtn') : document.getElementById('quickFetchBtn'); if (btn) { btn.dataset.originalText = btn.innerHTML; btn.innerHTML = '⏳'; btn.disabled = true; } try { let models = []; if (Backend.isManaged) { // Managed mode: fetch admin-curated enabled models const data = await Backend.listEnabledModels(); models = (data.models || []).map(m => ({ id: m.model_id || m.id, owned_by: m.provider_name || m.provider || null, config_id: m.config_id || null })); // Fallback: if no curated models, try raw aggregation if (models.length === 0) { const raw = await Backend.listAllModels(); models = (raw.models || []).map(m => ({ id: m.id || m.name, owned_by: m.owned_by || m.provider || null, config_id: m.config_id || null })); } } else { // Unmanaged mode: direct provider call const endpoint = showInModal ? document.getElementById('apiEndpoint').value.trim() : State.settings.apiEndpoint; const apiKey = showInModal ? document.getElementById('apiKey').value.trim() : State.settings.apiKey; if (!endpoint || !apiKey) { showToast('⚠️ Configure API endpoint and key first', 'warning'); if (!showInModal) openSettings(); return; } const response = await fetch(endpoint.replace(/\/$/, '') + '/models', { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); if (!response.ok) throw new Error(`API Error: ${response.status}`); const data = await response.json(); const raw = data.data || data.models || data || []; models = Array.isArray(raw) ? raw.map(m => ({ id: m.id || m.name || m, owned_by: m.owned_by || null })) : []; } State.models = models.sort((a, b) => a.id.localeCompare(b.id)); saveModels(); if (showInModal) { populateModelSelect(document.getElementById('model')); } updateQuickModelSelector(); showToast(`✅ Loaded ${State.models.length} models`, 'success'); } catch (error) { console.error('Fetch models error:', error); showToast(`❌ Failed: ${error.message}`, 'error'); } finally { if (btn) { btn.innerHTML = btn.dataset.originalText || '🔄'; btn.disabled = false; } } } async function sendApiRequest(messages) { const chatContainer = document.getElementById('chatMessages'); // Add typing indicator const typingDiv = document.createElement('div'); typingDiv.className = 'message assistant'; typingDiv.id = 'typingIndicator'; typingDiv.innerHTML = `
🤖
Assistant
`; chatContainer.appendChild(typingDiv); scrollToBottom(); State.isGenerating = true; State.abortController = new AbortController(); document.getElementById('stopBtn').classList.add('visible'); document.getElementById('sendBtn').disabled = true; const model = document.getElementById('quickModel').value || State.settings.model; const userContent = messages[messages.length - 1]?.content || ''; try { let response; if (Backend.isManaged) { // ── Managed mode: route through backend proxy ── // Backend loads conversation from DB, persists both messages. response = await Backend.streamCompletion( State.currentChatId, userContent, model, State.abortController.signal ); } else { // ── Unmanaged mode: direct provider call ── const endpoint = State.settings.apiEndpoint.replace(/\/$/, '') + '/chat/completions'; response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${State.settings.apiKey}` }, body: JSON.stringify({ model, messages: messages.map(m => ({ role: m.role, content: m.content })), max_tokens: State.settings.maxTokens, temperature: State.settings.temperature, top_p: State.settings.topP, presence_penalty: State.settings.presencePenalty, stream: State.settings.stream }), signal: State.abortController.signal }); if (!response.ok) { const error = await response.text(); throw new Error(`API Error: ${response.status} - ${error}`); } } document.getElementById('typingIndicator')?.remove(); let assistantContent = ''; const isStream = Backend.isManaged ? true : State.settings.stream; if (isStream) { const assistantDiv = document.createElement('div'); assistantDiv.className = 'message assistant'; assistantDiv.innerHTML = `
🤖
Assistant
`; chatContainer.appendChild(assistantDiv); const reader = response.body.getReader(); const decoder = new TextDecoder(); let sseBuffer = ''; // Buffer for incomplete SSE lines while (true) { const { done, value } = await reader.read(); if (done) break; sseBuffer += decoder.decode(value, { stream: true }); const lines = sseBuffer.split('\n'); // Keep the last (potentially incomplete) line in the buffer sseBuffer = lines.pop() || ''; for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6); if (data === '[DONE]') continue; try { const parsed = JSON.parse(data); const content = parsed.choices?.[0]?.delta?.content || ''; assistantContent += content; document.getElementById('streamingContent').innerHTML = formatMessage(assistantContent); scrollToBottom(); } catch (e) {} } } } } else { const data = await response.json(); assistantContent = data.choices?.[0]?.message?.content || 'No response'; } messages.push({ role: 'assistant', content: assistantContent, timestamp: new Date().toISOString() }); // In unmanaged mode, persist locally + to backend if connected await updateChat(State.currentChatId, { messages, model }); if (!Backend.isManaged) { await onAssistantResponse(assistantContent, model); } if (!isStream) { renderMessages(messages); } updateRegenerateButton(messages); } catch (error) { document.getElementById('typingIndicator')?.remove(); if (error.name === 'AbortError') { showToast('⚠️ Generation stopped', 'warning'); } else { console.error('API Error:', error); showToast(`❌ ${error.message}`, 'error'); } } finally { State.isGenerating = false; State.abortController = null; document.getElementById('stopBtn').classList.remove('visible'); document.getElementById('sendBtn').disabled = false; } } function stopGeneration() { if (State.abortController) { State.abortController.abort(); State.abortController = null; } State.isGenerating = false; document.getElementById('stopBtn').classList.remove('visible'); document.getElementById('sendBtn').disabled = false; }