honor admin default model selections
This commit is contained in:
parent
0c9abe4b93
commit
d734f4945b
4 changed files with 68 additions and 8 deletions
|
|
@ -287,6 +287,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
var costBadge = document.getElementById('model-cost-badge');
|
||||
window._currentModels = [];
|
||||
window._currentProvider = 'openrouter';
|
||||
window._defaultModelId = '';
|
||||
|
||||
fetch('/api/models')
|
||||
.then(function(r) { return r.json(); })
|
||||
|
|
@ -302,22 +303,27 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
opt.textContent = m.name;
|
||||
selectEl.appendChild(opt);
|
||||
});
|
||||
if (window._defaultModelId && !Array.prototype.some.call(selectEl.options, function(opt) { return opt.value === window._defaultModelId; })) {
|
||||
var saved = document.createElement('option');
|
||||
saved.value = window._defaultModelId;
|
||||
saved.textContent = window._defaultModelId + ' (saved default)';
|
||||
selectEl.appendChild(saved);
|
||||
}
|
||||
if (window._defaultModelId) selectEl.value = window._defaultModelId;
|
||||
}
|
||||
|
||||
// Determine default model (admin override or first model)
|
||||
var defaultModelId = data.defaultModel || (window._currentModels.length > 0 ? window._currentModels[0].id : '');
|
||||
window._defaultModelId = defaultModelId;
|
||||
|
||||
if (modelSelect && window._currentModels.length > 0) {
|
||||
window._buildModelOptions(modelSelect);
|
||||
// Select the admin-configured default
|
||||
if (defaultModelId) modelSelect.value = defaultModelId;
|
||||
if (costBadge) costBadge.textContent = '';
|
||||
}
|
||||
|
||||
// Populate all per-tab model selectors already in DOM
|
||||
document.querySelectorAll('.tab-model-select').forEach(function(sel) {
|
||||
window._buildModelOptions(sel);
|
||||
if (defaultModelId) sel.value = defaultModelId;
|
||||
});
|
||||
})
|
||||
.catch(function(err) { console.warn('Models load failed:', err); });
|
||||
|
|
|
|||
|
|
@ -266,6 +266,9 @@ app.get('/api/models', async (req, res) => {
|
|||
var db = require('./src/db/database');
|
||||
var models = await getAvailableModelsWithOverrides(db);
|
||||
var defaultOverride = await db.getSetting('models.default');
|
||||
if (defaultOverride && !models.find(function(m) { return m.id === defaultOverride; })) {
|
||||
models.push({ id: defaultOverride, name: defaultOverride + ' (saved default)', tag: 'SAVED' });
|
||||
}
|
||||
res.json({ models: models, provider: modelsProvider, defaultModel: defaultOverride || DEFAULT_MODEL });
|
||||
} catch(e) {
|
||||
res.json({ models: getAvailableModels(), provider: modelsProvider });
|
||||
|
|
|
|||
|
|
@ -417,14 +417,25 @@ async function assertModelAllowed(requestedModel, options) {
|
|||
}
|
||||
}
|
||||
|
||||
async function resolveModel(requestedModel) {
|
||||
var model = requestedModel && String(requestedModel).trim();
|
||||
if (model) return model;
|
||||
try {
|
||||
var db = require('../db/database');
|
||||
var adminDefault = await db.getSetting('models.default');
|
||||
if (adminDefault && String(adminDefault).trim()) return String(adminDefault).trim();
|
||||
} catch (e) {}
|
||||
return DEFAULT_MODEL;
|
||||
}
|
||||
|
||||
async function callAIStream(messages, options, onToken) {
|
||||
options = options || {};
|
||||
var requestedModel = options.model;
|
||||
var model = requestedModel || DEFAULT_MODEL;
|
||||
var model = await resolveModel(requestedModel);
|
||||
var temperature = options.temperature || 0.3;
|
||||
var maxTokens = options.maxTokens || 4000;
|
||||
var startTime = Date.now();
|
||||
await assertModelAllowed(requestedModel, options);
|
||||
await assertModelAllowed(model, options);
|
||||
|
||||
var client = null;
|
||||
var provider = null;
|
||||
|
|
@ -469,7 +480,7 @@ async function callAIStream(messages, options, onToken) {
|
|||
async function callAI(messages, options) {
|
||||
options = options || {};
|
||||
var requestedModel = options.model;
|
||||
var model = requestedModel || DEFAULT_MODEL;
|
||||
var model = await resolveModel(requestedModel);
|
||||
var temperature = options.temperature || 0.3;
|
||||
var maxTokens = options.maxTokens || 4000;
|
||||
var startTime = Date.now();
|
||||
|
|
@ -477,9 +488,9 @@ async function callAI(messages, options) {
|
|||
// Server-side whitelist: reject any model the operator hasn't enabled.
|
||||
// Prevents a client from passing e.g. model:"openai/o1" and draining
|
||||
// the budget on a reasoning model outside the configured roster.
|
||||
// Falls back silently to DEFAULT_MODEL when no model was provided.
|
||||
// Falls back to the admin default, then DEFAULT_MODEL, when no model was provided.
|
||||
// Admin test endpoints pass skipAllowlistCheck to test before adding.
|
||||
await assertModelAllowed(requestedModel, options);
|
||||
await assertModelAllowed(model, options);
|
||||
|
||||
try {
|
||||
var result;
|
||||
|
|
|
|||
40
test/model-defaults.test.js
Normal file
40
test/model-defaults.test.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(root, relativePath), 'utf8');
|
||||
}
|
||||
|
||||
test('api models endpoint preserves saved default in model list', () => {
|
||||
const server = read('server.js');
|
||||
assert.match(server, /var defaultOverride = await db\.getSetting\('models\.default'\)/);
|
||||
assert.match(server, /models\.push\(\{ id: defaultOverride, name: defaultOverride \+ ' \(saved default\)'/);
|
||||
assert.match(server, /defaultModel: defaultOverride \|\| DEFAULT_MODEL/);
|
||||
});
|
||||
|
||||
test('lazy-loaded tab model selectors apply admin default', () => {
|
||||
const app = read('public/js/app.js');
|
||||
assert.match(app, /window\._defaultModelId = ''/);
|
||||
assert.match(app, /window\._defaultModelId = defaultModelId/);
|
||||
assert.match(app, /window\._buildModelOptions\(sel\)/);
|
||||
assert.match(app, /selectEl\.value = window\._defaultModelId/);
|
||||
assert.match(app, /saved\.textContent = window\._defaultModelId \+ ' \(saved default\)'/);
|
||||
});
|
||||
|
||||
test('backend AI calls use admin default when request omits model', () => {
|
||||
const ai = read('src/utils/ai.js');
|
||||
assert.match(ai, /async function resolveModel\(requestedModel\)/);
|
||||
assert.match(ai, /await db\.getSetting\('models\.default'\)/);
|
||||
assert.match(ai, /var model = await resolveModel\(requestedModel\)/g);
|
||||
assert.match(ai, /await assertModelAllowed\(model, options\)/g);
|
||||
});
|
||||
|
||||
test('personal notes voice generation sends selected model only when populated', () => {
|
||||
const recorder = read('public/js/notes/recorder.js');
|
||||
assert.match(recorder, /var modelEl = document\.getElementById\('notes-model-select'\)/);
|
||||
assert.match(recorder, /options\.noteFromVoice\(\{ transcript: resp\.text, model: selectedModel \|\| undefined \}\)/);
|
||||
});
|
||||
Loading…
Reference in a new issue