feat(ki): Modell aus Liste der verfuegbaren OpenAI-Modelle waehlen
Bisher musste der Administrator den Modellnamen abtippen - ein Tippfehler fiel erst beim Test oder bei der ersten Frage auf, und welche Modelle der Schluessel ueberhaupt darf, war in Pulse nicht zu sehen. Einstellungen -> KI-Assistent zeigt jetzt ein Auswahlfeld. Die Liste kommt ueber POST /settings/ai/models live von OpenAI (/v1/models), neueste zuerst; Embedding-, Bild-, Audio-, Realtime-, Such- und Moderationsmodelle fliegen raus. "Liste laden" nimmt einen gerade eingetippten, noch nicht gespeicherten Schluessel mit. Das eingestellte Modell bleibt waehlbar, auch wenn OpenAI es nicht mehr listet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8a8e433397
commit
2f9c58f202
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
const ENDPOINT = 'https://api.openai.com/v1/responses';
|
||||
const MODELS_ENDPOINT = 'https://api.openai.com/v1/models';
|
||||
const TIMEOUT_MS = 120_000;
|
||||
|
||||
export const AI_DEFAULT_MODEL = 'gpt-5.6-luna';
|
||||
@@ -73,6 +74,42 @@ export async function askModel(ai, { instructions, input, tools, maxOutputTokens
|
||||
return data;
|
||||
}
|
||||
|
||||
/*
|
||||
* Modelle, die fuer den Assistenten in Frage kommen. /v1/models liefert alles,
|
||||
* was der Schluessel darf - auch Embeddings, Bild-, Audio- und Moderationsmodelle.
|
||||
* Die koennen keine Textantwort mit Werkzeugen und fliegen raus.
|
||||
*/
|
||||
const CHAT_MODEL = /^(gpt-|o\d|chatgpt-)/;
|
||||
const NOT_CHAT = /(embedding|tts|whisper|dall-e|image|audio|realtime|transcribe|moderation|search|instruct|codex)/;
|
||||
|
||||
/** Liste der Modelle fuer die Auswahl in den Einstellungen, neueste zuerst. */
|
||||
export async function listModels(apiKey) {
|
||||
if (!apiKey) throw new AiError('Es ist kein OpenAI-Schlüssel hinterlegt.', 400);
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 20_000);
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(MODELS_ENDPOINT, {
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
signal: controller.signal,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') throw new AiError('OpenAI hat nicht rechtzeitig geantwortet.', 504);
|
||||
throw new AiError(`OpenAI ist nicht erreichbar: ${err.message}`);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (response.status === 401) throw new AiError('OpenAI hat den Schlüssel abgelehnt.', 502);
|
||||
if (!response.ok) throw new AiError(`OpenAI: ${data?.error?.message || `HTTP ${response.status}`}`);
|
||||
|
||||
return (data.data || [])
|
||||
.filter((m) => CHAT_MODEL.test(m.id) && !NOT_CHAT.test(m.id))
|
||||
.sort((a, b) => (b.created || 0) - (a.created || 0) || a.id.localeCompare(b.id))
|
||||
.map((m) => ({ id: m.id, created: m.created ? new Date(m.created * 1000).toISOString().slice(0, 10) : null }));
|
||||
}
|
||||
|
||||
/** Text aller Nachrichten einer Antwort. */
|
||||
export function answerText(response) {
|
||||
return (response.output || [])
|
||||
|
||||
@@ -2,7 +2,7 @@ import express from 'express';
|
||||
import { requireRole, ROLES } from '../middleware/auth.js';
|
||||
import { DEFAULTS, getSettings, saveSetting } from '../lib/settings.js';
|
||||
import { ssoConfigured, testConnection } from '../lib/sso.js';
|
||||
import { answerText, askModel } from '../lib/ai.js';
|
||||
import { answerText, askModel, listModels } from '../lib/ai.js';
|
||||
import { sendDocument } from '../export/render.js';
|
||||
|
||||
const router = express.Router();
|
||||
@@ -178,6 +178,21 @@ router.post('/ai/test', requireRole('admin'), async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Verfuegbare Modelle fuer das Auswahlfeld. POST statt GET, weil ein gerade
|
||||
* eingetippter, noch nicht gespeicherter Schluessel mitkommen darf - der
|
||||
* gehoert nicht in eine URL. Ohne ihn gilt der hinterlegte.
|
||||
*/
|
||||
router.post('/ai/models', requireRole('admin'), async (req, res) => {
|
||||
try {
|
||||
const typed = typeof req.body?.api_key === 'string' ? req.body.api_key.trim().slice(0, 300) : '';
|
||||
const apiKey = typed || (await getSettings()).ai.api_key;
|
||||
res.json({ models: await listModels(apiKey) });
|
||||
} catch (err) {
|
||||
res.status(err.status || 502).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/sso/test', requireRole('admin'), async (req, res) => {
|
||||
try {
|
||||
const { sso } = await getSettings();
|
||||
|
||||
Reference in New Issue
Block a user