From f9dc1a69209d57d245e4c7ed0a288a2099fe394d Mon Sep 17 00:00:00 2001 From: southseact-3d Date: Mon, 9 Feb 2026 18:03:08 +0000 Subject: [PATCH] Fix provider limits to only use configured providers and support OpenRouter model formats --- chat/server.js | 147 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 139 insertions(+), 8 deletions(-) diff --git a/chat/server.js b/chat/server.js index 06279fa..9d707a9 100644 --- a/chat/server.js +++ b/chat/server.js @@ -5741,12 +5741,27 @@ function extractProviderName(p) { function collectProviderSeeds() { const seeds = new Set(DEFAULT_PROVIDER_SEEDS); - Object.keys(providerLimits.limits || {}).forEach((p) => seeds.add(normalizeProviderName(p))); - Object.keys(providerUsage || {}).forEach((p) => seeds.add(normalizeProviderName(p))); - adminModels.forEach((m) => { - (m.providers || []).forEach((p) => seeds.add(extractProviderName(p))); + const validProviders = new Set(DEFAULT_PROVIDER_SEEDS); + // Only include providers from limits/usage that are valid configured providers + // This prevents "moonshot" or "z-ai" from being treated as providers + Object.keys(providerLimits.limits || {}).forEach((p) => { + const normalized = normalizeProviderName(p); + if (validProviders.has(normalized)) seeds.add(normalized); + }); + Object.keys(providerUsage || {}).forEach((p) => { + const normalized = normalizeProviderName(p); + if (validProviders.has(normalized)) seeds.add(normalized); + }); + adminModels.forEach((m) => { + (m.providers || []).forEach((p) => { + const providerName = extractProviderName(p); + if (validProviders.has(providerName)) seeds.add(providerName); + }); + }); + (planSettings.planningChain || []).forEach((entry) => { + const normalized = normalizeProviderName(entry.provider); + if (validProviders.has(normalized)) seeds.add(normalized); }); - (planSettings.planningChain || []).forEach((entry) => seeds.add(normalizeProviderName(entry.provider))); return Array.from(seeds); } @@ -5792,13 +5807,38 @@ async function discoverProviderModels() { try { const models = await listModels(DEFAULT_PROVIDER_FALLBACK); + // Build a set of valid configured providers to avoid treating model name prefixes as providers + const validProviders = new Set(DEFAULT_PROVIDER_SEEDS); models.forEach((m) => { // listModels may return strings or objects; handle both. - const name = m.name || m.id || m; + let name = m.name || m.id || m; if (!name || typeof name !== 'string') return; + + // Handle OpenRouter model name formats: + // 1. "openrouter/z-ai/glm-4.7" -> provider: openrouter, model: openrouter/z-ai/glm-4.7 + // 2. "z-ai/glm-4.7 (OpenRouter)" -> provider: openrouter, model: z-ai/glm-4.7 + // 3. "z-ai/glm-4.7" without prefix/label -> provider: openrouter (detected from slash), model: z-ai/glm-4.7 + + const openRouterLabelMatch = name.match(/\s*\(openrouter\)\s*$/i); + if (openRouterLabelMatch) { + // Format: "z-ai/glm-4.7 (OpenRouter)" - strip the label and treat as openrouter + const cleanName = name.slice(0, openRouterLabelMatch.index).trim(); + add('openrouter', cleanName); + return; + } + const parts = name.split('/'); - if (parts.length > 1 && parts[0]) add(parts[0], name); - else add(DEFAULT_PROVIDER_FALLBACK, name); + if (parts.length > 1 && parts[0] && validProviders.has(parts[0].toLowerCase())) { + // The first part is a known configured provider (e.g., "openrouter/z-ai/glm-4.7") + add(parts[0], name); + } else if (parts.length > 1) { + // Model has a slash but first part is not a known provider (e.g., "z-ai/glm-4.7") + // This is likely an OpenRouter model without the prefix + add('openrouter', name); + } else { + // No slash in name, use default provider + add(DEFAULT_PROVIDER_FALLBACK, name); + } }); } catch (err) { log('provider discovery failed', { error: String(err) }); @@ -5826,6 +5866,19 @@ async function loadProviderLimits() { } catch (error) { log('Failed to load provider limits, using defaults', { error: String(error) }); } + + // Clean up any invalid providers that may have been saved previously + // (e.g., "moonshot", "z-ai" that were incorrectly extracted from model names) + const validProviders = new Set(DEFAULT_PROVIDER_SEEDS); + if (providerLimits.limits) { + Object.keys(providerLimits.limits).forEach((key) => { + if (!validProviders.has(normalizeProviderName(key))) { + delete providerLimits.limits[key]; + log('Removed invalid provider from limits', { provider: key }); + } + }); + } + collectProviderSeeds().forEach((p) => ensureProviderLimitDefaults(p)); if (!providerLimits.opencodeBackupModel) providerLimits.opencodeBackupModel = ''; } @@ -14844,6 +14897,83 @@ async function handleAdminCancelMessages(req, res) { } } +// Test Ollama connection from admin panel +async function handleAdminOllamaTest(req, res) { + const adminSession = requireAdminAuth(req, res); + if (!adminSession) return; + + try { + const startTime = Date.now(); + + // Log the configuration (without exposing the full API key) + const apiKeyConfigured = !!OLLAMA_API_KEY; + const apiKeyPreview = OLLAMA_API_KEY + ? `${OLLAMA_API_KEY.substring(0, 4)}...${OLLAMA_API_KEY.substring(OLLAMA_API_KEY.length - 4)}` + : 'Not configured'; + + console.log('[ADMIN] Testing Ollama connection', { + url: OLLAMA_API_URL, + model: OLLAMA_DEFAULT_MODEL, + apiKeyConfigured, + apiKeyPreview + }); + + // Make a test request to Ollama + const testMessages = [ + { role: 'user', content: 'Say "Ollama test successful" and nothing else.' } + ]; + + let result; + let error = null; + + try { + result = await sendOllamaChat({ messages: testMessages, model: OLLAMA_DEFAULT_MODEL }); + } catch (err) { + error = err; + console.log('[ADMIN] Ollama test failed', { + error: err.message, + status: err.status, + detail: err.detail + }); + } + + const duration = Date.now() - startTime; + + // Prepare the response + const response = { + ok: !error, + duration, + config: { + url: OLLAMA_API_URL, + model: OLLAMA_DEFAULT_MODEL, + apiKeyConfigured, + apiKeyPreview + }, + result: error ? null : { + reply: result.reply, + model: result.model + }, + error: error ? { + message: error.message, + status: error.status, + detail: error.detail, + isAuthError: error.isAuthError, + isModelMissing: error.isModelMissing, + code: error.code + } : null + }; + + console.log('[ADMIN] Ollama test completed', { ok: response.ok, duration }); + sendJson(res, 200, response); + } catch (error) { + console.error('[ADMIN] Ollama test handler error', error); + sendJson(res, 500, { + ok: false, + error: error.message || 'Internal server error during Ollama test' + }); + } +} + // Get detailed resource usage breakdown by session for admin panel async function handleAdminResources(req, res) { const adminSession = requireAdminAuth(req, res); @@ -16936,6 +17066,7 @@ async function routeInternal(req, res, url, pathname) { if (req.method === 'POST' && pathname === '/api/admin/external-testing-self-test') return handleAdminExternalTestingSelfTest(req, res); if (req.method === 'POST' && pathname === '/api/upgrade-popup-tracking') return handleUpgradePopupTracking(req, res); if (req.method === 'POST' && pathname === '/api/admin/cancel-messages') return handleAdminCancelMessages(req, res); + if (req.method === 'POST' && pathname === '/api/admin/ollama-test') return handleAdminOllamaTest(req, res); const adminDeleteMatch = pathname.match(/^\/api\/admin\/models\/([a-f0-9\-]+)$/i); if (req.method === 'DELETE' && adminDeleteMatch) return handleAdminModelDelete(req, res, adminDeleteMatch[1]); if (req.method === 'GET' && pathname === '/api/models') {