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:
schakal00783
2026-09-23 10:25:21 +02:00
co-authored by Claude Opus 5.5
parent 8a8e433397
commit 2f9c58f202
5 changed files with 132 additions and 4 deletions
+56 -3
View File
@@ -5,7 +5,7 @@ import { Team } from '../team/Team';
import { SsoDirectory } from './SsoDirectory';
import { HrworksSettings } from './HrworksSettings';
import { useAuth } from '../../lib/auth';
import { download, settingsApi, type AdminSettings } from '../../lib/api';
import { download, settingsApi, type AdminSettings, type AiModel } from '../../lib/api';
import { useLoader } from '../../lib/hooks';
import { useRouter } from '../../lib/router';
import { useToast } from '../Toast';
@@ -455,6 +455,28 @@ const AiSettings: React.FC<{ settings: AdminSettings; onSaved: () => void }> = (
useEffect(() => setAi({ ...settings.ai, api_key: '' }), [settings.ai]);
// Auswahl kommt live von OpenAI - welche Modelle es gibt, haengt am Schluessel.
const [models, setModels] = useState<AiModel[]>([]);
const [modelsBusy, setModelsBusy] = useState(false);
const [modelsError, setModelsError] = useState('');
const loadModels = async (apiKey?: string) => {
setModelsBusy(true);
setModelsError('');
try {
setModels((await settingsApi.aiModels(apiKey)).models);
} catch (err) {
setModelsError(err instanceof Error ? err.message : 'Modelle konnten nicht geladen werden.');
} finally {
setModelsBusy(false);
}
};
useEffect(() => {
if (settings.ai.key_set) loadModels();
}, [settings.ai.key_set]);
// Das eingestellte Modell bleibt waehlbar, auch wenn OpenAI es nicht (mehr) listet.
const modelOptions = models.some((m) => m.id === ai.model) || !ai.model ? models : [{ id: ai.model, created: null }, ...models];
const test = async () => {
setTesting(true);
try {
@@ -510,8 +532,39 @@ const AiSettings: React.FC<{ settings: AdminSettings; onSaved: () => void }> = (
/>
</Field>
<Field label="Modell" hint="Voreingestellt gpt-5.6-luna. Ein anderes Modell muss Werkzeuge über die Responses-API unterstützen.">
<input className={`${inputClass} font-mono text-xs`} value={ai.model} onChange={(e) => set({ model: e.target.value })} />
<Field
label="Modell"
hint={
modelsError ||
(models.length
? `${models.length} Modelle für diesen Schlüssel, neueste zuerst. Voreingestellt gpt-5.6-luna; ein anderes Modell muss Werkzeuge über die Responses-API unterstützen.`
: 'Die Liste kommt von OpenAI, sobald ein Schlüssel hinterlegt oder eingegeben ist.')
}
>
<div className="flex gap-2">
<select
className={`${inputClass} font-mono text-xs`}
value={ai.model}
onChange={(e) => set({ model: e.target.value })}
disabled={modelsBusy}
>
{modelOptions.map((m) => (
<option key={m.id} value={m.id}>
{m.id}
{m.created ? ` (${m.created})` : ''}
</option>
))}
</select>
<Button
variant="secondary"
icon="refresh"
busy={modelsBusy}
onClick={() => loadModels(ai.api_key)}
disabled={!settings.ai.key_set && !ai.api_key}
>
Liste laden
</Button>
</div>
</Field>
<Toggle
+6
View File
@@ -137,6 +137,7 @@ export const settingsApi = {
save: (changes: Partial<AdminSettingsInput>) => send<{ ok: true }>('PUT', '/settings', changes),
ssoTest: () => send<{ ok: true; sample: string[]; checked: number; eligible: number }>('POST', '/settings/sso/test'),
aiTest: () => send<{ ok: true; model: string; answer: string }>('POST', '/settings/ai/test'),
aiModels: (apiKey?: string) => send<{ models: AiModel[] }>('POST', '/settings/ai/models', { api_key: apiKey || undefined }),
};
export interface ExportSettings {
@@ -226,6 +227,11 @@ export const hrworksApi = {
removeAll: () => send<{ deleted: number }>('DELETE', '/hrworks/entries'),
};
export interface AiModel {
id: string;
created: string | null;
}
export interface AiSettings {
enabled: boolean;
api_key: string;