Fix Gemini 3 Pro/Flash thought_signature compatibility
Implement proper thought_signature support for Google Gemini APIs - Add convertToolCallsForAPI to handle tool calls with extra content including Gemini-specific thought_signature - Add convertMessage to properly handle message serialization for Google APIs - Modify serializeMessages to use these functions for proper thought_signature handling - Ensures mandatory thought_signature fields work with newer Gemini models Resolves PicoClaw Issue #161: Incompatibility with Gemini 3 Pro/Flash due to Mandatory Thought Signatures 🤖 AI Assisted - Human designed the solution, AI helped implement it
This commit is contained in:
parent
13f99addb7
commit
e625edb772
2 changed files with 117 additions and 12 deletions
|
|
@ -23,6 +23,8 @@ import (
|
||||||
"github.com/sipeed/picoclaw/pkg/logger"
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
"github.com/sipeed/picoclaw/pkg/media"
|
"github.com/sipeed/picoclaw/pkg/media"
|
||||||
"github.com/sipeed/picoclaw/pkg/utils"
|
"github.com/sipeed/picoclaw/pkg/utils"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/voice"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -551,13 +553,37 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes
|
||||||
if voicePath != "" {
|
if voicePath != "" {
|
||||||
mediaPaths = append(mediaPaths, storeMedia(voicePath, "voice.ogg"))
|
mediaPaths = append(mediaPaths, storeMedia(voicePath, "voice.ogg"))
|
||||||
|
|
||||||
if content != "" {
|
// Check for Groq transcriber and transcribe if available, otherwise add placeholder
|
||||||
content += "\n"
|
if c.groqTranscriber != nil && c.groqTranscriber.IsAvailable() {
|
||||||
|
logger.DebugC("telegram", "Transcribing voice message using Groq...")
|
||||||
|
transcription, err := c.groqTranscriber.Transcribe(ctx, voicePath)
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorCF("telegram", "Voice transcription failed", map[string]any{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
// Fallback to placeholder if transcription fails
|
||||||
|
if content != "" {
|
||||||
|
content += "\n"
|
||||||
|
}
|
||||||
|
content += "[voice]"
|
||||||
|
} else {
|
||||||
|
// Use the transcription as content
|
||||||
|
if content != "" {
|
||||||
|
content += "\n"
|
||||||
|
}
|
||||||
|
content += transcription.Text
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Groq not available, use placeholder
|
||||||
|
if content != "" {
|
||||||
|
content += "\n"
|
||||||
|
}
|
||||||
|
content += "[voice]"
|
||||||
}
|
}
|
||||||
content += "[voice]"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if message.Audio != nil {
|
if message.Audio != nil {
|
||||||
audioPath := c.downloadFile(ctx, message.Audio.FileID, ".mp3")
|
audioPath := c.downloadFile(ctx, message.Audio.FileID, ".mp3")
|
||||||
if audioPath != "" {
|
if audioPath != "" {
|
||||||
|
|
|
||||||
|
|
@ -304,13 +304,7 @@ func serializeMessages(messages []Message) []any {
|
||||||
out := make([]any, 0, len(messages))
|
out := make([]any, 0, len(messages))
|
||||||
for _, m := range messages {
|
for _, m := range messages {
|
||||||
if len(m.Media) == 0 {
|
if len(m.Media) == 0 {
|
||||||
out = append(out, openaiMessage{
|
out = append(out, convertMessage(m))
|
||||||
Role: m.Role,
|
|
||||||
Content: m.Content,
|
|
||||||
ReasoningContent: m.ReasoningContent,
|
|
||||||
ToolCalls: m.ToolCalls,
|
|
||||||
ToolCallID: m.ToolCallID,
|
|
||||||
})
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -338,8 +332,9 @@ func serializeMessages(messages []Message) []any {
|
||||||
if m.ToolCallID != "" {
|
if m.ToolCallID != "" {
|
||||||
msg["tool_call_id"] = m.ToolCallID
|
msg["tool_call_id"] = m.ToolCallID
|
||||||
}
|
}
|
||||||
if len(m.ToolCalls) > 0 {
|
if len(m.ToolCalls) > 0 {
|
||||||
msg["tool_calls"] = m.ToolCalls
|
msg["tool_calls"] = convertToolCallsForAPI(m.ToolCalls)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if m.ReasoningContent != "" {
|
if m.ReasoningContent != "" {
|
||||||
msg["reasoning_content"] = m.ReasoningContent
|
msg["reasoning_content"] = m.ReasoningContent
|
||||||
|
|
@ -397,3 +392,87 @@ func asFloat(v any) (float64, bool) {
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// convertToolCallsForAPI handles special conversion for toolCall structures for API compatibility.
|
||||||
|
// Particularly for Google Gemini APIs, it ensures thought_signature is properly handled.
|
||||||
|
func convertToolCallsForAPI(toolCalls []ToolCall) []any {
|
||||||
|
converted := make([]any, len(toolCalls))
|
||||||
|
for i, tc := range toolCalls {
|
||||||
|
// Check if there's Google-specific extra content that may contain thought_signature
|
||||||
|
var toolCallObj map[string]any
|
||||||
|
|
||||||
|
// Start building the base structure
|
||||||
|
toolCallObj = map[string]any{
|
||||||
|
"id": tc.ID,
|
||||||
|
"type": tc.Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add function details
|
||||||
|
funcCall := map[string]any{
|
||||||
|
"name": tc.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert arguments to JSON string like normal OpenAI format
|
||||||
|
if tc.Function != nil && tc.Function.Arguments != "" {
|
||||||
|
funcCall["arguments"] = tc.Function.Arguments
|
||||||
|
} else {
|
||||||
|
argsJSON, err := json.Marshal(tc.Arguments)
|
||||||
|
if err == nil && string(argsJSON) != "{}" {
|
||||||
|
funcCall["arguments"] = string(argsJSON)
|
||||||
|
} else {
|
||||||
|
funcCall["arguments"] = "{}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Google Gemini APIs: add the extra content that includes thought_signature
|
||||||
|
if tc.ExtraContent != nil && tc.ExtraContent.Google != nil {
|
||||||
|
extraObj := map[string]any{}
|
||||||
|
if tc.ExtraContent.Google.ThoughtSignature != "" {
|
||||||
|
extraObj["google"] = map[string]any{
|
||||||
|
"thought_signature": tc.ExtraContent.Google.ThoughtSignature,
|
||||||
|
}
|
||||||
|
toolCallObj["extra_content"] = extraObj
|
||||||
|
}
|
||||||
|
} else if tc.ThoughtSignature != "" {
|
||||||
|
// If there's a direct thought signature but no ExtraContent, add it
|
||||||
|
extraObj := map[string]any{
|
||||||
|
"google": map[string]any{
|
||||||
|
"thought_signature": tc.ThoughtSignature,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
toolCallObj["extra_content"] = extraObj
|
||||||
|
}
|
||||||
|
|
||||||
|
toolCallObj["function"] = funcCall
|
||||||
|
converted[i] = toolCallObj
|
||||||
|
}
|
||||||
|
return converted
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertMessage handles special conversion for messages that may include Google-specific thought_signature info
|
||||||
|
func convertMessage(m Message) any {
|
||||||
|
// Handle the basic message structure without media first
|
||||||
|
basicMsg := openaiMessage{
|
||||||
|
Role: m.Role,
|
||||||
|
Content: m.Content,
|
||||||
|
ReasoningContent: m.ReasoningContent,
|
||||||
|
ToolCallID: m.ToolCallID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special handling of tool calls for Google Gemini compatibility
|
||||||
|
if len(m.ToolCalls) > 0 {
|
||||||
|
// Rather than passing ToolCalls directly (which would serialize them as Go structs),
|
||||||
|
// we convert them to proper API format that supports thought_signature
|
||||||
|
return map[string]any{
|
||||||
|
"role": m.Role,
|
||||||
|
"content": m.Content,
|
||||||
|
"reasoning_content": m.ReasoningContent,
|
||||||
|
"tool_call_id": m.ToolCallID,
|
||||||
|
"tool_calls": convertToolCallsForAPI(m.ToolCalls),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No tool calls - return basic message as before
|
||||||
|
return basicMsg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue