feat(minimax): Thinking-Parsing, 1h-Cache-TTL, Null-Safety, Provider-Integration
- Thinking-Streaming: Parse 'reasoning_details' (MiniMax mit reasoning_split),
'reasoning' (Nvidia), 'reasoning_content' (DeepSeek/OpenAI-Compat),
und inline <think>...</think>-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
This commit is contained in:
1 parent
1ece62ce8c
commit
d66f8d2e73
5 files changed
+247
-42
No files matched your search
@@ -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<IResult> HandleRefresh(OpenAiService openAiService)
|
||||
{
|
||||
await openAiService.RefreshModelsFromApis();
|
||||
|
||||
+231
-41
@@ -25,7 +25,7 @@ public class OpenAiService
|
||||
private readonly ToolService _toolService;
|
||||
|
||||
// Provider-Keys (statisch für ChatService.ResolveModelId)
|
||||
public static readonly HashSet<string> ProviderKeys = new() { "nvidia", "pollinations", "gemini", "openai" };
|
||||
public static readonly HashSet<string> ProviderKeys = new() { "nvidia", "pollinations", "gemini", "openai", "minimax" };
|
||||
|
||||
// Provider-Definitionen (inkl. API-Keys, instanzgebunden)
|
||||
private readonly Dictionary<string, (string BaseUrl, string ApiKey)> Providers;
|
||||
@@ -69,6 +69,19 @@ public class OpenAiService
|
||||
private List<ProviderModel> _pollinationsModels = new List<ProviderModel>();
|
||||
private List<ProviderModel> _geminiModels = new List<ProviderModel>();
|
||||
private List<ProviderModel> _openaiModels = new List<ProviderModel>();
|
||||
private List<ProviderModel> _minimaxModels = new List<ProviderModel>();
|
||||
|
||||
// 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<ProviderModel> MinimaxFallbackModels = new List<ProviderModel>
|
||||
{
|
||||
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<ProviderModel>();
|
||||
_geminiModels = cache.ContainsKey("gemini") ? cache["gemini"] : new List<ProviderModel>();
|
||||
_openaiModels = cache.ContainsKey("openai") ? cache["openai"] : new List<ProviderModel>();
|
||||
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<ProviderModel>();
|
||||
// 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<ProviderModel> GetPollinationsModels() => _pollinationsModels;
|
||||
public List<ProviderModel> GetGeminiModels() => _geminiModels;
|
||||
public List<ProviderModel> GetOpenAiModels() => _openaiModels;
|
||||
public List<ProviderModel> GetMinimaxModels() => _minimaxModels;
|
||||
|
||||
/// <summary>
|
||||
/// 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<string, object>
|
||||
{
|
||||
["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 <think>...</think> 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-<think>-Tag-Parser für MiniMax-Modelle die Thinking als <think>...</think>
|
||||
// in delta.content streamen (statt in reasoning_details).
|
||||
// Buffer hält partielle Tags zurück (z.B. wenn ein Chunk auf "<thi" endet).
|
||||
var inlineThinkBuf = new StringBuilder();
|
||||
var inlineInThink = false;
|
||||
|
||||
string line;
|
||||
while ((line = await reader.ReadLineAsync(workCt)) != null)
|
||||
@@ -361,8 +415,9 @@ public class OpenAiService
|
||||
try { parsed = JsonSerializer.Deserialize<JsonElement>(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-<think>-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("</think>", StringComparison.Ordinal);
|
||||
if (endIdx >= 0)
|
||||
{
|
||||
// Thinking-Inhalt bis zum </think> 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 </think> 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 ("</think>".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("<think>", StringComparison.Ordinal);
|
||||
if (startIdx >= 0)
|
||||
{
|
||||
// Text vor <think> 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 <think> 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 ("<think>".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<JsonElement>(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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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" ),
|
||||
};
|
||||
|
||||
|
||||
@@ -220,6 +220,9 @@
|
||||
<optgroup label="Nvidia" id="nvidiaOptgroup">
|
||||
<option value="" disabled>Wird geladen...</option>
|
||||
</optgroup>
|
||||
<optgroup label="MiniMax" id="minimaxOptgroup">
|
||||
<option value="" disabled>Wird geladen...</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<div class="more">
|
||||
<a>Weitere Einstellungen</a>
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in new issue
Block a user