// ============================================================ // MODELS.JS — Provider-aware model list // Supports: OpenRouter, AWS Bedrock, Azure OpenAI, LiteLLM. // Vertex and anything else Google is reached through LiteLLM, not directly. // ============================================================ var activeProvider = process.env.AI_PROVIDER || (process.env.LITELLM_API_BASE ? 'litellm' : 'openrouter'); var OPENROUTER_MODELS = [ { id: 'google/gemini-2.5-flash', name: 'Gemini Flash 2.5' }, { id: 'google/gemini-2.5-pro', name: 'Gemini Pro 2.5' }, { id: 'google/gemini-2.5-flash:thinking', name: 'Gemini Flash Thinking' }, { id: 'deepseek/deepseek-chat-v3-0324', name: 'DeepSeek V3' }, { id: 'deepseek/deepseek-r1', name: 'DeepSeek R1' }, { id: 'deepseek/deepseek-r1:free', name: 'DeepSeek R1 Free' }, { id: 'qwen/qwen3-235b-a22b', name: 'Qwen3 235B' }, { id: 'qwen/qwen3-30b-a3b:free', name: 'Qwen3 30B Free' }, { id: 'meta-llama/llama-4-maverick', name: 'Llama 4 Maverick' }, { id: 'meta-llama/llama-3.3-70b-instruct', name: 'Llama 3.3 70B' }, { id: 'openai/gpt-4.1', name: 'GPT-4.1' }, { id: 'openai/gpt-4.1-mini', name: 'GPT-4.1 Mini' }, { id: 'openai/o4-mini', name: 'o4-mini (Reasoning)' }, { id: 'mistralai/mistral-large-2411', name: 'Mistral Large' }, { id: 'mistralai/mistral-small-3.2-24b-instruct:free', name: 'Mistral Small Free' } ]; var BEDROCK_MODELS = [ // All IDs verified against AWS docs 2026-03-24 // us. prefix = cross-region inference profile (required for most newer models) // maxOut = model's max output token limit (omit if >= 8192) // ── Amazon Nova (inference profiles) ── { id: 'amazon/nova-pro', name: 'Amazon Nova Pro', bedrockId: 'us.amazon.nova-pro-v1:0' }, { id: 'amazon/nova-lite', name: 'Amazon Nova Lite', bedrockId: 'us.amazon.nova-lite-v1:0' }, { id: 'amazon/nova-micro', name: 'Amazon Nova Micro', bedrockId: 'us.amazon.nova-micro-v1:0' }, // ── Meta Llama (inference profiles) ── { id: 'meta/llama-4-maverick', name: 'Llama 4 Maverick', bedrockId: 'us.meta.llama4-maverick-17b-instruct-v1:0' }, { id: 'meta/llama-4-scout', name: 'Llama 4 Scout', bedrockId: 'us.meta.llama4-scout-17b-instruct-v1:0' }, { id: 'meta/llama-3.3-70b', name: 'Llama 3.3 70B', bedrockId: 'us.meta.llama3-3-70b-instruct-v1:0' }, // ── DeepSeek (R1 has profile, V3.2 is on-demand) ── { id: 'deepseek/r1', name: 'DeepSeek R1', bedrockId: 'us.deepseek.r1-v1:0' }, { id: 'deepseek/v3.2', name: 'DeepSeek V3.2', bedrockId: 'deepseek.v3.2', regions: ['us-east-1', 'us-east-2', 'us-west-2'] }, // ── Mistral AI (on-demand, no profiles available) ── { id: 'mistral/large-3', name: 'Mistral Large 3 (675B)', bedrockId: 'mistral.mistral-large-3-675b-instruct', regions: ['us-east-1', 'us-east-2', 'us-west-2'] }, { id: 'mistral/magistral-small', name: 'Magistral Small', bedrockId: 'mistral.magistral-small-2509', regions: ['us-east-1', 'us-east-2', 'us-west-2'] }, // ── Cohere (on-demand, max 4096 output) ── { id: 'cohere/command-r-plus', name: 'Command R+', bedrockId: 'cohere.command-r-plus-v1:0', regions: ['us-east-1', 'us-west-2'], maxOut: 4096 }, { id: 'cohere/command-r', name: 'Command R', bedrockId: 'cohere.command-r-v1:0', regions: ['us-east-1', 'us-west-2'], maxOut: 4096 }, // ── AI21 Labs (on-demand, max 4096 output) ── { id: 'ai21/jamba-1.5-large', name: 'Jamba 1.5 Large', bedrockId: 'ai21.jamba-1-5-large-v1:0', regions: ['us-east-1'], maxOut: 4096 }, // ── Writer (inference profile) ── { id: 'writer/palmyra-x5', name: 'Palmyra X5', bedrockId: 'us.writer.palmyra-x5-v1:0' }, // ── Qwen (on-demand, no profiles) ── { id: 'qwen/qwen3-235b', name: 'Qwen3 235B', bedrockId: 'qwen.qwen3-235b-a22b-2507-v1:0' }, { id: 'qwen/qwen3-32b', name: 'Qwen3 32B', bedrockId: 'qwen.qwen3-32b-v1:0' } ]; var AZURE_MODELS = [ { id: 'gpt-4o', name: 'GPT-4o', deploymentNote: 'Set AZURE_DEPLOYMENT_NAME in .env' }, { id: 'gpt-4o-mini', name: 'GPT-4o Mini', deploymentNote: 'Set AZURE_DEPLOYMENT_NAME in .env' }, { id: 'gpt-4.1', name: 'GPT-4.1', deploymentNote: 'Set AZURE_DEPLOYMENT_NAME in .env' }, { id: 'gpt-4.1-mini', name: 'GPT-4.1 Mini', deploymentNote: 'Set AZURE_DEPLOYMENT_NAME in .env' } ]; // LiteLLM has NO built-in models — everything is discovered from the proxy via admin panel. // This array is intentionally empty. Do not add models here. var LITELLM_MODELS = []; function getAvailableModels() { switch (activeProvider) { case 'bedrock': var region = process.env.AWS_BEDROCK_REGION || 'us-east-1'; return BEDROCK_MODELS.filter(function(m) { return !m.regions || m.regions.indexOf(region) !== -1; }); case 'azure': return AZURE_MODELS; case 'litellm': return LITELLM_MODELS; case 'openrouter': default: return OPENROUTER_MODELS; } } function getDefaultModel() { switch (activeProvider) { case 'bedrock': return 'amazon/nova-pro'; case 'azure': return process.env.AZURE_DEPLOYMENT_NAME || 'gpt-4o-mini'; case 'litellm': return process.env.LITELLM_DEFAULT_MODEL || process.env.OPENAI_MODEL || ''; // OpenAI-compatible base: set admin default or env. case 'openrouter': default: return 'google/gemini-2.5-flash'; } } function getFallbackModel() { switch (activeProvider) { case 'bedrock': return 'amazon/nova-lite'; case 'azure': return process.env.AZURE_DEPLOYMENT_NAME || 'gpt-4o-mini'; case 'litellm': return process.env.LITELLM_FALLBACK_MODEL || ''; // No implicit cross-provider fallback. case 'openrouter': default: return 'deepseek/deepseek-chat-v3-0324'; } } function getBedrockModelId(modelId) { var found = BEDROCK_MODELS.find(function(m) { return m.id === modelId; }); return found ? found.bedrockId : modelId; } function getBedrockMaxOut(modelId) { var found = BEDROCK_MODELS.find(function(m) { return m.id === modelId; }); return found && found.maxOut ? found.maxOut : null; } var AVAILABLE_MODELS = getAvailableModels(); var DEFAULT_MODEL = getDefaultModel(); var FALLBACK_MODEL = getFallbackModel(); // node:test reads a test file's results back over the child's stdout, so a // banner written there can land inside a serialized frame and fail the whole // file. NODE_TEST_CONTEXT is set only inside those children, so production and // `node server.js` output is unchanged. if (!process.env.NODE_TEST_CONTEXT) { console.log('🤖 Provider:', activeProvider); console.log('🤖 Default model:', DEFAULT_MODEL); console.log('🤖 Models available:', AVAILABLE_MODELS.length); } // DB-aware model list (used by /api/models endpoint) async function getAvailableModelsWithOverrides(db) { var baseModels = getAvailableModels(); var disabledRaw = await db.getSetting('models.disabled'); var customRaw = await db.getSetting('models.custom'); var disabled = JSON.parse(disabledRaw == null ? '[]' : disabledRaw); var custom = JSON.parse(customRaw == null ? '[]' : customRaw); if (!Array.isArray(disabled) || disabled.some(function(id) { return typeof id !== 'string' || !id.trim(); }) || !Array.isArray(custom) || custom.some(function(m) { return !m || typeof m.id !== 'string' || !m.id.trim() || m.id !== m.id.trim(); })) { throw new Error('Invalid model settings'); } var result = baseModels.slice(); custom.forEach(function(m) { if (!result.some(function(r) { return r.id === m.id; })) result.push(m); }); return result.filter(function(m) { return !disabled.includes(m.id); }); } // Read policy on every call: no stale grants after edits or during a DB outage. async function getAllowedModelIds(db) { var models = await getAvailableModelsWithOverrides(db); return new Set(models.map(function(m) { return m.id; })); } // Never advertise or use a stale/disabled default, including source/env defaults. async function getEffectiveDefaultModel(db, models) { models = models || await getAvailableModelsWithOverrides(db); var saved = await db.getSetting('models.default'); var preferred = saved || getDefaultModel(); return models.some(function(m) { return m.id === preferred; }) ? preferred : (models[0] ? models[0].id : ''); } async function reconcileDefaultModel(db) { var saved = await db.getSetting('models.default'); if (saved && !(await getAvailableModelsWithOverrides(db)).some(function(m) { return m.id === saved; })) { await db.setSetting('models.default', ''); } } // Synchronous check against the static baseline (no DB access). Used when // the async-aware helper is inconvenient. For full enforcement use // getAllowedModelIds + Set.has() in the route before invoking callAI. function isStaticAllowedModel(id) { if (!id) return false; return getAvailableModels().some(function(m) { return m.id === id; }); } module.exports = { AVAILABLE_MODELS, DEFAULT_MODEL, FALLBACK_MODEL, OPENROUTER_MODELS, BEDROCK_MODELS, AZURE_MODELS, LITELLM_MODELS, getAvailableModels, getAvailableModelsWithOverrides, getAllowedModelIds, getEffectiveDefaultModel, reconcileDefaultModel, isStaticAllowedModel, getDefaultModel, getFallbackModel, getBedrockModelId, getBedrockMaxOut, activeProvider };