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:
1 parent
f38f3a42d1
commit
7305a5a50f
8 files changed
+136
-31
No files matched your search
@@ -111,9 +111,10 @@ public class CronjobService : BackgroundService
|
||||
useCliFallback = true;
|
||||
_logger.LogInformation("Cronjob '{Name}': Kein API-Key für Agent {AgentId}, verwende CLI-Fallback", job.Name, ctx.AgentId);
|
||||
}
|
||||
else if (ctx.Provider == "openai")
|
||||
else if (ctx.Provider == "openai" && !_openAiService.HasServerOpenAiKey())
|
||||
{
|
||||
throw new Exception($"Kein API-Key für Agent {ctx.AgentId} zugewiesen (User {job.UserId}). OpenAI benötigt zwingend einen zugewiesenen API-Key.");
|
||||
// Ohne Agent-Key greift der Server-Key aus dem Setting 'OpenAiApiKey' (analog Gemini/MiniMax).
|
||||
throw new Exception($"Kein OpenAI-Key für Agent {ctx.AgentId} (User {job.UserId}): weder zugewiesen noch als Server-Setting 'OpenAiApiKey' gesetzt.");
|
||||
}
|
||||
// sonst: nvidia/gemini/minimax/pollinations → ctx.ApiToken bleibt leer,
|
||||
// der Server-seitige Provider-Key wird in ResolveProviderConfig aufgelöst.
|
||||
@@ -233,9 +234,9 @@ public class CronjobService : BackgroundService
|
||||
notifyUseCliFallback = true;
|
||||
}
|
||||
}
|
||||
else if (notifyCtx.Provider == "openai")
|
||||
else if (notifyCtx.Provider == "openai" && !_openAiService.HasServerOpenAiKey())
|
||||
{
|
||||
_logger.LogWarning("Notify-Agent-Reaktion für Cronjob '{Name}' übersprungen: OpenAI benötigt zwingend einen Key", job.Name);
|
||||
_logger.LogWarning("Notify-Agent-Reaktion für Cronjob '{Name}' übersprungen: kein OpenAI-Key (weder zugewiesen noch Server-Setting 'OpenAiApiKey')", job.Name);
|
||||
canRunNotifyAgent = false;
|
||||
}
|
||||
// sonst: nvidia/gemini/minimax/pollinations → notifyCtx.ApiToken bleibt leer (Server-Key-Auflösung).
|
||||
|
||||
+87
-16
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ public class ServerSettingsService
|
||||
new SettingDefinition( "HuggingFaceToken", "string", "", "HuggingFace Token (optional, erhöht Rate-Limits)", "password" ),
|
||||
new SettingDefinition( "NvidiaApiKey", "string", "", "Nvidia API-Key (direkt statt g4f.space Proxy)", "password" ),
|
||||
new SettingDefinition( "MinimaxApiKey", "string", "", "MiniMax API-Key (für minimax-Provider im Chat)", "password" ),
|
||||
new SettingDefinition( "OpenAiApiKey", "string", "", "OpenAI API-Key (für openai-Provider im Chat + Modell-Liste)", "password" ),
|
||||
new SettingDefinition( "ZaiApiKey", "string", "", "z.ai API-Key (für GLM-Modelle über die Claude CLI)", "password" ),
|
||||
new SettingDefinition( "ScannerKeyTypes", "string_array", "anthropic_oauth,krea_ai", "Key-Scanner: Aktive Key-Typen", "checkboxes" ),
|
||||
};
|
||||
|
||||
@@ -201,9 +201,11 @@ public class TelegramService
|
||||
|
||||
useCliFallback = true;
|
||||
}
|
||||
else if (ctx.Provider == "openai")
|
||||
else if (ctx.Provider == "openai" && string.IsNullOrEmpty(_settings.Get("OpenAiApiKey")))
|
||||
{
|
||||
await SendMessage(chatId, botToken, "❌ Kein API-Key für diesen Agent zugewiesen. OpenAI benötigt zwingend einen Key – der Bot-Besitzer muss im Web-UI einen Key zuweisen.");
|
||||
// Ohne Agent-Key greift der Server-Key aus dem Setting 'OpenAiApiKey'
|
||||
// (analog Gemini/MiniMax). Fehlt auch der, ist OpenAI nicht nutzbar.
|
||||
await SendMessage(chatId, botToken, "❌ Kein OpenAI-Key vorhanden: weder dem Agent zugewiesen noch als Server-Setting 'OpenAiApiKey' gesetzt.");
|
||||
return;
|
||||
}
|
||||
// sonst: nvidia/gemini/minimax/pollinations → ctx.ApiToken bleibt leer (Server-Key-Auflösung).
|
||||
|
||||
Reference in new issue
Block a user