diff --git a/public/components/admin.html b/public/components/admin.html
index f104a43..0ef4245 100644
--- a/public/components/admin.html
+++ b/public/components/admin.html
@@ -325,6 +325,11 @@
diff --git a/public/js/admin.js b/public/js/admin.js
index 95c6843..52a63c3 100644
--- a/public/js/admin.js
+++ b/public/js/admin.js
@@ -703,6 +703,7 @@ function adminFlashButtonBackground(btn, color) {
if (e.target.closest('#btn-test-assistant-chat-model')) testAssistantChatModel();
if (e.target.closest('#btn-refresh-assistant-image-models')) loadAssistantImageModels();
if (e.target.closest('#btn-test-assistant-image-model')) testAssistantImageModel();
+ if (e.target.closest('#btn-use-custom-assistant-image-model')) useCustomAssistantImageModel();
});
function loadAssistantAdmin() {
@@ -826,6 +827,22 @@ function adminFlashButtonBackground(btn, color) {
});
}
+ function useCustomAssistantImageModel() {
+ var input = document.getElementById('assistant-custom-image-model');
+ var sel = document.getElementById('assistant-image-model');
+ var model = input ? input.value.trim() : '';
+ if (!model) { showToast('Enter an image model ID', 'error'); return; }
+ if (sel && !Array.prototype.some.call(sel.options, function(o) { return o.value === model; })) {
+ var opt = document.createElement('option');
+ opt.value = model;
+ opt.textContent = model + ' (custom)';
+ sel.appendChild(opt);
+ }
+ if (sel) sel.value = model;
+ window._assistantImageModelValue = model;
+ showToast('Custom image model selected. Save settings to keep it.', 'info');
+ }
+
function saveAssistantAdmin() {
var status = document.getElementById('assistant-admin-status');
if (status) status.textContent = 'Saving...';
diff --git a/src/routes/adminConfig.js b/src/routes/adminConfig.js
index fadd260..8dc589b 100644
--- a/src/routes/adminConfig.js
+++ b/src/routes/adminConfig.js
@@ -10,7 +10,7 @@ var PROMPTS = require('../utils/prompts');
var logger = require('../utils/logger');
var { gatewayUrl } = require('../utils/errors');
var { getTTSEnvProvider, getLiteLLMTTSDiscoveryItems, getTTSProvider, getTTSVoiceLists } = require('../utils/ttsProvider');
-var { getLiteLLMHeaders } = require('../utils/litellm');
+var { getLiteLLMHeaders, getLiteLLMAdminHeaders } = require('../utils/litellm');
var { getSTTDependencies, getLiteLLMSTTModels, getSTTModelLists, getSTTProvider } = require('../utils/sttProvider');
var { getLiteLLMEmbeddingModels } = require('../utils/embeddings');
@@ -448,7 +448,7 @@ router.get('/config/image-models/discover', async function(req, res) {
var axios = require('axios');
var discovered = [];
if (process.env.LITELLM_API_BASE) {
- var resp = await axios.get(liteLLMBaseUrl() + '/model/info', { headers: getLiteLLMHeaders(), timeout: 15000 });
+ var resp = await axios.get(liteLLMBaseUrl() + '/model/info', { headers: getLiteLLMAdminHeaders(), timeout: 15000 });
var models = (resp.data && resp.data.data) || [];
models.forEach(function(m) {
if (liteLLMModelMode(m) === 'image_generation') {
@@ -535,7 +535,7 @@ router.get('/config/tts/discover', async function(req, res) {
var currentModel = dbModel || process.env.LITELLM_TTS_MODEL || '';
var modelInfo = [];
try {
- var lResp = await axios.get(liteLLMBaseUrl() + '/model/info', { headers: getLiteLLMHeaders(), timeout: 10000 });
+ var lResp = await axios.get(liteLLMBaseUrl() + '/model/info', { headers: getLiteLLMAdminHeaders(), timeout: 10000 });
modelInfo = lResp.data && lResp.data.data ? lResp.data.data : [];
} catch (e) { logger.warn('LiteLLM TTS model list failed: ' + e.message); }
getLiteLLMTTSDiscoveryItems(modelInfo, { currentModel: currentModel, currentVoice: currentVoice }).forEach(function(item) {
@@ -616,7 +616,7 @@ router.get('/config/stt/discover', async function(req, res) {
if (provider === 'litellm' && process.env.LITELLM_API_BASE) {
try {
- var sResp = await axios.get(liteLLMBaseUrl() + '/model/info', { headers: getLiteLLMHeaders(), timeout: 10000 });
+ var sResp = await axios.get(liteLLMBaseUrl() + '/model/info', { headers: getLiteLLMAdminHeaders(), timeout: 10000 });
getLiteLLMSTTModels(sResp.data && sResp.data.data).forEach(function(id) {
discovered.push({ id: id, name: id, source: 'gateway-api' });
});
@@ -712,7 +712,7 @@ router.get('/config/embeddings/discover', async function(req, res) {
if (provider === 'litellm') {
try {
- var eResp = await axios.get(liteLLMBaseUrl() + '/model/info', { headers: getLiteLLMHeaders(), timeout: 10000 });
+ var eResp = await axios.get(liteLLMBaseUrl() + '/model/info', { headers: getLiteLLMAdminHeaders(), timeout: 10000 });
var embeddingModels = getLiteLLMEmbeddingModels(eResp.data && eResp.data.data);
for (var i = 0; i < embeddingModels.length; i++) {
var m = embeddingModels[i];
diff --git a/src/utils/litellm.js b/src/utils/litellm.js
index 96b47ea..166e274 100644
--- a/src/utils/litellm.js
+++ b/src/utils/litellm.js
@@ -6,6 +6,15 @@ function getLiteLLMHeaders(contentType) {
return headers;
}
+function getLiteLLMAdminHeaders(contentType) {
+ var headers = {};
+ if (contentType) headers['Content-Type'] = contentType;
+ var key = process.env.LITELLM_MASTER_KEY || process.env.LITELLM_API_KEY;
+ if (key) headers.Authorization = 'Bearer ' + key;
+ return headers;
+}
+
module.exports = {
- getLiteLLMHeaders
+ getLiteLLMHeaders,
+ getLiteLLMAdminHeaders
};
diff --git a/test/tts-provider.test.js b/test/tts-provider.test.js
index 7e62ec2..5d5696b 100644
--- a/test/tts-provider.test.js
+++ b/test/tts-provider.test.js
@@ -1,7 +1,7 @@
const { test } = require('node:test');
const assert = require('node:assert/strict');
-const ENV_KEYS = ['TTS_PROVIDER', 'LITELLM_API_BASE', 'LITELLM_API_KEY', 'LITELLM_TTS_VOICES'];
+const ENV_KEYS = ['TTS_PROVIDER', 'LITELLM_API_BASE', 'LITELLM_API_KEY', 'LITELLM_MASTER_KEY', 'LITELLM_TTS_VOICES'];
function withEnv(overrides, fn) {
const previous = {};
@@ -112,3 +112,11 @@ test('LiteLLM headers include optional content type without exposing key value',
});
});
});
+
+test('LiteLLM admin headers prefer master key for metadata routes', () => {
+ const { getLiteLLMHeaders, getLiteLLMAdminHeaders } = require('../src/utils/litellm');
+ withEnv({ LITELLM_API_KEY: 'virtual-token', LITELLM_MASTER_KEY: 'master-token' }, () => {
+ assert.equal(getLiteLLMHeaders().Authorization, 'Bearer virtual-token');
+ assert.equal(getLiteLLMAdminHeaders().Authorization, 'Bearer master-token');
+ });
+});