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/media"
|
||||
"github.com/sipeed/picoclaw/pkg/utils"
|
||||
"github.com/sipeed/picoclaw/pkg/voice"
|
||||
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -551,12 +553,36 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes
|
|||
if voicePath != "" {
|
||||
mediaPaths = append(mediaPaths, storeMedia(voicePath, "voice.ogg"))
|
||||
|
||||
// Check for Groq transcriber and transcribe if available, otherwise add placeholder
|
||||
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]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if message.Audio != nil {
|
||||
audioPath := c.downloadFile(ctx, message.Audio.FileID, ".mp3")
|
||||
|
|
|
|||
|
|
@ -304,13 +304,7 @@ func serializeMessages(messages []Message) []any {
|
|||
out := make([]any, 0, len(messages))
|
||||
for _, m := range messages {
|
||||
if len(m.Media) == 0 {
|
||||
out = append(out, openaiMessage{
|
||||
Role: m.Role,
|
||||
Content: m.Content,
|
||||
ReasoningContent: m.ReasoningContent,
|
||||
ToolCalls: m.ToolCalls,
|
||||
ToolCallID: m.ToolCallID,
|
||||
})
|
||||
out = append(out, convertMessage(m))
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -339,7 +333,8 @@ func serializeMessages(messages []Message) []any {
|
|||
msg["tool_call_id"] = m.ToolCallID
|
||||
}
|
||||
if len(m.ToolCalls) > 0 {
|
||||
msg["tool_calls"] = m.ToolCalls
|
||||
msg["tool_calls"] = convertToolCallsForAPI(m.ToolCalls)
|
||||
}
|
||||
}
|
||||
if m.ReasoningContent != "" {
|
||||
msg["reasoning_content"] = m.ReasoningContent
|
||||
|
|
@ -397,3 +392,87 @@ func asFloat(v any) (float64, bool) {
|
|||
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