// ========================================== // Chat Switchboard – Token Estimation // ========================================== // Context tracking, token estimation, and context warning. // ── Token Estimation + Context Tracking ───── const Tokens = { // Rough heuristic: ~4 chars per token for English (GPT/Claude average). // Not exact, but good enough for a UI indicator. estimate(text) { if (!text) return 0; return Math.ceil(text.length / 4); }, // Estimate tokens for the full conversation context sent to the model estimateConversation(messages, systemPrompt) { let total = 0; // System prompt if (systemPrompt) total += this.estimate(systemPrompt) + 4; // +4 for role/delimiters // Messages for (const m of messages) { total += this.estimate(m.content) + 4; // +4 per message overhead (role, delimiters) } return total; }, // Get context budget for current model getContextBudget() { const caps = UI.getSelectedModelCaps(); return { maxContext: caps.max_context || 0, maxOutput: caps.max_output_tokens || 0, }; }, // Format token count for display format(n) { if (n >= 100000) return (n / 1000).toFixed(0) + 'K'; if (n >= 10000) return (n / 1000).toFixed(1) + 'K'; if (n >= 1000) return (n / 1000).toFixed(1) + 'K'; return String(n); }, _warningDismissed: false, }; // Update the token counter below the input function updateInputTokens() { const el = document.getElementById('inputTokenCount'); if (!el) return; const input = document.getElementById('messageInput'); const inputText = input?.value || ''; const inputTokens = Tokens.estimate(inputText); if (!inputText.trim()) { el.textContent = ''; el.className = 'input-token-count'; return; } const budget = Tokens.getContextBudget(); if (budget.maxContext > 0) { // Show relative to available context const chat = App.chats.find(c => c.id === App.currentChatId); const convTokens = Tokens.estimateConversation(chat?.messages || [], App.settings.systemPrompt); const totalWithInput = convTokens + inputTokens; const pct = totalWithInput / budget.maxContext; el.textContent = `~${Tokens.format(inputTokens)} tokens · ${Tokens.format(totalWithInput)} / ${Tokens.format(budget.maxContext)} context`; el.className = 'input-token-count' + (pct > 0.9 ? ' danger' : pct > 0.75 ? ' warning' : ''); } else { el.textContent = `~${Tokens.format(inputTokens)} tokens`; el.className = 'input-token-count'; } } // Check conversation length and show/hide warning function updateContextWarning() { const warning = document.getElementById('contextWarning'); const text = document.getElementById('contextWarningText'); const summarizeBtn = document.getElementById('summarizeBtn'); if (!warning || !text) return; const budget = Tokens.getContextBudget(); if (budget.maxContext <= 0) { warning.style.display = 'none'; return; } const chat = App.chats.find(c => c.id === App.currentChatId); if (!chat || !chat.messages?.length) { warning.style.display = 'none'; return; } const convTokens = Tokens.estimateConversation(chat.messages, App.settings.systemPrompt); const pct = convTokens / budget.maxContext; // Show summarize button at ≥75% context usage (if enough messages) const showSummarize = pct >= 0.75 && chat.messages.length >= 4; if (summarizeBtn) { summarizeBtn.style.display = showSummarize ? 'inline-block' : 'none'; } // Show auto-compact toggle next to summarize button const autoToggle = document.getElementById('autoCompactToggle'); const autoCheck = document.getElementById('autoCompactCheck'); if (autoToggle && autoCheck) { autoToggle.style.display = showSummarize ? 'inline-flex' : 'none'; // Reflect current channel setting const chatSettings = chat?.settings || {}; autoCheck.checked = chatSettings.auto_compaction !== false; // default: on } if (pct >= 0.9 && !Tokens._warningDismissed) { warning.style.display = 'flex'; warning.className = 'context-warning danger'; text.textContent = `This conversation is using ~${Math.round(pct * 100)}% of the model's context window (~${Tokens.format(convTokens)} / ${Tokens.format(budget.maxContext)} tokens). Responses may lose earlier context.`; } else if (pct >= 0.75 && !Tokens._warningDismissed) { warning.style.display = 'flex'; warning.className = 'context-warning'; text.textContent = `Conversation is getting long (~${Math.round(pct * 100)}% of context window). The model may start losing track of earlier messages.`; } else { warning.style.display = 'none'; } } function dismissContextWarning() { Tokens._warningDismissed = true; const el = document.getElementById('contextWarning'); if (el) el.style.display = 'none'; }