fix: Gemini provider compatibility with Gemini 3 models

- Strip additionalProperties and $schema from tool parameter schemas
  (Gemini API rejects these unsupported JSON Schema fields)
- Add thoughtSignature to functionCall parts for multi-turn tool calling
  (Gemini 3 models require thought signatures on function call history)
- Ensure functionResponse.response is always a JSON object, never an
  array or primitive (wrap non-objects in {"result": ...})
- Resolve tool names from tool_call_id or positional index when the
  tool message doesn't carry a name field
- Add functionCall parsing in streaming responses (emitGeminiChunk)
  to properly relay tool calls via SSE
- Add error logging for Gemini stream failures with request body dump
- Add unit tests for schema cleaning and streaming tool call emission
This commit is contained in:
2026-07-17 13:41:41 +00:00
parent a187d8e20e
commit eb90f949a0
3 changed files with 337 additions and 22 deletions
+26 -2
View File
@@ -409,8 +409,12 @@ type geminiStreamChunk struct {
Candidates []struct {
Content struct {
Parts []struct {
Text string `json:"text,omitempty"`
Thought string `json:"thought,omitempty"`
Text string `json:"text,omitempty"`
Thought string `json:"thought,omitempty"`
FunctionCall *struct {
Name string `json:"name"`
Args json.RawMessage `json:"args"`
} `json:"functionCall,omitempty"`
} `json:"parts"`
} `json:"content"`
FinishReason string `json:"finishReason"`
@@ -433,6 +437,7 @@ func emitGeminiChunk(ch chan<- *models.ChatCompletionStreamResponse, chunk *gemi
content := ""
var reasoning *string
var finishReason *string
var toolCalls []models.ToolCallDelta
if len(chunk.Candidates) > 0 {
for _, p := range chunk.Candidates[0].Content.Parts {
if p.Text != "" {
@@ -444,8 +449,26 @@ func emitGeminiChunk(ch chan<- *models.ChatCompletionStreamResponse, chunk *gemi
}
*reasoning += p.Thought
}
if p.FunctionCall != nil {
name := p.FunctionCall.Name
args := string(p.FunctionCall.Args)
tcID := fmt.Sprintf("call_%s", name)
tcType := "function"
toolCalls = append(toolCalls, models.ToolCallDelta{
Index: uint32(len(toolCalls)),
ID: &tcID,
Type: &tcType,
Function: &models.FunctionCallDelta{
Name: &name,
Arguments: &args,
},
})
}
}
fr := strings.ToLower(chunk.Candidates[0].FinishReason)
if len(toolCalls) > 0 && fr == "" {
fr = "tool_calls"
}
finishReason = &fr
}
@@ -460,6 +483,7 @@ func emitGeminiChunk(ch chan<- *models.ChatCompletionStreamResponse, chunk *gemi
Delta: models.ChatStreamDelta{
Content: &content,
ReasoningContent: reasoning,
ToolCalls: toolCalls,
},
FinishReason: finishReason,
},