pediatric-ai-scribe-v3/src/utils/models.js
Daniel 1d031af5d6
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 49s
Forgejo Docker Build / Root app tests (push) Successful in 58s
Forgejo Android APK / Build signed APK (push) Successful in 2m9s
Forgejo Docker Build / Build Docker image (push) Successful in 12s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
refactor: Google models go through LiteLLM; the Vertex SDK is gone
npm audit reports 0 vulnerabilities. It reported 2 high and 2 moderate this
morning.

@google-cloud/vertexai was the last source of findings — gaxios and a uuid with
a missing buffer bounds check, neither reachable in this deployment because
GOOGLE_VERTEX_PROJECT is unset and the require sits inside that check. Dormant
is not the same as gone, and the provider is available through the gateway
anyway, so the direct path has been removed rather than left to rot:

  - the SDK client and callVertex, which without the package could never run
  - the dispatch and discovery branches that reached them
  - VERTEX_MODELS, a list of ids nothing could route any more, and the two
    places in adminConfig that concatenated it into the built-in set
  - the health endpoint's vertex line, and the env vars documented for it

AI_PROVIDER=vertex now says where to configure the model instead of quietly
becoming something else. The Google STT and TTS paths keyed off the same
variable are untouched; neither ever used this SDK.

Verified after deploy: provider litellm, the assistant answers with 8 sources,
/api/models returns 10, and @aws-sdk/s3-request-presigner — which documents.js
needs for presigned MinIO URLs — is still declared and resolvable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-11 13:51:22 +02:00

221 lines
9.1 KiB
JavaScript

// ============================================================
// 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
};