OpenAI: Server-Key-Setting, Filter-Fix fuer o-Reihe, ehrlicher Modell-Refresh

Der Modell-Refresh war fuer OpenAI faktisch ein No-Op: der Key wurde nur aus
der api_keys-Tabelle geholt, und ohne Key blieb die alte Liste im Speicher
stehen und wurde direkt wieder in models_cache.json geschrieben - waehrend der
Endpoint pauschal Erfolg meldete.

- ServerSettingsService: neues Setting OpenAiApiKey (OpenAI war der einzige
  Provider ohne Server-Key, analog Gemini/MiniMax/z.ai)
- OpenAiService: ResolveOpenAiKey() (Setting -> api_keys), HasServerOpenAiKey(),
  ResolveProviderConfig zieht den Setting-Key auch im Chat
- OpenAiService: IsOpenAiChatModel() erfasst die o-Reihe inkl. bare IDs
  (o1, o3, kuenftig o5) - StartsWith("o3-") hat die verschluckt
- RefreshModelsFromApis() liefert ProviderRefreshResult[] pro Provider;
  ModelsEndpoint gibt providers[] zurueck, agent-page.js meldet uebersprungene
  Provider als Fehler-Toast statt pauschalem Erfolg
- ChatEndpoint/TelegramService/CronjobService: OpenAI ohne Agent-Key faellt auf
  den Server-Key zurueck statt hart abzubrechen
- ARCHITECTURE.md nachgezogen

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 5 committed 2026-08-15 11:55:22 +02:00
1 parent f38f3a42d1
commit 7305a5a50f
8 files changed
+136 -31

No files matched your search

+87 -16
View File
@@ -149,37 +149,99 @@ public class OpenAiService
}
/// <summary>
/// Lädt Modelle von allen Provider-APIs und speichert sie im Cache.
/// Ergebnis eines Modell-Refreshs pro Provider – damit der Aufrufer (und die UI)
/// sieht, was tatsächlich geladen und was übersprungen wurde.
/// </summary>
public async Task RefreshModelsFromApis()
public class ProviderRefreshResult
{
public string Provider { get; set; }
public bool Loaded { get; set; } // true = frisch von der API geladen
public int Count { get; set; } // Anzahl Modelle nach dem Refresh
public string Note { get; set; } // Grund bei Skip/Fehler, sonst ""
}
/// <summary>
/// Löst den OpenAI-Key auf: bevorzugt das Server-Setting (OpenAiApiKey, analog
/// Gemini/MiniMax), sonst der erste aktive 'openai'-Key aus der api_keys-Tabelle.
/// </summary>
public bool HasServerOpenAiKey() => !string.IsNullOrEmpty(_settings.Get("OpenAiApiKey"));
public async Task<string> ResolveOpenAiKey()
{
var fromSettings = _settings.Get("OpenAiApiKey");
if (!string.IsNullOrEmpty(fromSettings)) return fromSettings;
return await _db.GetFirstActiveKeyByProvider("openai");
}
/// <summary>
/// Chat-Modelle von OpenAI filtern: gpt-*, chatgpt-* und die o-Reihe.
/// Wichtig: auch bare IDs ohne Bindestrich (o1, o3, künftig o5) – der alte
/// Filter (StartsWith("o3-")) hat die verschluckt.
/// </summary>
private static bool IsOpenAiChatModel(string id)
{
if (string.IsNullOrEmpty(id)) return false;
if (id.StartsWith("gpt-", StringComparison.OrdinalIgnoreCase)) return true;
if (id.StartsWith("chatgpt-", StringComparison.OrdinalIgnoreCase)) return true;
// o-Reihe: 'o' + Ziffer, danach Ende oder '-'/'.' (o1, o3, o3-mini, o4-mini, o5.1)
if (id.Length >= 2 && (id[0] == 'o' || id[0] == 'O') && char.IsDigit(id[1]))
return true;
return false;
}
/// <summary>
/// Lädt Modelle von allen Provider-APIs und speichert sie im Cache.
/// Gibt pro Provider zurück, ob geladen oder übersprungen wurde.
/// </summary>
public async Task<List<ProviderRefreshResult>> RefreshModelsFromApis()
{
var report = new List<ProviderRefreshResult>();
void Report(string provider, bool loaded, int count, string note = "")
{
report.Add(new ProviderRefreshResult { Provider = provider, Loaded = loaded, Count = count, Note = note });
Console.WriteLine($"[OpenAiService] {provider}: {(loaded ? "geladen" : "übersprungen")} – {count} Modelle{(string.IsNullOrEmpty(note) ? "" : $" ({note})")}");
}
var nvidiaApiKey = _settings.Get("NvidiaApiKey");
if (!string.IsNullOrEmpty(nvidiaApiKey))
_nvidiaModels = await FetchModels("https://integrate.api.nvidia.com/v1/models", "Nvidia", nvidiaApiKey);
else
_nvidiaModels = await FetchModels("https://g4f.space/api/nvidia/models", "Nvidia");
Report("nvidia", _nvidiaModels.Count > 0, _nvidiaModels.Count,
_nvidiaModels.Count == 0 ? "API lieferte keine Modelle" : "");
_pollinationsModels = await FetchModels("https://text.pollinations.ai/models", "Pollinations");
Report("pollinations", _pollinationsModels.Count > 0, _pollinationsModels.Count,
_pollinationsModels.Count == 0 ? "API lieferte keine Modelle" : "");
var geminiApiKey = _settings.Get("GeminiApiKey");
if (!string.IsNullOrEmpty(geminiApiKey))
{
_geminiModels = await FetchModels("https://generativelanguage.googleapis.com/v1beta/openai/models", "Gemini", geminiApiKey);
Report("gemini", _geminiModels.Count > 0, _geminiModels.Count,
_geminiModels.Count == 0 ? "Key gesetzt, aber API lieferte nichts" : "");
}
else
Console.WriteLine("[OpenAiService] GeminiApiKey nicht konfiguriert – Gemini-Modelle nicht geladen.");
Report("gemini", false, _geminiModels.Count, "GeminiApiKey nicht konfiguriert – alte Liste bleibt");
// OpenAI: Key aus DB holen (erster aktiver OpenAI-Key)
var openaiKey = await _db.GetFirstActiveKeyByProvider("openai");
// OpenAI: Key aus Settings (OpenAiApiKey), sonst aus der api_keys-Tabelle.
var openaiKey = await ResolveOpenAiKey();
if (!string.IsNullOrEmpty(openaiKey))
{
var allOpenAi = await FetchModels("https://api.openai.com/v1/models", "OpenAI", openaiKey);
// Nur Chat-Modelle filtern (gpt-*, o1-*, o3-*, o4-*, chatgpt-*)
_openaiModels = allOpenAi
.Where(m => m.Id.StartsWith("gpt-") || m.Id.StartsWith("o1-") || m.Id.StartsWith("o3-") || m.Id.StartsWith("o4-") || m.Id.StartsWith("chatgpt-"))
.OrderBy(m => m.Id)
.ToList();
Console.WriteLine($"[OpenAiService] {_openaiModels.Count} OpenAI Chat-Modelle gefiltert (von {allOpenAi.Count} total).");
if (allOpenAi.Count > 0)
{
_openaiModels = allOpenAi
.Where(m => IsOpenAiChatModel(m.Id))
.OrderBy(m => m.Id)
.ToList();
Report("openai", true, _openaiModels.Count, $"gefiltert aus {allOpenAi.Count} total");
}
else
Report("openai", false, _openaiModels.Count, "Key gesetzt, aber /v1/models lieferte nichts (Key ungültig?) – alte Liste bleibt");
}
else
Console.WriteLine("[OpenAiService] Kein aktiver OpenAI API-Key in DB – OpenAI-Modelle nicht geladen.");
Report("openai", false, _openaiModels.Count, "kein OpenAI-Key (Setting 'OpenAiApiKey' oder api_keys-Tabelle) – alte Liste bleibt");
// MiniMax: Key aus Settings (MinimaxApiKey). Versuche /v1/models, sonst statische Fallback-Liste.
var minimaxKey = _settings.Get("MinimaxApiKey");
@@ -187,22 +249,26 @@ public class OpenAiService
{
var fetched = await FetchModels("https://api.minimax.io/v1/models", "MiniMax", minimaxKey);
_minimaxModels = fetched.Count > 0 ? fetched : MinimaxFallbackModels;
if (fetched.Count == 0)
Console.WriteLine($"[OpenAiService] MiniMax /v1/models leer/nicht erreichbar – nutze Fallback-Liste ({_minimaxModels.Count} Modelle).");
Report("minimax", fetched.Count > 0, _minimaxModels.Count,
fetched.Count == 0 ? "/v1/models leer – Fallback-Liste" : "");
}
else
{
// Ohne Key: statische Fallback-Liste anbieten, damit das Dropdown nicht leer ist.
_minimaxModels = MinimaxFallbackModels;
Console.WriteLine($"[OpenAiService] MinimaxApiKey nicht konfiguriert – nutze Fallback-Liste ({_minimaxModels.Count} Modelle).");
Report("minimax", false, _minimaxModels.Count, "MinimaxApiKey nicht konfiguriert – Fallback-Liste");
}
// z.ai / GLM: Key aus Settings (ZaiApiKey). OpenAI-kompatibler /models-Endpoint von z.ai.
var zaiKey = _settings.Get("ZaiApiKey");
if (!string.IsNullOrEmpty(zaiKey))
{
_glmModels = await FetchModels("https://api.z.ai/api/paas/v4/models", "GLM (z.ai)", zaiKey);
Report("glm", _glmModels.Count > 0, _glmModels.Count,
_glmModels.Count == 0 ? "Key gesetzt, aber API lieferte nichts" : "");
}
else
Console.WriteLine("[OpenAiService] ZaiApiKey nicht konfiguriert – GLM-Modelle nicht geladen.");
Report("glm", false, _glmModels.Count, "ZaiApiKey nicht konfiguriert – alte Liste bleibt");
// Cache speichern
try
@@ -224,6 +290,8 @@ public class OpenAiService
{
Console.WriteLine($"[OpenAiService] Fehler beim Speichern des Modelle-Cache: {ex.Message}");
}
return report;
}
private async Task<List<ProviderModel>> FetchModels(string url, string providerName, string apiKey = "")
@@ -1530,6 +1598,9 @@ public class OpenAiService
if (provider == "minimax" && string.IsNullOrEmpty(providerConfig.ApiKey))
providerConfig = (providerConfig.BaseUrl, _settings.Get("MinimaxApiKey"));
if (provider == "openai" && string.IsNullOrEmpty(providerConfig.ApiKey))
providerConfig = (providerConfig.BaseUrl, _settings.Get("OpenAiApiKey"));
return (provider, modelId, providerConfig);
}