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:
+100
-20
@@ -59,6 +59,7 @@ type GeminiPart struct {
|
|||||||
InlineData *GeminiInlineData `json:"inlineData,omitempty"`
|
InlineData *GeminiInlineData `json:"inlineData,omitempty"`
|
||||||
FunctionCall *GeminiFunctionCall `json:"functionCall,omitempty"`
|
FunctionCall *GeminiFunctionCall `json:"functionCall,omitempty"`
|
||||||
FunctionResponse *GeminiFunctionResponse `json:"functionResponse,omitempty"`
|
FunctionResponse *GeminiFunctionResponse `json:"functionResponse,omitempty"`
|
||||||
|
ThoughtSignature string `json:"thoughtSignature,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GeminiInlineData struct {
|
type GeminiInlineData struct {
|
||||||
@@ -231,6 +232,7 @@ func (p *GeminiProvider) ChatCompletion(ctx context.Context, req *models.Unified
|
|||||||
Name: tc.Function.Name,
|
Name: tc.Function.Name,
|
||||||
Args: json.RawMessage(tc.Function.Arguments),
|
Args: json.RawMessage(tc.Function.Arguments),
|
||||||
},
|
},
|
||||||
|
ThoughtSignature: "skip_thought_signature_validator",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
contents = append(contents, GeminiContent{Role: "model", Parts: parts})
|
contents = append(contents, GeminiContent{Role: "model", Parts: parts})
|
||||||
@@ -256,16 +258,9 @@ func (p *GeminiProvider) ChatCompletion(ctx context.Context, req *models.Unified
|
|||||||
if len(m.Content) > 0 {
|
if len(m.Content) > 0 {
|
||||||
text = m.Content[0].Text
|
text = m.Content[0].Text
|
||||||
}
|
}
|
||||||
name := "unknown_function"
|
name := resolveToolName(m, msg.ToolCalls, j-i-1)
|
||||||
if m.Name != nil {
|
|
||||||
name = *m.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
var responseObj interface{}
|
respBytes := ensureJSONObject(text)
|
||||||
if err := json.Unmarshal([]byte(text), &responseObj); err != nil {
|
|
||||||
responseObj = map[string]interface{}{"result": text}
|
|
||||||
}
|
|
||||||
respBytes, _ := json.Marshal(responseObj)
|
|
||||||
|
|
||||||
functionParts = append(functionParts, GeminiPart{
|
functionParts = append(functionParts, GeminiPart{
|
||||||
FunctionResponse: &GeminiFunctionResponse{
|
FunctionResponse: &GeminiFunctionResponse{
|
||||||
@@ -353,7 +348,9 @@ func (p *GeminiProvider) ChatCompletion(ctx context.Context, req *models.Unified
|
|||||||
geminiTool := GeminiTool{FunctionDeclarations: []models.FunctionDef{}}
|
geminiTool := GeminiTool{FunctionDeclarations: []models.FunctionDef{}}
|
||||||
for _, t := range req.Tools {
|
for _, t := range req.Tools {
|
||||||
if t.Type == "function" {
|
if t.Type == "function" {
|
||||||
geminiTool.FunctionDeclarations = append(geminiTool.FunctionDeclarations, t.Function)
|
funcDef := t.Function
|
||||||
|
funcDef.Parameters = cleanParametersSchema(funcDef.Parameters)
|
||||||
|
geminiTool.FunctionDeclarations = append(geminiTool.FunctionDeclarations, funcDef)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(geminiTool.FunctionDeclarations) > 0 {
|
if len(geminiTool.FunctionDeclarations) > 0 {
|
||||||
@@ -509,6 +506,7 @@ func (p *GeminiProvider) ChatCompletionStream(ctx context.Context, req *models.U
|
|||||||
Name: tc.Function.Name,
|
Name: tc.Function.Name,
|
||||||
Args: json.RawMessage(tc.Function.Arguments),
|
Args: json.RawMessage(tc.Function.Arguments),
|
||||||
},
|
},
|
||||||
|
ThoughtSignature: "skip_thought_signature_validator",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
contents = append(contents, GeminiContent{Role: "model", Parts: parts})
|
contents = append(contents, GeminiContent{Role: "model", Parts: parts})
|
||||||
@@ -522,16 +520,9 @@ func (p *GeminiProvider) ChatCompletionStream(ctx context.Context, req *models.U
|
|||||||
if len(m.Content) > 0 {
|
if len(m.Content) > 0 {
|
||||||
text = m.Content[0].Text
|
text = m.Content[0].Text
|
||||||
}
|
}
|
||||||
name := "unknown_function"
|
name := resolveToolName(m, msg.ToolCalls, j-i-1)
|
||||||
if m.Name != nil {
|
|
||||||
name = *m.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
var responseObj interface{}
|
respBytes := ensureJSONObject(text)
|
||||||
if err := json.Unmarshal([]byte(text), &responseObj); err != nil {
|
|
||||||
responseObj = map[string]interface{}{"result": text}
|
|
||||||
}
|
|
||||||
respBytes, _ := json.Marshal(responseObj)
|
|
||||||
|
|
||||||
functionParts = append(functionParts, GeminiPart{
|
functionParts = append(functionParts, GeminiPart{
|
||||||
FunctionResponse: &GeminiFunctionResponse{
|
FunctionResponse: &GeminiFunctionResponse{
|
||||||
@@ -610,7 +601,9 @@ func (p *GeminiProvider) ChatCompletionStream(ctx context.Context, req *models.U
|
|||||||
geminiTool := GeminiTool{FunctionDeclarations: []models.FunctionDef{}}
|
geminiTool := GeminiTool{FunctionDeclarations: []models.FunctionDef{}}
|
||||||
for _, t := range req.Tools {
|
for _, t := range req.Tools {
|
||||||
if t.Type == "function" {
|
if t.Type == "function" {
|
||||||
geminiTool.FunctionDeclarations = append(geminiTool.FunctionDeclarations, t.Function)
|
funcDef := t.Function
|
||||||
|
funcDef.Parameters = cleanParametersSchema(funcDef.Parameters)
|
||||||
|
geminiTool.FunctionDeclarations = append(geminiTool.FunctionDeclarations, funcDef)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(geminiTool.FunctionDeclarations) > 0 {
|
if len(geminiTool.FunctionDeclarations) > 0 {
|
||||||
@@ -652,6 +645,9 @@ func (p *GeminiProvider) ChatCompletionStream(ctx context.Context, req *models.U
|
|||||||
msg = string(body)
|
msg = string(body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fmt.Printf("[Gemini-Stream] API Error %d: %s\n", resp.StatusCode(), msg)
|
||||||
|
reqJSON, _ := json.Marshal(body)
|
||||||
|
fmt.Printf("[Gemini-Stream] Request Body: %s\n", string(reqJSON))
|
||||||
return nil, fmt.Errorf("Gemini API error (%d): %s", resp.StatusCode(), msg)
|
return nil, fmt.Errorf("Gemini API error (%d): %s", resp.StatusCode(), msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -668,3 +664,87 @@ func uint32Ptr(v uint32) *uint32 {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cleanGeminiSchema(schema map[string]interface{}) {
|
||||||
|
delete(schema, "additionalProperties")
|
||||||
|
delete(schema, "$schema")
|
||||||
|
for _, v := range schema {
|
||||||
|
if subMap, ok := v.(map[string]interface{}); ok {
|
||||||
|
cleanGeminiSchema(subMap)
|
||||||
|
} else if subList, ok := v.([]interface{}); ok {
|
||||||
|
for _, item := range subList {
|
||||||
|
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||||
|
cleanGeminiSchema(itemMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanParametersSchema(raw json.RawMessage) json.RawMessage {
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
var schema map[string]interface{}
|
||||||
|
if err := json.Unmarshal(raw, &schema); err != nil {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
cleanGeminiSchema(schema)
|
||||||
|
cleaned, err := json.Marshal(schema)
|
||||||
|
if err != nil {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
return json.RawMessage(cleaned)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureJSONObject ensures that a tool result string is serialized as a JSON object
|
||||||
|
// for Gemini's functionResponse.response field, which must always be an object.
|
||||||
|
// If the text is a JSON array, primitive, or invalid JSON, it gets wrapped in {"result": ...}.
|
||||||
|
func ensureJSONObject(text string) []byte {
|
||||||
|
if text == "" {
|
||||||
|
b, _ := json.Marshal(map[string]interface{}{"result": ""})
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsed interface{}
|
||||||
|
if err := json.Unmarshal([]byte(text), &parsed); err != nil {
|
||||||
|
// Not valid JSON — wrap as string
|
||||||
|
b, _ := json.Marshal(map[string]interface{}{"result": text})
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow map types through; wrap everything else
|
||||||
|
if _, ok := parsed.(map[string]interface{}); ok {
|
||||||
|
return []byte(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's a JSON array, number, string, bool, or null — wrap it
|
||||||
|
b, _ := json.Marshal(map[string]interface{}{"result": parsed})
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveToolName determines the function name for a tool response message.
|
||||||
|
// It tries: 1) the Name field on the tool message, 2) matching by ToolCallID
|
||||||
|
// against the preceding assistant's tool calls, 3) positional index match.
|
||||||
|
func resolveToolName(toolMsg models.UnifiedMessage, toolCalls []models.ToolCall, posIndex int) string {
|
||||||
|
if toolMsg.Name != nil && *toolMsg.Name != "" {
|
||||||
|
return *toolMsg.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to match by tool_call_id
|
||||||
|
if toolMsg.ToolCallID != nil && *toolMsg.ToolCallID != "" {
|
||||||
|
for _, tc := range toolCalls {
|
||||||
|
if tc.ID == *toolMsg.ToolCallID {
|
||||||
|
return tc.Function.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Positional fallback
|
||||||
|
if posIndex >= 0 && posIndex < len(toolCalls) {
|
||||||
|
return toolCalls[posIndex].Function.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown_function"
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gophergate/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCleanParametersSchema(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "flat additionalProperties and $schema",
|
||||||
|
input: `{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"query": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["query"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}`,
|
||||||
|
expected: `{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"query": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["query"]
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nested additionalProperties",
|
||||||
|
input: `{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"user": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": true
|
||||||
|
}`,
|
||||||
|
expected: `{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"user": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nested additionalProperties in array items",
|
||||||
|
input: `{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
expected: `{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
inputRaw := json.RawMessage(tc.input)
|
||||||
|
cleanedRaw := cleanParametersSchema(inputRaw)
|
||||||
|
|
||||||
|
var cleanedMap, expectedMap map[string]interface{}
|
||||||
|
if err := json.Unmarshal(cleanedRaw, &cleanedMap); err != nil {
|
||||||
|
t.Fatalf("failed to unmarshal cleaned: %v", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(tc.expected), &expectedMap); err != nil {
|
||||||
|
t.Fatalf("failed to unmarshal expected: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(cleanedMap, expectedMap) {
|
||||||
|
t.Errorf("expected %v, got %v", expectedMap, cleanedMap)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmitGeminiChunk_ToolCalls(t *testing.T) {
|
||||||
|
ch := make(chan *models.ChatCompletionStreamResponse, 1)
|
||||||
|
defer close(ch)
|
||||||
|
|
||||||
|
chunk := &geminiStreamChunk{}
|
||||||
|
chunk.Candidates = []struct {
|
||||||
|
Content struct {
|
||||||
|
Parts []struct {
|
||||||
|
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"`
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Content: struct {
|
||||||
|
Parts []struct {
|
||||||
|
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"`
|
||||||
|
}{
|
||||||
|
Parts: []struct {
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
|
Thought string `json:"thought,omitempty"`
|
||||||
|
FunctionCall *struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Args json.RawMessage `json:"args"`
|
||||||
|
} `json:"functionCall,omitempty"`
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
FunctionCall: &struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Args json.RawMessage `json:"args"`
|
||||||
|
}{
|
||||||
|
Name: "google_search",
|
||||||
|
Args: json.RawMessage(`{"query": "test"}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
FinishReason: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
emitted := emitGeminiChunk(ch, chunk, "gemini-3-flash-preview")
|
||||||
|
if !emitted {
|
||||||
|
t.Fatalf("expected emitGeminiChunk to return true")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case resp := <-ch:
|
||||||
|
if len(resp.Choices) != 1 {
|
||||||
|
t.Fatalf("expected 1 choice, got %d", len(resp.Choices))
|
||||||
|
}
|
||||||
|
choice := resp.Choices[0]
|
||||||
|
if choice.FinishReason == nil || *choice.FinishReason != "tool_calls" {
|
||||||
|
t.Errorf("expected finish_reason 'tool_calls', got %v", choice.FinishReason)
|
||||||
|
}
|
||||||
|
if len(choice.Delta.ToolCalls) != 1 {
|
||||||
|
t.Fatalf("expected 1 tool call in delta, got %d", len(choice.Delta.ToolCalls))
|
||||||
|
}
|
||||||
|
tc := choice.Delta.ToolCalls[0]
|
||||||
|
if tc.ID == nil || *tc.ID != "call_google_search" {
|
||||||
|
t.Errorf("expected ID 'call_google_search', got %v", tc.ID)
|
||||||
|
}
|
||||||
|
if tc.Function == nil || tc.Function.Name == nil || *tc.Function.Name != "google_search" {
|
||||||
|
t.Errorf("expected function name 'google_search', got %v", tc.Function)
|
||||||
|
}
|
||||||
|
if tc.Function == nil || tc.Function.Arguments == nil || *tc.Function.Arguments != `{"query": "test"}` {
|
||||||
|
t.Errorf("expected arguments '{\"query\": \"test\"}', got %v", tc.Function)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatalf("expected response on channel")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -409,8 +409,12 @@ type geminiStreamChunk struct {
|
|||||||
Candidates []struct {
|
Candidates []struct {
|
||||||
Content struct {
|
Content struct {
|
||||||
Parts []struct {
|
Parts []struct {
|
||||||
Text string `json:"text,omitempty"`
|
Text string `json:"text,omitempty"`
|
||||||
Thought string `json:"thought,omitempty"`
|
Thought string `json:"thought,omitempty"`
|
||||||
|
FunctionCall *struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Args json.RawMessage `json:"args"`
|
||||||
|
} `json:"functionCall,omitempty"`
|
||||||
} `json:"parts"`
|
} `json:"parts"`
|
||||||
} `json:"content"`
|
} `json:"content"`
|
||||||
FinishReason string `json:"finishReason"`
|
FinishReason string `json:"finishReason"`
|
||||||
@@ -433,6 +437,7 @@ func emitGeminiChunk(ch chan<- *models.ChatCompletionStreamResponse, chunk *gemi
|
|||||||
content := ""
|
content := ""
|
||||||
var reasoning *string
|
var reasoning *string
|
||||||
var finishReason *string
|
var finishReason *string
|
||||||
|
var toolCalls []models.ToolCallDelta
|
||||||
if len(chunk.Candidates) > 0 {
|
if len(chunk.Candidates) > 0 {
|
||||||
for _, p := range chunk.Candidates[0].Content.Parts {
|
for _, p := range chunk.Candidates[0].Content.Parts {
|
||||||
if p.Text != "" {
|
if p.Text != "" {
|
||||||
@@ -444,8 +449,26 @@ func emitGeminiChunk(ch chan<- *models.ChatCompletionStreamResponse, chunk *gemi
|
|||||||
}
|
}
|
||||||
*reasoning += p.Thought
|
*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)
|
fr := strings.ToLower(chunk.Candidates[0].FinishReason)
|
||||||
|
if len(toolCalls) > 0 && fr == "" {
|
||||||
|
fr = "tool_calls"
|
||||||
|
}
|
||||||
finishReason = &fr
|
finishReason = &fr
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,6 +483,7 @@ func emitGeminiChunk(ch chan<- *models.ChatCompletionStreamResponse, chunk *gemi
|
|||||||
Delta: models.ChatStreamDelta{
|
Delta: models.ChatStreamDelta{
|
||||||
Content: &content,
|
Content: &content,
|
||||||
ReasoningContent: reasoning,
|
ReasoningContent: reasoning,
|
||||||
|
ToolCalls: toolCalls,
|
||||||
},
|
},
|
||||||
FinishReason: finishReason,
|
FinishReason: finishReason,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user