mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-10-16 12:37:24 +08:00
feat(translator): support strict tool mode mapping for gemini and claude
- Map strict tools to `VALIDATED` function calling mode in Gemini translators when tool choice is auto or omitted - Preserve tool `strict` setting when translating OpenAI requests to Claude Closes: #5958
This commit is contained in:
@@ -368,6 +368,17 @@ func convertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream,
|
||||
if !gjson.GetBytes(anthropicTool, "cache_control").Exists() {
|
||||
anthropicTool = common.AttachCacheControl(anthropicTool, function)
|
||||
}
|
||||
strict := function.Get("strict")
|
||||
if !strict.Exists() {
|
||||
strict = tool.Get("strict")
|
||||
}
|
||||
if strict.Exists() {
|
||||
if strict.Type == gjson.True {
|
||||
anthropicTool, _ = sjson.SetBytes(anthropicTool, "strict", true)
|
||||
} else if strict.Type == gjson.False {
|
||||
anthropicTool, _ = sjson.SetBytes(anthropicTool, "strict", false)
|
||||
}
|
||||
}
|
||||
|
||||
anthropicTools = append(anthropicTools, anthropicTool)
|
||||
}
|
||||
|
||||
@@ -1188,3 +1188,101 @@ func TestConvertOpenAIRequestToClaude_ToolChoice(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConvertOpenAIRequestToClaude_ToolStrict(t *testing.T) {
|
||||
t.Run("preserves strict true on function tool", func(t *testing.T) {
|
||||
inputJSON := `{
|
||||
"model": "claude-sonnet-4-6",
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "tool_a",
|
||||
"description": "Controlled tool.",
|
||||
"strict": true,
|
||||
"parameters": {"type": "object", "properties": {}}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
result := ConvertOpenAIRequestToClaude("claude-sonnet-4-6", []byte(inputJSON), false)
|
||||
toolStrict := gjson.GetBytes(result, "tools.0.strict")
|
||||
if !toolStrict.Exists() {
|
||||
t.Fatalf("expected tools.0.strict to exist in Claude output: %s", result)
|
||||
}
|
||||
if !toolStrict.Bool() {
|
||||
t.Fatalf("expected tools.0.strict to be true, got %v", toolStrict.Value())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("preserves strict true when on top level tool", func(t *testing.T) {
|
||||
inputJSON := `{
|
||||
"model": "claude-sonnet-4-6",
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"strict": true,
|
||||
"function": {
|
||||
"name": "tool_b",
|
||||
"description": "Controlled tool.",
|
||||
"parameters": {"type": "object", "properties": {}}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
result := ConvertOpenAIRequestToClaude("claude-sonnet-4-6", []byte(inputJSON), false)
|
||||
toolStrict := gjson.GetBytes(result, "tools.0.strict")
|
||||
if !toolStrict.Exists() || !toolStrict.Bool() {
|
||||
t.Fatalf("expected tools.0.strict to be true, got %s", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("preserves strict false on function tool", func(t *testing.T) {
|
||||
inputJSON := `{
|
||||
"model": "claude-sonnet-4-6",
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "tool_c",
|
||||
"description": "Controlled tool.",
|
||||
"strict": false,
|
||||
"parameters": {"type": "object", "properties": {}}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
result := ConvertOpenAIRequestToClaude("claude-sonnet-4-6", []byte(inputJSON), false)
|
||||
toolStrict := gjson.GetBytes(result, "tools.0.strict")
|
||||
if !toolStrict.Exists() {
|
||||
t.Fatalf("expected tools.0.strict to exist in Claude output: %s", result)
|
||||
}
|
||||
if toolStrict.Bool() {
|
||||
t.Fatalf("expected tools.0.strict to be false, got %v", toolStrict.Value())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("omits strict when not provided", func(t *testing.T) {
|
||||
inputJSON := `{
|
||||
"model": "claude-sonnet-4-6",
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "tool_d",
|
||||
"description": "Controlled tool.",
|
||||
"parameters": {"type": "object", "properties": {}}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
result := ConvertOpenAIRequestToClaude("claude-sonnet-4-6", []byte(inputJSON), false)
|
||||
if gjson.GetBytes(result, "tools.0.strict").Exists() {
|
||||
t.Fatalf("expected tools.0.strict to be omitted when not provided, got %s", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -239,9 +239,13 @@ func convertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool,
|
||||
}
|
||||
|
||||
// tools
|
||||
var toolItems [][]byte
|
||||
hasStrictTool := false
|
||||
if toolsResult := gjson.GetBytes(rawJSON, "tools"); toolsResult.IsArray() {
|
||||
var toolItems [][]byte
|
||||
toolsResult.ForEach(func(_, toolResult gjson.Result) bool {
|
||||
if toolResult.Get("strict").Type == gjson.True {
|
||||
hasStrictTool = true
|
||||
}
|
||||
inputSchemaResult := toolResult.Get("input_schema")
|
||||
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
|
||||
inputSchema := util.CleanJSONSchemaForGemini(inputSchemaResult.Raw)
|
||||
@@ -281,7 +285,7 @@ func convertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool,
|
||||
|
||||
// tool_choice
|
||||
toolChoiceResult := gjson.GetBytes(rawJSON, "tool_choice")
|
||||
if toolChoiceResult.Exists() {
|
||||
if toolChoiceResult.Exists() && toolChoiceResult.Type != gjson.Null {
|
||||
toolChoiceType := ""
|
||||
toolChoiceName := ""
|
||||
if toolChoiceResult.IsObject() {
|
||||
@@ -293,7 +297,11 @@ func convertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool,
|
||||
|
||||
switch toolChoiceType {
|
||||
case "auto":
|
||||
out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.mode", "AUTO")
|
||||
if hasStrictTool {
|
||||
out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.mode", "VALIDATED")
|
||||
} else {
|
||||
out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.mode", "AUTO")
|
||||
}
|
||||
case "none":
|
||||
out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.mode", "NONE")
|
||||
case "any":
|
||||
@@ -304,6 +312,8 @@ func convertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool,
|
||||
out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.allowedFunctionNames", []string{util.SanitizeFunctionName(toolChoiceName)})
|
||||
}
|
||||
}
|
||||
} else if hasStrictTool && len(toolItems) > 0 {
|
||||
out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.mode", "VALIDATED")
|
||||
}
|
||||
|
||||
// Map Anthropic thinking -> Gemini thinking config when enabled
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package claude
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
internalsignature "github.com/router-for-me/CLIProxyAPI/v7/internal/signature"
|
||||
@@ -492,3 +493,132 @@ func TestConvertClaudeRequestToGemini_ToolResultWithTrailingSystemReminderReorde
|
||||
t.Fatalf("unexpected functionResponse.id in parts[1]: %q", gotID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertClaudeRequestToGemini_ToolStrictMapsToValidatedMode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
toolChoice string
|
||||
expectedMode string
|
||||
allowedNames []string
|
||||
}{
|
||||
{
|
||||
name: "absent tool_choice maps to VALIDATED",
|
||||
toolChoice: "",
|
||||
expectedMode: "VALIDATED",
|
||||
},
|
||||
{
|
||||
name: "explicit auto tool_choice maps to VALIDATED",
|
||||
toolChoice: `"tool_choice": {"type": "auto"},`,
|
||||
expectedMode: "VALIDATED",
|
||||
},
|
||||
{
|
||||
name: "null tool_choice maps to VALIDATED",
|
||||
toolChoice: `"tool_choice": null,`,
|
||||
expectedMode: "VALIDATED",
|
||||
},
|
||||
{
|
||||
name: "none tool_choice maps to NONE",
|
||||
toolChoice: `"tool_choice": {"type": "none"},`,
|
||||
expectedMode: "NONE",
|
||||
},
|
||||
{
|
||||
name: "any tool_choice maps to ANY",
|
||||
toolChoice: `"tool_choice": {"type": "any"},`,
|
||||
expectedMode: "ANY",
|
||||
},
|
||||
{
|
||||
name: "specific tool maps to ANY with allowedFunctionNames",
|
||||
toolChoice: `"tool_choice": {"type": "tool", "name": "tool_a"},`,
|
||||
expectedMode: "ANY",
|
||||
allowedNames: []string{"tool_a"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
inputJSON := fmt.Sprintf(`{
|
||||
"model": "gemini-3.8-flash",
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
%s
|
||||
"tools": [
|
||||
{
|
||||
"name": "tool_a",
|
||||
"description": "Controlled tool.",
|
||||
"strict": true,
|
||||
"input_schema": {"type": "object", "properties": {}}
|
||||
}
|
||||
]
|
||||
}`, tt.toolChoice)
|
||||
|
||||
result := ConvertClaudeRequestToGemini("gemini-3.8-flash", []byte(inputJSON), false)
|
||||
if gjson.GetBytes(result, "tools.0.functionDeclarations.0.strict").Exists() {
|
||||
t.Fatalf("strict must be removed from functionDeclarations: %s", result)
|
||||
}
|
||||
mode := gjson.GetBytes(result, "toolConfig.functionCallingConfig.mode").String()
|
||||
if mode != tt.expectedMode {
|
||||
t.Fatalf("expected toolConfig.functionCallingConfig.mode = %q, got %q. Output: %s", tt.expectedMode, mode, result)
|
||||
}
|
||||
if len(tt.allowedNames) > 0 {
|
||||
allowed := gjson.GetBytes(result, "toolConfig.functionCallingConfig.allowedFunctionNames").Array()
|
||||
if len(allowed) != len(tt.allowedNames) {
|
||||
t.Fatalf("expected %d allowedFunctionNames, got %d", len(tt.allowedNames), len(allowed))
|
||||
}
|
||||
for i, name := range tt.allowedNames {
|
||||
if allowed[i].String() != name {
|
||||
t.Fatalf("allowedFunctionNames[%d] = %q, want %q", i, allowed[i].String(), name)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("mixed tools where one is strict maps to VALIDATED", func(t *testing.T) {
|
||||
inputJSON := []byte(`{
|
||||
"model": "gemini-3.8-flash",
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
"tools": [
|
||||
{
|
||||
"name": "tool_a",
|
||||
"description": "Loose tool.",
|
||||
"strict": false,
|
||||
"input_schema": {"type": "object", "properties": {}}
|
||||
},
|
||||
{
|
||||
"name": "tool_b",
|
||||
"description": "Strict tool.",
|
||||
"strict": true,
|
||||
"input_schema": {"type": "object", "properties": {}}
|
||||
}
|
||||
]
|
||||
}`)
|
||||
result := ConvertClaudeRequestToGemini("gemini-3.8-flash", inputJSON, false)
|
||||
mode := gjson.GetBytes(result, "toolConfig.functionCallingConfig.mode").String()
|
||||
if mode != "VALIDATED" {
|
||||
t.Fatalf("expected mode = 'VALIDATED' for mixed tools, got %q", mode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-strict tools omit toolConfig mode", func(t *testing.T) {
|
||||
inputJSON := []byte(`{
|
||||
"model": "gemini-3.8-flash",
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
"tools": [
|
||||
{
|
||||
"name": "tool_a",
|
||||
"description": "Loose tool.",
|
||||
"strict": false,
|
||||
"input_schema": {"type": "object", "properties": {}}
|
||||
},
|
||||
{
|
||||
"name": "tool_b",
|
||||
"description": "Unspecified tool.",
|
||||
"input_schema": {"type": "object", "properties": {}}
|
||||
}
|
||||
]
|
||||
}`)
|
||||
result := ConvertClaudeRequestToGemini("gemini-3.8-flash", inputJSON, false)
|
||||
if gjson.GetBytes(result, "toolConfig").Exists() {
|
||||
t.Fatalf("expected toolConfig not to be set when no strict tools and no tool_choice, got: %s", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -327,6 +327,7 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
||||
declaredOriginalToSanitized := make(map[string]string)
|
||||
sanitizedToOriginalCounts := make(map[string]int)
|
||||
var functionDeclarations [][]byte
|
||||
hasStrictTool := false
|
||||
tools := gjson.GetBytes(rawJSON, "tools")
|
||||
toolResults := tools.Array()
|
||||
if tools.IsArray() && len(toolResults) > 0 {
|
||||
@@ -394,8 +395,20 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
||||
fnRawBytes, _ = sjson.SetRawBytes(fnRawBytes, "parametersJsonSchema", []byte(cleanedParameters))
|
||||
}
|
||||
}
|
||||
if gjson.GetBytes(fnRawBytes, "strict").Exists() {
|
||||
fnRawBytes, _ = sjson.DeleteBytes(fnRawBytes, "strict")
|
||||
strictVal := gjson.GetBytes(fnRawBytes, "strict")
|
||||
if !strictVal.Exists() {
|
||||
strictVal = fn.Get("strict")
|
||||
if !strictVal.Exists() {
|
||||
strictVal = t.Get("strict")
|
||||
}
|
||||
}
|
||||
if strictVal.Exists() {
|
||||
if strictVal.Type == gjson.True {
|
||||
hasStrictTool = true
|
||||
}
|
||||
if gjson.GetBytes(fnRawBytes, "strict").Exists() {
|
||||
fnRawBytes, _ = sjson.DeleteBytes(fnRawBytes, "strict")
|
||||
}
|
||||
}
|
||||
functionDeclarations = append(functionDeclarations, fnRawBytes)
|
||||
}
|
||||
@@ -468,6 +481,8 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
||||
allowedList = append(allowedList, gjson.GetBytes(fnRaw, "name").String())
|
||||
}
|
||||
out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.allowedFunctionNames", allowedList)
|
||||
} else if hasStrictTool {
|
||||
out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.mode", "VALIDATED")
|
||||
} else {
|
||||
// Mode AUTO: functionDeclarations contains only allowed tools, mode is AUTO without allowedFunctionNames.
|
||||
out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.mode", "AUTO")
|
||||
@@ -482,7 +497,11 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
||||
|
||||
switch toolChoiceType {
|
||||
case "auto":
|
||||
out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.mode", "AUTO")
|
||||
if hasStrictTool {
|
||||
out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.mode", "VALIDATED")
|
||||
} else {
|
||||
out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.mode", "AUTO")
|
||||
}
|
||||
case "none":
|
||||
out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.mode", "NONE")
|
||||
case "required", "any":
|
||||
@@ -504,6 +523,8 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
||||
// Unrecognized tool_choice type: fail-closed.
|
||||
out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.mode", "NONE")
|
||||
}
|
||||
} else if hasStrictTool && len(functionDeclarations) > 0 {
|
||||
out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.mode", "VALIDATED")
|
||||
}
|
||||
|
||||
// parallel_tool_calls handling:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package chat_completions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/signature"
|
||||
@@ -999,3 +1000,157 @@ func TestConvertOpenAIRequestToGemini_ToolChoice(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConvertOpenAIRequestToGemini_ToolStrictMapsToValidatedMode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
toolChoice string // empty means absent
|
||||
expectedMode string
|
||||
allowedNames []string
|
||||
}{
|
||||
{
|
||||
name: "absent tool_choice maps to VALIDATED",
|
||||
toolChoice: "",
|
||||
expectedMode: "VALIDATED",
|
||||
},
|
||||
{
|
||||
name: "explicit auto tool_choice maps to VALIDATED",
|
||||
toolChoice: `"tool_choice": "auto",`,
|
||||
expectedMode: "VALIDATED",
|
||||
},
|
||||
{
|
||||
name: "null tool_choice maps to VALIDATED",
|
||||
toolChoice: `"tool_choice": null,`,
|
||||
expectedMode: "VALIDATED",
|
||||
},
|
||||
{
|
||||
name: "required tool_choice maps to ANY",
|
||||
toolChoice: `"tool_choice": "required",`,
|
||||
expectedMode: "ANY",
|
||||
},
|
||||
{
|
||||
name: "none tool_choice maps to NONE",
|
||||
toolChoice: `"tool_choice": "none",`,
|
||||
expectedMode: "NONE",
|
||||
},
|
||||
{
|
||||
name: "specific function tool_choice maps to ANY with allowedFunctionNames",
|
||||
toolChoice: `"tool_choice": {"type": "function", "function": {"name": "tool_a"}},`,
|
||||
expectedMode: "ANY",
|
||||
allowedNames: []string{"tool_a"},
|
||||
},
|
||||
{
|
||||
name: "allowed_tools with auto mode and strict tool maps to VALIDATED",
|
||||
toolChoice: `"tool_choice": {"type": "allowed_tools", "allowed_tools": {"mode": "auto", "tools": [{"type": "function", "function": {"name": "tool_a"}}]}},`,
|
||||
expectedMode: "VALIDATED",
|
||||
},
|
||||
{
|
||||
name: "allowed_tools with required mode and strict tool maps to ANY",
|
||||
toolChoice: `"tool_choice": {"type": "allowed_tools", "allowed_tools": {"mode": "required", "tools": [{"type": "function", "function": {"name": "tool_a"}}]}},`,
|
||||
expectedMode: "ANY",
|
||||
allowedNames: []string{"tool_a"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
inputJSON := fmt.Sprintf(`{
|
||||
"model": "gemini-3.1-pro-high",
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
%s
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "tool_a",
|
||||
"description": "Controlled tool.",
|
||||
"strict": true,
|
||||
"parameters": {"type": "object", "properties": {}}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`, tt.toolChoice)
|
||||
result := ConvertOpenAIRequestToGemini("gemini-3.1-pro-high", []byte(inputJSON), false)
|
||||
if gjson.GetBytes(result, "tools.0.functionDeclarations.0.strict").Exists() {
|
||||
t.Fatalf("tools.0.functionDeclarations.0.strict should be removed from function declaration: %s", result)
|
||||
}
|
||||
mode := gjson.GetBytes(result, "toolConfig.functionCallingConfig.mode").String()
|
||||
if mode != tt.expectedMode {
|
||||
t.Fatalf("expected toolConfig.functionCallingConfig.mode = %q, got %q. Output: %s", tt.expectedMode, mode, result)
|
||||
}
|
||||
if len(tt.allowedNames) > 0 {
|
||||
allowed := gjson.GetBytes(result, "toolConfig.functionCallingConfig.allowedFunctionNames").Array()
|
||||
if len(allowed) != len(tt.allowedNames) {
|
||||
t.Fatalf("expected %d allowedFunctionNames, got %d", len(tt.allowedNames), len(allowed))
|
||||
}
|
||||
for i, name := range tt.allowedNames {
|
||||
if allowed[i].String() != name {
|
||||
t.Fatalf("allowedFunctionNames[%d] = %q, want %q", i, allowed[i].String(), name)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("mixed tools where one is strict maps to VALIDATED", func(t *testing.T) {
|
||||
inputJSON := `{
|
||||
"model": "gemini-3.1-pro-high",
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "tool_a",
|
||||
"description": "Loose tool.",
|
||||
"strict": false,
|
||||
"parameters": {"type": "object", "properties": {}}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "tool_b",
|
||||
"description": "Strict tool.",
|
||||
"strict": true,
|
||||
"parameters": {"type": "object", "properties": {}}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
result := ConvertOpenAIRequestToGemini("gemini-3.1-pro-high", []byte(inputJSON), false)
|
||||
mode := gjson.GetBytes(result, "toolConfig.functionCallingConfig.mode").String()
|
||||
if mode != "VALIDATED" {
|
||||
t.Fatalf("expected mode = 'VALIDATED' for mixed tools, got %q", mode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-strict tools omit toolConfig mode", func(t *testing.T) {
|
||||
inputJSON := `{
|
||||
"model": "gemini-3.1-pro-high",
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "tool_a",
|
||||
"description": "Loose tool.",
|
||||
"strict": false,
|
||||
"parameters": {"type": "object", "properties": {}}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "tool_b",
|
||||
"description": "Unspecified tool.",
|
||||
"parameters": {"type": "object", "properties": {}}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
result := ConvertOpenAIRequestToGemini("gemini-3.1-pro-high", []byte(inputJSON), false)
|
||||
if gjson.GetBytes(result, "toolConfig").Exists() {
|
||||
t.Fatalf("expected toolConfig not to be set when no strict tools and no tool_choice, got: %s", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user