feat: Gemini thought_signature korrekt lesen und zurücksenden
- thought_signature aus delta.tool_calls[n].extra_content.google.thought_signature lesen (korrekter Pfad) - ThoughtSignature-Feld in MessageRecord + DB-Migration (ALTER TABLE messages ADD COLUMN thought_signature) - SaveMessage speichert ThoughtSignature pro Tool-Call - OpenAiService: toolCallAccumulators Tuple um ThoughtSignature erweitert - Tool-Calls mit thought_signature werden mit extra_content.google.thought_signature zurückgesendet (nicht function.thought_signature) - ConvertMessage (History-Rebuild) setzt thought_signature ebenfalls korrekt - ChatService.BuildApiMessages übergibt thought_signature aus DB-Record - Gemini-Modelle dynamisch laden statt hardcodiert - Tool-Call-Akkumulator nutzt id als primären Key (Gemini sendet parallele Calls ohne index) - content=null → content='' für Gemini-Kompatibilität
This commit is contained in:
1 parent
e87455ffe9
commit
145441fa7b
6 files changed
+149
-65
No files matched your search
@@ -362,22 +362,24 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop?
|
||||
try { inputEl = JsonSerializer.Deserialize<JsonElement>(msg.Content ?? "{}"); }
|
||||
catch { inputEl = JsonSerializer.Deserialize<JsonElement>("{}"); }
|
||||
|
||||
// thought_signature mitführen falls vorhanden (für Gemini-Provider)
|
||||
object toolUseBlock = !string.IsNullOrEmpty(msg.ThoughtSignature)
|
||||
? (object)new { type = "tool_use", id = msg.ToolCallId, name = msg.ToolName, input = inputEl, thought_signature = msg.ThoughtSignature }
|
||||
: new { type = "tool_use", id = msg.ToolCallId, name = msg.ToolName, input = inputEl };
|
||||
|
||||
var lastAssistant = history.Count > 0 ? history[history.Count - 1] : null;
|
||||
if (lastAssistant is Dictionary<string, object> dict &&
|
||||
dict.TryGetValue("role", out var role) && role.ToString() == "assistant" &&
|
||||
dict.TryGetValue("content", out var existingContent) && existingContent is List<object> contentList)
|
||||
{
|
||||
contentList.Add(new { type = "tool_use", id = msg.ToolCallId, name = msg.ToolName, input = inputEl });
|
||||
contentList.Add(toolUseBlock);
|
||||
}
|
||||
else
|
||||
{
|
||||
history.Add(new Dictionary<string, object>
|
||||
{
|
||||
["role"] = "assistant",
|
||||
["content"] = new List<object>
|
||||
{
|
||||
new { type = "tool_use", id = msg.ToolCallId, name = msg.ToolName, input = inputEl }
|
||||
}
|
||||
["content"] = new List<object> { toolUseBlock }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +135,7 @@ public class DatabaseService
|
||||
TryAddColumn(conn, "agents", "tool_choice", "TEXT DEFAULT 'any'");
|
||||
TryAddColumn(conn, "agents", "custom_commands", "TEXT");
|
||||
TryAddColumn(conn, "messages", "is_marked", "INTEGER NOT NULL DEFAULT 0");
|
||||
TryAddColumn(conn, "messages", "thought_signature", "TEXT");
|
||||
TryAddColumn(conn, "agents", "auto_commit", "INTEGER NOT NULL DEFAULT 0");
|
||||
|
||||
// Migration: auto_commit von INTEGER (0/1) zu TEXT ("", "mention", "forbid")
|
||||
@@ -768,8 +769,8 @@ public class DatabaseService
|
||||
using var conn = CreateConnection();
|
||||
record.CreatedAt = DateTime.UtcNow.ToString("o");
|
||||
return await conn.ExecuteScalarAsync<long>(@"
|
||||
INSERT INTO messages (chain_id, role, type, content, tool_name, tool_call_id, is_error, iteration, attachment_count, tag, created_at)
|
||||
VALUES (@ChainId, @Role, @Type, @Content, @ToolName, @ToolCallId, @IsError, @Iteration, @AttachmentCount, @Tag, @CreatedAt);
|
||||
INSERT INTO messages (chain_id, role, type, content, tool_name, tool_call_id, is_error, iteration, attachment_count, tag, thought_signature, created_at)
|
||||
VALUES (@ChainId, @Role, @Type, @Content, @ToolName, @ToolCallId, @IsError, @Iteration, @AttachmentCount, @Tag, @ThoughtSignature, @CreatedAt);
|
||||
SELECT last_insert_rowid();
|
||||
", record);
|
||||
}
|
||||
|
||||
+102
-36
@@ -173,13 +173,12 @@ public class OpenAiService
|
||||
_nvidiaModels = await FetchModels("https://g4f.space/api/nvidia/models", "Nvidia");
|
||||
_pollinationsModels = await FetchModels("https://text.pollinations.ai/models", "Pollinations");
|
||||
|
||||
// Gemini: nur kostenlose Free-Tier-Modelle (hardcodiert)
|
||||
_geminiModels = new List<ProviderModel>
|
||||
{
|
||||
new ProviderModel("gemini-2.5-pro", "Gemini 2.5 Pro"),
|
||||
new ProviderModel("gemini-2.5-flash", "Gemini 2.5 Flash"),
|
||||
new ProviderModel("gemini-2.5-flash-lite", "Gemini 2.5 Flash Lite"),
|
||||
};
|
||||
// Gemini: dynamisch von der API laden (Bearer-Token erforderlich)
|
||||
var geminiApiKey = Providers["gemini"].ApiKey;
|
||||
if (!string.IsNullOrEmpty(geminiApiKey))
|
||||
_geminiModels = await FetchModels("https://generativelanguage.googleapis.com/v1beta/openai/models", "Gemini", geminiApiKey);
|
||||
else
|
||||
Console.WriteLine("[OpenAiService] GeminiApiKey nicht konfiguriert – Gemini-Modelle nicht geladen.");
|
||||
}
|
||||
|
||||
private async Task<List<ProviderModel>> FetchModels(string url, string providerName, string apiKey = "")
|
||||
@@ -363,7 +362,8 @@ public class OpenAiService
|
||||
|
||||
var fullText = new StringBuilder();
|
||||
// Tool-Call Akkumulatoren: id → (name, arguments)
|
||||
var toolCallAccumulators = new Dictionary<int, (string Id, string Name, StringBuilder Args)>();
|
||||
var toolCallAccumulators = new Dictionary<int, (string Id, string Name, StringBuilder Args, string ThoughtSignature)>();
|
||||
string thoughtSignature = null;
|
||||
var textStarted = false;
|
||||
var stopReason = "end_turn";
|
||||
|
||||
@@ -381,13 +381,15 @@ public class OpenAiService
|
||||
catch { continue; }
|
||||
|
||||
if (!parsed.TryGetProperty("choices", out var choices)) continue;
|
||||
|
||||
|
||||
foreach (var choice in choices.EnumerateArray())
|
||||
{
|
||||
// Finish reason
|
||||
if (choice.TryGetProperty("finish_reason", out var finishProp) && finishProp.ValueKind != JsonValueKind.Null)
|
||||
{
|
||||
var fr = finishProp.GetString() ?? "";
|
||||
if (fr == "tool_calls") stopReason = "tool_use";
|
||||
var fr = (finishProp.GetString() ?? "").ToLowerInvariant();
|
||||
if (fr == "tool_calls" || fr == "function_call") stopReason = "tool_use";
|
||||
else if (fr == "stop") stopReason = "end_turn";
|
||||
}
|
||||
|
||||
@@ -409,29 +411,74 @@ public class OpenAiService
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Tool-Call Deltas akkumulieren
|
||||
if (delta.TryGetProperty("tool_calls", out var toolCallsProp))
|
||||
{
|
||||
foreach (var tc in toolCallsProp.EnumerateArray())
|
||||
{
|
||||
var idx = tc.TryGetProperty("index", out var idxProp) ? idxProp.GetInt32() : 0;
|
||||
// thought_signature aus tc.extra_content.google.thought_signature (Gemini-spezifisch)
|
||||
if (tc.TryGetProperty("extra_content", out var extraContent)
|
||||
&& extraContent.TryGetProperty("google", out var googleContent)
|
||||
&& googleContent.TryGetProperty("thought_signature", out var tcTsProp)
|
||||
&& tcTsProp.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var ts = tcTsProp.GetString();
|
||||
if (!string.IsNullOrEmpty(ts)) thoughtSignature = ts;
|
||||
}
|
||||
|
||||
// ID aus diesem Chunk lesen (kann leer sein bei Folge-Deltas)
|
||||
var chunkId = tc.TryGetProperty("id", out var idProp2) ? (idProp2.GetString() ?? "") : "";
|
||||
|
||||
// Schlüssel bestimmen: zuerst per id, dann per index
|
||||
// → verhindert Kollisionen bei parallelen Tool-Calls ohne index (z.B. Gemini)
|
||||
int idx;
|
||||
if (!string.IsNullOrEmpty(chunkId))
|
||||
{
|
||||
// Prüfen ob diese ID schon einem Index zugeordnet ist
|
||||
idx = -1;
|
||||
foreach (var kv in toolCallAccumulators)
|
||||
{
|
||||
if (kv.Value.Id == chunkId) { idx = kv.Key; break; }
|
||||
}
|
||||
if (idx == -1)
|
||||
{
|
||||
// Neue ID → nächsten freien Index vergeben
|
||||
idx = toolCallAccumulators.Count;
|
||||
while (toolCallAccumulators.ContainsKey(idx)) idx++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
idx = tc.TryGetProperty("index", out var idxProp) ? idxProp.GetInt32() : 0;
|
||||
}
|
||||
|
||||
if (!toolCallAccumulators.ContainsKey(idx))
|
||||
toolCallAccumulators[idx] = ("", "", new StringBuilder());
|
||||
toolCallAccumulators[idx] = ("", "", new StringBuilder(), null);
|
||||
|
||||
var (accId, accName, accArgs) = toolCallAccumulators[idx];
|
||||
var (accId, accName, accArgs, accTs) = toolCallAccumulators[idx];
|
||||
|
||||
if (tc.TryGetProperty("id", out var idProp))
|
||||
accId = idProp.GetString() ?? accId;
|
||||
if (!string.IsNullOrEmpty(chunkId))
|
||||
accId = chunkId;
|
||||
|
||||
if (tc.TryGetProperty("function", out var funcProp))
|
||||
{
|
||||
if (funcProp.TryGetProperty("name", out var nameProp2))
|
||||
accName = nameProp2.GetString() ?? accName;
|
||||
{
|
||||
var n = nameProp2.GetString() ?? "";
|
||||
if (!string.IsNullOrEmpty(n)) accName = n;
|
||||
}
|
||||
if (funcProp.TryGetProperty("arguments", out var argsProp))
|
||||
accArgs.Append(argsProp.GetString() ?? "");
|
||||
|
||||
}
|
||||
|
||||
toolCallAccumulators[idx] = (accId, accName, accArgs);
|
||||
// Globalen thoughtSignature-Wert auch in accTs des ersten Slots übernehmen
|
||||
if (!string.IsNullOrEmpty(thoughtSignature) && accTs == null && idx == 0)
|
||||
accTs = thoughtSignature;
|
||||
|
||||
toolCallAccumulators[idx] = (accId, accName, accArgs, accTs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -462,8 +509,8 @@ public class OpenAiService
|
||||
});
|
||||
}
|
||||
|
||||
// Keine Tool-Calls → fertig
|
||||
if (toolCallAccumulators.Count == 0 || stopReason != "tool_use")
|
||||
// Keine Tool-Calls → fertig (stopReason als Fallback, primär Count prüfen)
|
||||
if (toolCallAccumulators.Count == 0)
|
||||
{
|
||||
await FinalizeChain(chain, usage, iteration, stopReason, sw);
|
||||
await TryWriteSse(response, "done", new
|
||||
@@ -482,35 +529,51 @@ public class OpenAiService
|
||||
// Tool-Calls ausführen
|
||||
// Assistant-Message mit tool_calls in die History einfügen
|
||||
var toolCallsForHistory = new List<object>();
|
||||
foreach (var (_, (tcId, tcName, tcArgs)) in toolCallAccumulators)
|
||||
foreach (var (_, (tcId, tcName, tcArgs, tcTs)) in toolCallAccumulators)
|
||||
{
|
||||
toolCallsForHistory.Add(new
|
||||
// thought_signature pro Tool-Call mitschicken falls vorhanden
|
||||
// Gemini erwartet sie in extra_content.google.thought_signature (nicht in function.thought_signature)
|
||||
if (!string.IsNullOrEmpty(tcTs))
|
||||
{
|
||||
id = tcId,
|
||||
type = "function",
|
||||
function = new { name = tcName, arguments = tcArgs.ToString() }
|
||||
});
|
||||
toolCallsForHistory.Add(new
|
||||
{
|
||||
id = tcId,
|
||||
type = "function",
|
||||
function = new { name = tcName, arguments = tcArgs.ToString() },
|
||||
extra_content = new { google = new { thought_signature = tcTs } }
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
toolCallsForHistory.Add(new
|
||||
{
|
||||
id = tcId,
|
||||
type = "function",
|
||||
function = new { name = tcName, arguments = tcArgs.ToString() }
|
||||
});
|
||||
}
|
||||
|
||||
// Tool-Use in DB speichern
|
||||
// In DB speichern – thought_signature als eigenes Feld
|
||||
await _db.SaveMessage(new MessageRecord
|
||||
{
|
||||
ChainId = chainId, Iteration = iteration,
|
||||
Role = "assistant", Type = "tool_use",
|
||||
Content = tcArgs.ToString(),
|
||||
ToolCallId = tcId, ToolName = tcName
|
||||
ToolCallId = tcId, ToolName = tcName,
|
||||
ThoughtSignature = tcTs
|
||||
});
|
||||
}
|
||||
|
||||
apiMessages.Add(new
|
||||
{
|
||||
role = "assistant",
|
||||
content = finalText,
|
||||
content = string.IsNullOrEmpty(finalText) ? "" : finalText,
|
||||
tool_calls = toolCallsForHistory
|
||||
});
|
||||
|
||||
// Tool-Results sammeln
|
||||
var toolResults = new List<object>();
|
||||
foreach (var (_, (tcId, tcName, tcArgs)) in toolCallAccumulators)
|
||||
foreach (var (_, (tcId, tcName, tcArgs, _)) in toolCallAccumulators)
|
||||
{
|
||||
// Tool-Start SSE
|
||||
await TryWriteSse(response, "tool_start", new { name = tcName, id = tcId }, sseCt);
|
||||
@@ -632,12 +695,15 @@ public class OpenAiService
|
||||
var tcInput = block.TryGetProperty("input", out var inputProp)
|
||||
? JsonSerializer.Serialize(inputProp)
|
||||
: "{}";
|
||||
toolCalls.Add(new
|
||||
{
|
||||
id = tcId,
|
||||
type = "function",
|
||||
function = new { name = tcName, arguments = tcInput }
|
||||
});
|
||||
// thought_signature als eigenes Feld (vom DB-Record direkt in den Block übernommen)
|
||||
var tcTs = block.TryGetProperty("thought_signature", out var tsProp2) && tsProp2.ValueKind == JsonValueKind.String
|
||||
? tsProp2.GetString() : null;
|
||||
|
||||
// Gemini erwartet thought_signature in extra_content.google.thought_signature
|
||||
if (!string.IsNullOrEmpty(tcTs))
|
||||
toolCalls.Add(new { id = tcId, type = "function", function = new { name = tcName, arguments = tcInput }, extra_content = new { google = new { thought_signature = tcTs } } });
|
||||
else
|
||||
toolCalls.Add(new { id = tcId, type = "function", function = new { name = tcName, arguments = tcInput } });
|
||||
}
|
||||
else if (blockType == "tool_result")
|
||||
{
|
||||
@@ -665,7 +731,7 @@ public class OpenAiService
|
||||
return new
|
||||
{
|
||||
role = "assistant",
|
||||
content = string.IsNullOrEmpty(text) ? (object)null : text,
|
||||
content = string.IsNullOrEmpty(text) ? "" : text,
|
||||
tool_calls = toolCalls
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user