From d66f8d2e7382556fe72372892b315783fb4af89d Mon Sep 17 00:00:00 2001 From: Meleki Date: Thu, 14 May 2026 00:55:32 +0200 Subject: [PATCH] feat(minimax): Thinking-Parsing, 1h-Cache-TTL, Null-Safety, Provider-Integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Thinking-Streaming: Parse 'reasoning_details' (MiniMax mit reasoning_split), 'reasoning' (Nvidia), 'reasoning_content' (DeepSeek/OpenAI-Compat), und inline ...-Tags aus delta.content (Fallback) - MiniMax: max_tokens=32768, stream_options={include_usage}, reasoning_split=true - 1h-Prompt-Cache-TTL für API (extended-cache-ttl-2025-04-11 Header, Cache-Breakpoints auf System-Prompt + letztes Tool) und CLI (ENABLE_PROMPT_CACHING_1H=1) - Null-Safety: choices/delta/tool_calls/usage null-Checks in Streaming + Non-Streaming - finish_reason='length' → warning-Event + stopReason='max_tokens' - MiniMax-Provider: BaseUrl, MinimaxApiKey-Setting, /api/models/minimax-Endpoint, Model-Dropdown, statische Fallback-Modelle, Cache-Laden mit Fallback - Safety-Net: InvalidOperationException-Catch um SSE-Zeilen-Block --- Endpoints/ModelsEndpoint.cs | 9 + Services/OpenAiService.cs | 272 +++++++++++++++++++++++++----- Services/ServerSettingsService.cs | 1 + wwwroot/index.html | 3 + wwwroot/js/pages/agent-page.js | 4 +- 5 files changed, 247 insertions(+), 42 deletions(-) diff --git a/Endpoints/ModelsEndpoint.cs b/Endpoints/ModelsEndpoint.cs index 78f59c8..45013b6 100644 --- a/Endpoints/ModelsEndpoint.cs +++ b/Endpoints/ModelsEndpoint.cs @@ -15,6 +15,7 @@ public static class ModelsEndpoint app.MapGet("/api/models/pollinations", HandlePollinationsModels); app.MapGet("/api/models/gemini", HandleGeminiModels); app.MapGet("/api/models/openai", HandleOpenAiModels); + app.MapGet("/api/models/minimax", HandleMinimaxModels); app.MapPost("/api/models/refresh", HandleRefresh); } @@ -50,6 +51,14 @@ public static class ModelsEndpoint return Results.Json(new { models }); } + private static IResult HandleMinimaxModels(OpenAiService openAiService) + { + var models = openAiService.GetMinimaxModels() + .Select(m => new { id = m.Id, displayName = m.DisplayName }) + .ToList(); + return Results.Json(new { models }); + } + private static async Task HandleRefresh(OpenAiService openAiService) { await openAiService.RefreshModelsFromApis(); diff --git a/Services/OpenAiService.cs b/Services/OpenAiService.cs index 36048b9..ee4623e 100644 --- a/Services/OpenAiService.cs +++ b/Services/OpenAiService.cs @@ -25,7 +25,7 @@ public class OpenAiService private readonly ToolService _toolService; // Provider-Keys (statisch für ChatService.ResolveModelId) - public static readonly HashSet ProviderKeys = new() { "nvidia", "pollinations", "gemini", "openai" }; + public static readonly HashSet ProviderKeys = new() { "nvidia", "pollinations", "gemini", "openai", "minimax" }; // Provider-Definitionen (inkl. API-Keys, instanzgebunden) private readonly Dictionary Providers; @@ -69,6 +69,19 @@ public class OpenAiService private List _pollinationsModels = new List(); private List _geminiModels = new List(); private List _openaiModels = new List(); + private List _minimaxModels = new List(); + + // Fallback-Modell-Liste für MiniMax (falls /v1/models nichts liefert). + // Quelle: https://platform.minimax.io/docs/api-reference/api-overview + private static readonly List MinimaxFallbackModels = new List + { + new ProviderModel("MiniMax-M2.7", "MiniMax-M2.7"), + new ProviderModel("MiniMax-M2.7-highspeed", "MiniMax-M2.7-highspeed"), + new ProviderModel("MiniMax-M2.5", "MiniMax-M2.5"), + new ProviderModel("MiniMax-M2.5-highspeed", "MiniMax-M2.5-highspeed"), + new ProviderModel("MiniMax-M2.1", "MiniMax-M2.1"), + new ProviderModel("MiniMax-M2", "MiniMax-M2") + }; private readonly string _cacheFile; private readonly ServerSettingsService _settings; @@ -85,7 +98,8 @@ public class OpenAiService ["nvidia"] = ("https://g4f.space/api/nvidia/v1", ""), ["pollinations"] = ("https://text.pollinations.ai/openai", ""), ["gemini"] = ("https://generativelanguage.googleapis.com/v1beta/openai", ""), // Key kommt dynamisch aus Settings - ["openai"] = ("https://api.openai.com/v1", "") // Key kommt dynamisch aus DB + ["openai"] = ("https://api.openai.com/v1", ""), // Key kommt dynamisch aus DB + ["minimax"] = ("https://api.minimax.io/v1", "") // Key kommt dynamisch aus Settings (MinimaxApiKey) }; // Tool-Definitionen aus ToolService konvertieren (single source of truth) @@ -109,7 +123,15 @@ public class OpenAiService _pollinationsModels = cache.ContainsKey("pollinations") ? cache["pollinations"] : new List(); _geminiModels = cache.ContainsKey("gemini") ? cache["gemini"] : new List(); _openaiModels = cache.ContainsKey("openai") ? cache["openai"] : new List(); - Console.WriteLine($"[OpenAiService] Modelle aus Cache geladen: {_nvidiaModels.Count} Nvidia, {_pollinationsModels.Count} Pollinations, {_geminiModels.Count} Gemini, {_openaiModels.Count} OpenAI"); + _minimaxModels = cache.ContainsKey("minimax") ? cache["minimax"] : new List(); + // Falls der Cache älter als die MiniMax-Integration ist oder leer ist: + // statische Fallback-Liste verwenden, damit das Dropdown nicht leer bleibt. + if (_minimaxModels.Count == 0) + { + _minimaxModels = MinimaxFallbackModels; + Console.WriteLine($"[OpenAiService] MiniMax-Cache leer/fehlt – nutze Fallback-Liste ({_minimaxModels.Count} Modelle). Tipp: /api/models/refresh aufrufen."); + } + Console.WriteLine($"[OpenAiService] Modelle aus Cache geladen: {_nvidiaModels.Count} Nvidia, {_pollinationsModels.Count} Pollinations, {_geminiModels.Count} Gemini, {_openaiModels.Count} OpenAI, {_minimaxModels.Count} MiniMax"); return; } } @@ -155,6 +177,22 @@ public class OpenAiService else Console.WriteLine("[OpenAiService] Kein aktiver OpenAI API-Key in DB – OpenAI-Modelle nicht geladen."); + // MiniMax: Key aus Settings (MinimaxApiKey). Versuche /v1/models, sonst statische Fallback-Liste. + var minimaxKey = _settings.Get("MinimaxApiKey"); + if (!string.IsNullOrEmpty(minimaxKey)) + { + 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)."); + } + 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)."); + } + // Cache speichern try { @@ -163,7 +201,8 @@ public class OpenAiService ["nvidia"] = _nvidiaModels, ["pollinations"] = _pollinationsModels, ["gemini"] = _geminiModels, - ["openai"] = _openaiModels + ["openai"] = _openaiModels, + ["minimax"] = _minimaxModels }; var json = JsonSerializer.Serialize(cache, new JsonSerializerOptions { WriteIndented = true }); await File.WriteAllTextAsync(_cacheFile, json); @@ -222,6 +261,7 @@ public class OpenAiService public List GetPollinationsModels() => _pollinationsModels; public List GetGeminiModels() => _geminiModels; public List GetOpenAiModels() => _openaiModels; + public List GetMinimaxModels() => _minimaxModels; /// /// Gibt den Provider und das eigentliche Modell-ID aus einem kombinierten Model-String zurück. @@ -297,12 +337,15 @@ public class OpenAiService iteration++; var client = CreateClient(providerConfig.ApiKey); + // MiniMax-spezifisch: höheres Output-Budget (Thinking-Modelle brauchen viel) + var maxTokens = provider == "minimax" ? 32768 : 16384; var payload = new Dictionary { ["model"] = modelId, ["messages"] = apiMessages, ["stream"] = true, - ["max_tokens"] = 16384, + ["max_tokens"] = maxTokens, + ["stream_options"] = new { include_usage = true }, ["tools"] = _openAiToolDefs, ["tool_choice"] = "auto" }; @@ -310,6 +353,11 @@ public class OpenAiService if (provider == "nvidia" && IsNvidiaThinkingModel(modelId)) payload["chat_template_kwargs"] = new { thinking = true }; + // MiniMax-Thinking-Modelle: reasoning_split → Thinking landet in delta.reasoning_details + // statt als ... inline in delta.content (siehe MiniMax-OpenAI-Compat-Docs) + if (provider == "minimax") + payload["extra_body"] = new { reasoning_split = true }; + var jsonPayload = JsonSerializer.Serialize(payload); var httpContent = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); @@ -347,6 +395,12 @@ public class OpenAiService var textStarted = false; var thinkingStarted = false; var stopReason = "end_turn"; + var gotLengthFinish = false; + // Inline--Tag-Parser für MiniMax-Modelle die Thinking als ... + // in delta.content streamen (statt in reasoning_details). + // Buffer hält partielle Tags zurück (z.B. wenn ein Chunk auf "(data); } catch { continue; } - if (!parsed.TryGetProperty("choices", out var choices)) continue; - + try + { + if (!parsed.TryGetProperty("choices", out var choices) || choices.ValueKind != JsonValueKind.Array) goto parseUsage; foreach (var choice in choices.EnumerateArray()) { @@ -372,50 +427,153 @@ public class OpenAiService var fr = (finishProp.GetString() ?? "").ToLowerInvariant(); if (fr == "tool_calls" || fr == "function_call") stopReason = "tool_use"; else if (fr == "stop") stopReason = "end_turn"; + else if (fr == "length") { stopReason = "max_tokens"; gotLengthFinish = true; } } - if (!choice.TryGetProperty("delta", out var delta)) continue; + if (!choice.TryGetProperty("delta", out var delta) || delta.ValueKind != JsonValueKind.Object) continue; - // Reasoning/Thinking-Delta (Nvidia Thinking-Modelle) + // Reasoning/Thinking-Delta: + // - "reasoning" → Nvidia Thinking-Modelle (String) + // - "reasoning_content"→ DeepSeek/manche OpenAI-Compat-Anbieter (String) + // - "reasoning_details"→ MiniMax mit extra_body.reasoning_split (Array von {text, ...}) + string reasoningChunk = null; if (delta.TryGetProperty("reasoning", out var reasoningProp) && reasoningProp.ValueKind == JsonValueKind.String) + reasoningChunk = reasoningProp.GetString(); + else if (delta.TryGetProperty("reasoning_content", out var rcProp) && rcProp.ValueKind == JsonValueKind.String) + reasoningChunk = rcProp.GetString(); + else if (delta.TryGetProperty("reasoning_details", out var rdProp) && rdProp.ValueKind == JsonValueKind.Array) { - var chunk = reasoningProp.GetString(); - if (!string.IsNullOrEmpty(chunk)) + var sbRd = new StringBuilder(); + foreach (var rd in rdProp.EnumerateArray()) { - if (!thinkingStarted) - { - await SseHelper.TryWriteSse(response, "thinking_start", new { }, sseCt); - thinkingStarted = true; - } - thinkingText.Append(chunk); - await SseHelper.TryWriteSse(response, "thinking_delta", new { text = chunk }, sseCt); + if (rd.ValueKind == JsonValueKind.Object && rd.TryGetProperty("text", out var rdText) && rdText.ValueKind == JsonValueKind.String) + sbRd.Append(rdText.GetString()); } + if (sbRd.Length > 0) reasoningChunk = sbRd.ToString(); + } + if (!string.IsNullOrEmpty(reasoningChunk)) + { + if (!thinkingStarted) + { + await SseHelper.TryWriteSse(response, "thinking_start", new { }, sseCt); + thinkingStarted = true; + } + thinkingText.Append(reasoningChunk); + await SseHelper.TryWriteSse(response, "thinking_delta", new { text = reasoningChunk }, sseCt); } - // Text-Delta + // Text-Delta – mit Inline--Tag-Parser (Fallback für MiniMax wenn reasoning_split nicht greift) if (delta.TryGetProperty("content", out var contentProp) && contentProp.ValueKind == JsonValueKind.String) { var chunk = contentProp.GetString(); if (!string.IsNullOrEmpty(chunk)) { - // Thinking-Block beenden falls noch offen - if (thinkingStarted) + inlineThinkBuf.Append(chunk); + while (true) { - await SseHelper.TryWriteSse(response, "thinking_end", new { }, sseCt); - thinkingStarted = false; + var buf = inlineThinkBuf.ToString(); + if (inlineInThink) + { + var endIdx = buf.IndexOf("", StringComparison.Ordinal); + if (endIdx >= 0) + { + // Thinking-Inhalt bis zum emittieren, dann state-flip + var thinkPart = buf.Substring(0, endIdx); + if (thinkPart.Length > 0) + { + if (!thinkingStarted) + { + await SseHelper.TryWriteSse(response, "thinking_start", new { }, sseCt); + thinkingStarted = true; + } + thinkingText.Append(thinkPart); + await SseHelper.TryWriteSse(response, "thinking_delta", new { text = thinkPart }, sseCt); + } + inlineThinkBuf.Clear(); + inlineThinkBuf.Append(buf.Substring(endIdx + 8)); + inlineInThink = false; + continue; + } + // Kein vollständiger in Sicht – partielles Tag-Ende zurückhalten (max 7 Zeichen) + var safeLen = buf.Length; + for (int k = 1; k <= Math.Min(7, buf.Length); k++) + { + if ("".StartsWith(buf.Substring(buf.Length - k))) { safeLen = buf.Length - k; break; } + } + if (safeLen > 0) + { + var flush = buf.Substring(0, safeLen); + if (!thinkingStarted) + { + await SseHelper.TryWriteSse(response, "thinking_start", new { }, sseCt); + thinkingStarted = true; + } + thinkingText.Append(flush); + await SseHelper.TryWriteSse(response, "thinking_delta", new { text = flush }, sseCt); + inlineThinkBuf.Clear(); + inlineThinkBuf.Append(buf.Substring(safeLen)); + } + break; + } + else + { + var startIdx = buf.IndexOf("", StringComparison.Ordinal); + if (startIdx >= 0) + { + // Text vor als Text emittieren, dann state-flip + var textPart = buf.Substring(0, startIdx); + if (textPart.Length > 0) + { + if (thinkingStarted) + { + await SseHelper.TryWriteSse(response, "thinking_end", new { }, sseCt); + thinkingStarted = false; + } + if (!textStarted) + { + await SseHelper.TryWriteSse(response, "text_start", new { index = 0 }, sseCt); + textStarted = true; + } + fullText.Append(textPart); + await SseHelper.TryWriteSse(response, "text_delta", new { text = textPart }, sseCt); + } + inlineThinkBuf.Clear(); + inlineThinkBuf.Append(buf.Substring(startIdx + 7)); + inlineInThink = true; + continue; + } + // Kein vollständiger in Sicht – partielles Tag-Ende zurückhalten (max 6 Zeichen) + var safeLen = buf.Length; + for (int k = 1; k <= Math.Min(6, buf.Length); k++) + { + if ("".StartsWith(buf.Substring(buf.Length - k))) { safeLen = buf.Length - k; break; } + } + if (safeLen > 0) + { + var flush = buf.Substring(0, safeLen); + if (thinkingStarted) + { + await SseHelper.TryWriteSse(response, "thinking_end", new { }, sseCt); + thinkingStarted = false; + } + if (!textStarted) + { + await SseHelper.TryWriteSse(response, "text_start", new { index = 0 }, sseCt); + textStarted = true; + } + fullText.Append(flush); + await SseHelper.TryWriteSse(response, "text_delta", new { text = flush }, sseCt); + inlineThinkBuf.Clear(); + inlineThinkBuf.Append(buf.Substring(safeLen)); + } + break; + } } - if (!textStarted) - { - await SseHelper.TryWriteSse(response, "text_start", new { index = 0 }, sseCt); - textStarted = true; - } - fullText.Append(chunk); - await SseHelper.TryWriteSse(response, "text_delta", new { text = chunk }, sseCt); } } // Tool-Call Deltas akkumulieren - if (delta.TryGetProperty("tool_calls", out var toolCallsProp)) + if (delta.TryGetProperty("tool_calls", out var toolCallsProp) && toolCallsProp.ValueKind == JsonValueKind.Array) { foreach (var tc in toolCallsProp.EnumerateArray()) { @@ -484,14 +642,37 @@ public class OpenAiService } } + parseUsage: // Usage aus letztem Chunk - if (parsed.TryGetProperty("usage", out var usageProp)) + if (parsed.TryGetProperty("usage", out var usageProp) && usageProp.ValueKind == JsonValueKind.Object) { - if (usageProp.TryGetProperty("prompt_tokens", out var pt)) + if (usageProp.TryGetProperty("prompt_tokens", out var pt) && pt.ValueKind == JsonValueKind.Number) usage.InputTokens = pt.GetInt64(); - if (usageProp.TryGetProperty("completion_tokens", out var ct2)) + if (usageProp.TryGetProperty("completion_tokens", out var ct2) && ct2.ValueKind == JsonValueKind.Number) usage.OutputTokens = ct2.GetInt64(); } + } + catch (InvalidOperationException) { /* Unerwartetes JSON-Format von Provider – Zeile überspringen */ } + } + + // Inline-Think-Buffer-Rest flushen (falls Stream mit partiellem Tag-Fragment endete) + if (inlineThinkBuf.Length > 0) + { + var tail = inlineThinkBuf.ToString(); + if (inlineInThink) + { + if (!thinkingStarted) { await SseHelper.TryWriteSse(response, "thinking_start", new { }, sseCt); thinkingStarted = true; } + thinkingText.Append(tail); + await SseHelper.TryWriteSse(response, "thinking_delta", new { text = tail }, sseCt); + } + else + { + if (thinkingStarted) { await SseHelper.TryWriteSse(response, "thinking_end", new { }, sseCt); thinkingStarted = false; } + if (!textStarted) { await SseHelper.TryWriteSse(response, "text_start", new { index = 0 }, sseCt); textStarted = true; } + fullText.Append(tail); + await SseHelper.TryWriteSse(response, "text_delta", new { text = tail }, sseCt); + } + inlineThinkBuf.Clear(); } // Thinking-End senden falls noch offen @@ -502,6 +683,13 @@ public class OpenAiService if (textStarted) await SseHelper.TryWriteSse(response, "text_end", new { }, sseCt); + // Warnung wenn finish_reason="length" → User soll wissen dass Output gekürzt wurde + if (gotLengthFinish) + { + var warnMsg = $"Antwort wurde wegen max_tokens-Limit ({maxTokens}) gekürzt. Erhöhe das Limit oder lass das Modell die Antwort fortsetzen."; + await SseHelper.TryWriteSse(response, "warning", new { message = warnMsg }, sseCt); + } + // Thinking-Text in DB speichern var finalThinking = thinkingText.ToString(); if (!string.IsNullOrEmpty(finalThinking)) @@ -741,20 +929,19 @@ public class OpenAiService var resp = JsonSerializer.Deserialize(respBody); // Usage tracken - if (resp.TryGetProperty("usage", out var usageProp)) + if (resp.TryGetProperty("usage", out var usageProp) && usageProp.ValueKind == JsonValueKind.Object) { - if (usageProp.TryGetProperty("prompt_tokens", out var pt)) + if (usageProp.TryGetProperty("prompt_tokens", out var pt) && pt.ValueKind == JsonValueKind.Number) usage.InputTokens = pt.GetInt64(); - if (usageProp.TryGetProperty("completion_tokens", out var ct2)) + if (usageProp.TryGetProperty("completion_tokens", out var ct2) && ct2.ValueKind == JsonValueKind.Number) usage.OutputTokens = ct2.GetInt64(); } // Erste Choice auswerten - var choices = resp.GetProperty("choices"); - if (choices.GetArrayLength() == 0) break; + if (!resp.TryGetProperty("choices", out var choices) || choices.ValueKind != JsonValueKind.Array || choices.GetArrayLength() == 0) break; var choice = choices[0]; - var message = choice.GetProperty("message"); + if (!choice.TryGetProperty("message", out var message) || message.ValueKind != JsonValueKind.Object) break; var finishReason = choice.TryGetProperty("finish_reason", out var frProp) ? frProp.GetString() ?? "" : ""; // Reasoning/Thinking extrahieren (Nvidia Thinking-Modelle) @@ -789,7 +976,7 @@ public class OpenAiService } // Tool-Calls prüfen - if (!message.TryGetProperty("tool_calls", out var toolCallsProp) || toolCallsProp.GetArrayLength() == 0) + if (!message.TryGetProperty("tool_calls", out var toolCallsProp) || toolCallsProp.ValueKind != JsonValueKind.Array || toolCallsProp.GetArrayLength() == 0) { await FinalizeChain(chain, usage, iterations, "end_turn", sw); break; @@ -982,6 +1169,9 @@ public class OpenAiService if (provider == "gemini" && string.IsNullOrEmpty(providerConfig.ApiKey)) providerConfig = (providerConfig.BaseUrl, _settings.Get("GeminiApiKey")); + if (provider == "minimax" && string.IsNullOrEmpty(providerConfig.ApiKey)) + providerConfig = (providerConfig.BaseUrl, _settings.Get("MinimaxApiKey")); + return (provider, modelId, providerConfig); } diff --git a/Services/ServerSettingsService.cs b/Services/ServerSettingsService.cs index 39c9ca7..7c4f101 100644 --- a/Services/ServerSettingsService.cs +++ b/Services/ServerSettingsService.cs @@ -37,6 +37,7 @@ public class ServerSettingsService new SettingDefinition( "GitHubEventScanner", "bool", "false", "GitHub Event Scanner (Echtzeit-Monitoring öffentlicher Pushes)", "toggle" ), 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( "ScannerKeyTypes", "string_array", "anthropic_oauth,krea_ai", "Key-Scanner: Aktive Key-Typen", "checkboxes" ), }; diff --git a/wwwroot/index.html b/wwwroot/index.html index aa38636..7394cb7 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -220,6 +220,9 @@ + + +
Weitere Einstellungen diff --git a/wwwroot/js/pages/agent-page.js b/wwwroot/js/pages/agent-page.js index 3bdcaf8..f123e52 100644 --- a/wwwroot/js/pages/agent-page.js +++ b/wwwroot/js/pages/agent-page.js @@ -27,6 +27,7 @@ registerPage('agent', { const pollinationsOptgroup = document.getElementById('pollinationsOptgroup'); const geminiOptgroup = document.getElementById('geminiOptgroup'); const openaiOptgroup = document.getElementById('openaiOptgroup'); + const minimaxOptgroup = document.getElementById('minimaxOptgroup'); // Modelle dynamisch laden const loadProviderModels = async (url, prefix, optgroup) => { @@ -53,7 +54,8 @@ registerPage('agent', { loadProviderModels('/api/models/openai', 'openai', openaiOptgroup), loadProviderModels('/api/models/nvidia', 'nvidia', nvidiaOptgroup), loadProviderModels('/api/models/pollinations', 'pollinations', pollinationsOptgroup), - loadProviderModels('/api/models/gemini', 'gemini', geminiOptgroup) + loadProviderModels('/api/models/gemini', 'gemini', geminiOptgroup), + loadProviderModels('/api/models/minimax', 'minimax', minimaxOptgroup) ]); const allowUserChoiceEl = document.getElementById('agentAllowUserChoice'); const autoCommitEl = document.getElementById('agentAutoCommit');