feat: Nvidia API direkt-Anbindung mit Thinking-Mode Support
Neues Server-Setting NvidiaApiKey – wenn gesetzt, wird direkt integrate.api.nvidia.com verwendet statt g4f.space Proxy. Thinking-Modelle (kimi-k2, qwq, deepseek-r1) erhalten automatisch chat_template_kwargs und reasoning-Output wird als klappbare Denkprozess-Bubble im Chat angezeigt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
1101bb83a0
commit
4960b57ec7
6 files changed
+176
-3
No files matched your search
@@ -127,7 +127,11 @@ public class OpenAiService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task RefreshModelsFromApis()
|
public async Task RefreshModelsFromApis()
|
||||||
{
|
{
|
||||||
_nvidiaModels = await FetchModels("https://g4f.space/api/nvidia/models", "Nvidia");
|
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");
|
||||||
_pollinationsModels = await FetchModels("https://text.pollinations.ai/models", "Pollinations");
|
_pollinationsModels = await FetchModels("https://text.pollinations.ai/models", "Pollinations");
|
||||||
|
|
||||||
var geminiApiKey = _settings.Get("GeminiApiKey");
|
var geminiApiKey = _settings.Get("GeminiApiKey");
|
||||||
@@ -303,6 +307,9 @@ public class OpenAiService
|
|||||||
["tool_choice"] = "auto"
|
["tool_choice"] = "auto"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (provider == "nvidia" && IsNvidiaThinkingModel(modelId))
|
||||||
|
payload["chat_template_kwargs"] = new { thinking = true };
|
||||||
|
|
||||||
var jsonPayload = JsonSerializer.Serialize(payload);
|
var jsonPayload = JsonSerializer.Serialize(payload);
|
||||||
var httpContent = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
|
var httpContent = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
@@ -333,10 +340,12 @@ public class OpenAiService
|
|||||||
using var reader = new System.IO.StreamReader(stream);
|
using var reader = new System.IO.StreamReader(stream);
|
||||||
|
|
||||||
var fullText = new StringBuilder();
|
var fullText = new StringBuilder();
|
||||||
|
var thinkingText = new StringBuilder();
|
||||||
// Tool-Call Akkumulatoren: id → (name, arguments)
|
// Tool-Call Akkumulatoren: id → (name, arguments)
|
||||||
var toolCallAccumulators = new Dictionary<int, (string Id, string Name, StringBuilder Args, string ThoughtSignature)>();
|
var toolCallAccumulators = new Dictionary<int, (string Id, string Name, StringBuilder Args, string ThoughtSignature)>();
|
||||||
string thoughtSignature = null;
|
string thoughtSignature = null;
|
||||||
var textStarted = false;
|
var textStarted = false;
|
||||||
|
var thinkingStarted = false;
|
||||||
var stopReason = "end_turn";
|
var stopReason = "end_turn";
|
||||||
|
|
||||||
string line;
|
string line;
|
||||||
@@ -367,12 +376,34 @@ public class OpenAiService
|
|||||||
|
|
||||||
if (!choice.TryGetProperty("delta", out var delta)) continue;
|
if (!choice.TryGetProperty("delta", out var delta)) continue;
|
||||||
|
|
||||||
|
// Reasoning/Thinking-Delta (Nvidia Thinking-Modelle)
|
||||||
|
if (delta.TryGetProperty("reasoning", out var reasoningProp) && reasoningProp.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
var chunk = reasoningProp.GetString();
|
||||||
|
if (!string.IsNullOrEmpty(chunk))
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Text-Delta
|
// Text-Delta
|
||||||
if (delta.TryGetProperty("content", out var contentProp) && contentProp.ValueKind == JsonValueKind.String)
|
if (delta.TryGetProperty("content", out var contentProp) && contentProp.ValueKind == JsonValueKind.String)
|
||||||
{
|
{
|
||||||
var chunk = contentProp.GetString();
|
var chunk = contentProp.GetString();
|
||||||
if (!string.IsNullOrEmpty(chunk))
|
if (!string.IsNullOrEmpty(chunk))
|
||||||
{
|
{
|
||||||
|
// Thinking-Block beenden falls noch offen
|
||||||
|
if (thinkingStarted)
|
||||||
|
{
|
||||||
|
await SseHelper.TryWriteSse(response, "thinking_end", new { }, sseCt);
|
||||||
|
thinkingStarted = false;
|
||||||
|
}
|
||||||
if (!textStarted)
|
if (!textStarted)
|
||||||
{
|
{
|
||||||
await SseHelper.TryWriteSse(response, "text_start", new { index = 0 }, sseCt);
|
await SseHelper.TryWriteSse(response, "text_start", new { index = 0 }, sseCt);
|
||||||
@@ -383,8 +414,6 @@ public class OpenAiService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Tool-Call Deltas akkumulieren
|
// Tool-Call Deltas akkumulieren
|
||||||
if (delta.TryGetProperty("tool_calls", out var toolCallsProp))
|
if (delta.TryGetProperty("tool_calls", out var toolCallsProp))
|
||||||
{
|
{
|
||||||
@@ -465,10 +494,26 @@ public class OpenAiService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Thinking-End senden falls noch offen
|
||||||
|
if (thinkingStarted)
|
||||||
|
await SseHelper.TryWriteSse(response, "thinking_end", new { }, sseCt);
|
||||||
|
|
||||||
// Text-End senden falls Text gestreamt wurde
|
// Text-End senden falls Text gestreamt wurde
|
||||||
if (textStarted)
|
if (textStarted)
|
||||||
await SseHelper.TryWriteSse(response, "text_end", new { }, sseCt);
|
await SseHelper.TryWriteSse(response, "text_end", new { }, sseCt);
|
||||||
|
|
||||||
|
// Thinking-Text in DB speichern
|
||||||
|
var finalThinking = thinkingText.ToString();
|
||||||
|
if (!string.IsNullOrEmpty(finalThinking))
|
||||||
|
{
|
||||||
|
await _db.SaveMessage(new MessageRecord
|
||||||
|
{
|
||||||
|
ChainId = chainId, Iteration = iteration,
|
||||||
|
Role = "assistant", Type = "thinking",
|
||||||
|
Content = finalThinking
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Text in DB speichern
|
// Text in DB speichern
|
||||||
var finalText = fullText.ToString();
|
var finalText = fullText.ToString();
|
||||||
if (!string.IsNullOrEmpty(finalText))
|
if (!string.IsNullOrEmpty(finalText))
|
||||||
@@ -669,6 +714,9 @@ public class OpenAiService
|
|||||||
["tool_choice"] = "auto"
|
["tool_choice"] = "auto"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (provider == "nvidia" && IsNvidiaThinkingModel(modelId))
|
||||||
|
payload["chat_template_kwargs"] = new { thinking = true };
|
||||||
|
|
||||||
var jsonPayload = JsonSerializer.Serialize(payload);
|
var jsonPayload = JsonSerializer.Serialize(payload);
|
||||||
var httpContent = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
|
var httpContent = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
@@ -709,6 +757,21 @@ public class OpenAiService
|
|||||||
var message = choice.GetProperty("message");
|
var message = choice.GetProperty("message");
|
||||||
var finishReason = choice.TryGetProperty("finish_reason", out var frProp) ? frProp.GetString() ?? "" : "";
|
var finishReason = choice.TryGetProperty("finish_reason", out var frProp) ? frProp.GetString() ?? "" : "";
|
||||||
|
|
||||||
|
// Reasoning/Thinking extrahieren (Nvidia Thinking-Modelle)
|
||||||
|
if (message.TryGetProperty("reasoning", out var reasoningProp) && reasoningProp.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
var reasoning = reasoningProp.GetString() ?? "";
|
||||||
|
if (!string.IsNullOrEmpty(reasoning))
|
||||||
|
{
|
||||||
|
await _db.SaveMessage(new MessageRecord
|
||||||
|
{
|
||||||
|
ChainId = ctx.ChainId, Iteration = iterations,
|
||||||
|
Role = "assistant", Type = "thinking",
|
||||||
|
Content = reasoning
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Text extrahieren
|
// Text extrahieren
|
||||||
if (message.TryGetProperty("content", out var contentProp) && contentProp.ValueKind == JsonValueKind.String)
|
if (message.TryGetProperty("content", out var contentProp) && contentProp.ValueKind == JsonValueKind.String)
|
||||||
{
|
{
|
||||||
@@ -909,12 +972,30 @@ public class OpenAiService
|
|||||||
if (!string.IsNullOrEmpty(apiKeyOverride))
|
if (!string.IsNullOrEmpty(apiKeyOverride))
|
||||||
providerConfig = (providerConfig.BaseUrl, apiKeyOverride);
|
providerConfig = (providerConfig.BaseUrl, apiKeyOverride);
|
||||||
|
|
||||||
|
if (provider == "nvidia" && string.IsNullOrEmpty(providerConfig.ApiKey))
|
||||||
|
{
|
||||||
|
var nvidiaKey = _settings.Get("NvidiaApiKey");
|
||||||
|
if (!string.IsNullOrEmpty(nvidiaKey))
|
||||||
|
providerConfig = ("https://integrate.api.nvidia.com/v1", nvidiaKey);
|
||||||
|
}
|
||||||
|
|
||||||
if (provider == "gemini" && string.IsNullOrEmpty(providerConfig.ApiKey))
|
if (provider == "gemini" && string.IsNullOrEmpty(providerConfig.ApiKey))
|
||||||
providerConfig = (providerConfig.BaseUrl, _settings.Get("GeminiApiKey"));
|
providerConfig = (providerConfig.BaseUrl, _settings.Get("GeminiApiKey"));
|
||||||
|
|
||||||
return (provider, modelId, providerConfig);
|
return (provider, modelId, providerConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prüft ob ein Nvidia-Modell Thinking-Mode unterstützt (chat_template_kwargs).
|
||||||
|
/// </summary>
|
||||||
|
private static bool IsNvidiaThinkingModel(string modelId)
|
||||||
|
{
|
||||||
|
// Modelle mit Extended-Thinking-Support bei Nvidia
|
||||||
|
return modelId.Contains("kimi-k2", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| modelId.Contains("qwq", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| modelId.Contains("deepseek-r1", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Konvertiert Anthropic-Format Messages in OpenAI-kompatibles Format.
|
/// Konvertiert Anthropic-Format Messages in OpenAI-kompatibles Format.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ public class ServerSettingsService
|
|||||||
new SettingDefinition( "ScanDailyHour", "int", "-1", "Key-Scanner Full-Scan Uhrzeit (0-23, -1 = deaktiviert)", "number" ),
|
new SettingDefinition( "ScanDailyHour", "int", "-1", "Key-Scanner Full-Scan Uhrzeit (0-23, -1 = deaktiviert)", "number" ),
|
||||||
new SettingDefinition( "GitHubEventScanner", "bool", "false", "GitHub Event Scanner (Echtzeit-Monitoring öffentlicher Pushes)", "toggle" ),
|
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( "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( "ScannerKeyTypes", "string_array", "anthropic_oauth,krea_ai", "Key-Scanner: Aktive Key-Typen", "checkboxes" ),
|
new SettingDefinition( "ScannerKeyTypes", "string_array", "anthropic_oauth,krea_ai", "Key-Scanner: Aktive Key-Typen", "checkboxes" ),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1126,6 +1126,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Thinking Box (Nvidia Reasoning) */
|
||||||
|
|
||||||
|
.thinking-box .tool-box-header .tool-name {
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-box .thinking-content {
|
||||||
|
padding: var(--box-padding);
|
||||||
|
}
|
||||||
|
|
||||||
/* Review Bubbles */
|
/* Review Bubbles */
|
||||||
|
|
||||||
.review-bubble {
|
.review-bubble {
|
||||||
|
|||||||
@@ -28,6 +28,18 @@ function addChainMetaBubble(d, streamed) {
|
|||||||
|
|
||||||
function handleStreamEvent(eventType, data) {
|
function handleStreamEvent(eventType, data) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
|
case 'thinking_start':
|
||||||
|
startThinkingBubble();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'thinking_delta':
|
||||||
|
appendToThinking(data.text);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'thinking_end':
|
||||||
|
endThinkingBubble();
|
||||||
|
break;
|
||||||
|
|
||||||
case 'text_start':
|
case 'text_start':
|
||||||
if (!_streamDiv) startStreamBubble();
|
if (!_streamDiv) startStreamBubble();
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -259,6 +259,24 @@ function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds, opts) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'thinking': {
|
||||||
|
if (msg.role === 'assistant' && msg.content) {
|
||||||
|
var thinkDiv = document.createElement('div');
|
||||||
|
thinkDiv.className = 'tool-box thinking-box' + (archived ? ' archived' : '');
|
||||||
|
var thinkHeader = document.createElement('div');
|
||||||
|
thinkHeader.className = 'tool-box-header clickable';
|
||||||
|
thinkHeader.innerHTML = '<span class="tool-name">Denkprozess</span>';
|
||||||
|
var thinkContent = document.createElement('div');
|
||||||
|
thinkContent.className = 'thinking-content markdown';
|
||||||
|
thinkContent.innerHTML = renderMarkdown(msg.content);
|
||||||
|
thinkContent.hidden = true;
|
||||||
|
thinkHeader.addEventListener('click', function() { thinkContent.hidden = !thinkContent.hidden; });
|
||||||
|
thinkDiv.appendChild(thinkHeader);
|
||||||
|
thinkDiv.appendChild(thinkContent);
|
||||||
|
fragment.appendChild(thinkDiv);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'tool_use':
|
case 'tool_use':
|
||||||
if (msg.toolName === 'ask_user_choice') {
|
if (msg.toolName === 'ask_user_choice') {
|
||||||
_renderChoiceBubbleInFragment(fragment, msgs, _msgIdx);
|
_renderChoiceBubbleInFragment(fragment, msgs, _msgIdx);
|
||||||
|
|||||||
@@ -52,6 +52,57 @@ function endStreamBubble() {
|
|||||||
_streamText = '';
|
_streamText = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Thinking Bubble (Nvidia Reasoning) ──
|
||||||
|
|
||||||
|
let _thinkingDiv = null;
|
||||||
|
let _thinkingText = '';
|
||||||
|
|
||||||
|
function startThinkingBubble() {
|
||||||
|
_thinkingDiv = document.createElement('div');
|
||||||
|
_thinkingDiv.className = 'tool-box thinking-box streamed';
|
||||||
|
var header = document.createElement('div');
|
||||||
|
header.className = 'tool-box-header clickable';
|
||||||
|
header.innerHTML = '<span class="tool-name">Denkprozess</span>';
|
||||||
|
var content = document.createElement('div');
|
||||||
|
content.className = 'thinking-content markdown';
|
||||||
|
content.hidden = true;
|
||||||
|
content.innerHTML = '<p><span class="cursor"></span></p>';
|
||||||
|
header.addEventListener('click', function() { content.hidden = !content.hidden; });
|
||||||
|
_thinkingDiv.appendChild(header);
|
||||||
|
_thinkingDiv.appendChild(content);
|
||||||
|
_thinkingText = '';
|
||||||
|
chatEl().appendChild(_thinkingDiv);
|
||||||
|
scrollDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendToThinking(text) {
|
||||||
|
if (!_thinkingDiv) startThinkingBubble();
|
||||||
|
_thinkingText += text;
|
||||||
|
var contentEl = _thinkingDiv.querySelector('.thinking-content');
|
||||||
|
contentEl.innerHTML = renderMarkdown(_thinkingText);
|
||||||
|
var cursor = document.createElement('span');
|
||||||
|
cursor.className = 'cursor';
|
||||||
|
var lastBlock = contentEl.lastElementChild;
|
||||||
|
if (lastBlock) lastBlock.appendChild(cursor);
|
||||||
|
else contentEl.appendChild(cursor);
|
||||||
|
scrollDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
function endThinkingBubble() {
|
||||||
|
if (_thinkingDiv) {
|
||||||
|
var cursor = _thinkingDiv.querySelector('.cursor');
|
||||||
|
if (cursor) cursor.remove();
|
||||||
|
var contentEl = _thinkingDiv.querySelector('.thinking-content');
|
||||||
|
if (!_thinkingText.trim()) _thinkingDiv.remove();
|
||||||
|
else {
|
||||||
|
contentEl.innerHTML = renderMarkdown(_thinkingText);
|
||||||
|
contentEl.hidden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_thinkingDiv = null;
|
||||||
|
_thinkingText = '';
|
||||||
|
}
|
||||||
|
|
||||||
// ── Review Bubbles (Streaming) ──
|
// ── Review Bubbles (Streaming) ──
|
||||||
|
|
||||||
let _reviewBubble = null;
|
let _reviewBubble = null;
|
||||||
|
|||||||
Reference in new issue
Block a user