Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion plugins/wasm-go/extensions/ai-proxy/provider/gemini.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,13 @@ func (g *geminiProvider) buildGeminiChatRequest(request *chatCompletionRequest)
if request.Tools != nil {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

🚨 Gemini buildToolCalls 逻辑反了:FunctionCall 存在时直接 return,导致永远不输出 tool_calls

buildToolCalls 中:

  • item := candidate.Content.Parts[0]
  • if item.FunctionCall != nil { return toolCalls }
    这会在 FunctionCall 存在时直接返回空数组;而后续又访问 item.FunctionCall.Arguments,逻辑自相矛盾。应当是 item.FunctionCall == nil 时返回空数组,否则构造 toolCall。

建议: 修正条件判断,并确保读取字段一致(FunctionName/Arguments)。

Suggested change
if request.Tools != nil {
func (g *geminiProvider) buildToolCalls(candidate *geminiChatCandidate) []toolCall {
var toolCalls []toolCall
if len(candidate.Content.Parts) == 0 {
return toolCalls
}
item := candidate.Content.Parts[0]
if item.FunctionCall == nil {
return toolCalls
}
argsBytes, err := json.Marshal(item.FunctionCall.Arguments)
if err != nil {
log.Errorf("get toolCalls from gemini response failed: " + err.Error())
return toolCalls
}
toolCalls = append(toolCalls, toolCall{
Id: fmt.Sprintf("call_%s", uuid.New().String()),
Type: "function",
Function: functionCall{
Arguments: string(argsBytes),
Name: item.FunctionCall.FunctionName,
},
})
return toolCalls
}

functions := make([]function, 0, len(request.Tools))
for _, tool := range request.Tools {
functions = append(functions, tool.Function)
// 清理 function parameters 中不支持的 JSON Schema 字段
cleanedFunc := function{
Name: tool.Function.Name,
Description: tool.Function.Description,
Parameters: cleanFunctionParameters(tool.Function.Parameters),
}
functions = append(functions, cleanedFunc)
}
geminiRequest.Tools = []geminiChatTools{
{
Expand Down
62 changes: 62 additions & 0 deletions plugins/wasm-go/extensions/ai-proxy/provider/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,68 @@ type function struct {
Parameters map[string]interface{} `json:"parameters,omitempty"`
}

// cleanFunctionParameters 清理 function parameters 中某些 AI 服务(如 Vertex AI、Gemini)不支持的 JSON Schema 字段
// 这些服务基于 OpenAPI 3.0 规范,不支持标准 JSON Schema 的元数据字段如 $schema, $id, $ref 等
// 注意:某些客户端可能使用不带 $ 前缀的变体(如 ref 代替 $ref),也需要一并清理
func cleanFunctionParameters(params map[string]interface{}) map[string]interface{} {
if params == nil {
return nil
}

// 需要移除的 JSON Schema 元数据字段
// 包括带 $ 前缀的标准字段和不带 $ 前缀的变体
unsupportedKeys := []string{
// 标准 JSON Schema 元数据字段
"$schema",
"$id",
"$ref",
"$defs",
"definitions",
"$comment",
"$vocabulary",
"$anchor",
"$dynamicRef",
"$dynamicAnchor",
// 不带 $ 前缀的变体(某些客户端可能使用)
"ref",
}

result := make(map[string]interface{})
for key, value := range params {
// 检查是否是不支持的字段
isUnsupported := false
for _, unsupportedKey := range unsupportedKeys {
if key == unsupportedKey {
isUnsupported = true
break
}
}
if isUnsupported {
continue
}

// 递归清理嵌套的 map
switch v := value.(type) {
case map[string]interface{}:
result[key] = cleanFunctionParameters(v)
case []interface{}:
// 处理数组中的 map 元素
cleanedArray := make([]interface{}, len(v))
for i, item := range v {
if itemMap, ok := item.(map[string]interface{}); ok {
cleanedArray[i] = cleanFunctionParameters(itemMap)
} else {
cleanedArray[i] = item
}
}
result[key] = cleanedArray
default:
result[key] = value
}
}
return result
}

type toolChoice struct {
Type string `json:"type"`
Function function `json:"function"`
Expand Down
265 changes: 265 additions & 0 deletions plugins/wasm-go/extensions/ai-proxy/provider/model_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
package provider

import (
"reflect"
"testing"
)

func TestCleanFunctionParameters(t *testing.T) {
tests := []struct {
name string
input map[string]interface{}
expected map[string]interface{}
}{
{
name: "nil input",
input: nil,
expected: nil,
},
{
name: "empty map",
input: map[string]interface{}{},
expected: map[string]interface{}{},
},
{
name: "remove $schema at root level",
input: map[string]interface{}{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": map[string]interface{}{
"location": map[string]interface{}{
"type": "string",
"description": "The city and state",
},
},
},
expected: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"location": map[string]interface{}{
"type": "string",
"description": "The city and state",
},
},
},
},
{
name: "remove multiple unsupported fields",
input: map[string]interface{}{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://example.com/schema",
"$comment": "This is a comment",
"definitions": map[string]interface{}{},
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{
"type": "string",
},
},
},
expected: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{
"type": "string",
},
},
},
},
{
name: "nested $schema in properties",
input: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"nested": map[string]interface{}{
"$schema": "should be removed",
"type": "object",
"properties": map[string]interface{}{
"field": map[string]interface{}{
"type": "string",
},
},
},
},
},
expected: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"nested": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"field": map[string]interface{}{
"type": "string",
},
},
},
},
},
},
{
name: "array with map elements",
input: map[string]interface{}{
"type": "array",
"items": []interface{}{
map[string]interface{}{
"$schema": "should be removed",
"type": "string",
},
map[string]interface{}{
"type": "number",
},
},
},
expected: map[string]interface{}{
"type": "array",
"items": []interface{}{
map[string]interface{}{
"type": "string",
},
map[string]interface{}{
"type": "number",
},
},
},
},
{
name: "preserve valid fields",
input: map[string]interface{}{
"type": "object",
"description": "A valid description",
"properties": map[string]interface{}{
"location": map[string]interface{}{
"type": "string",
"description": "The city",
"enum": []interface{}{"NYC", "LA", "SF"},
},
},
"required": []interface{}{"location"},
},
expected: map[string]interface{}{
"type": "object",
"description": "A valid description",
"properties": map[string]interface{}{
"location": map[string]interface{}{
"type": "string",
"description": "The city",
"enum": []interface{}{"NYC", "LA", "SF"},
},
},
"required": []interface{}{"location"},
},
},
{
name: "remove $defs field",
input: map[string]interface{}{
"$defs": map[string]interface{}{
"Address": map[string]interface{}{
"type": "object",
},
},
"type": "object",
},
expected: map[string]interface{}{
"type": "object",
},
},
{
name: "remove ref field without dollar sign",
input: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"options": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{
"ref": "QuestionOption",
"type": "object",
"properties": map[string]interface{}{
"label": map[string]interface{}{
"type": "string",
},
},
},
},
},
},
expected: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"options": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"label": map[string]interface{}{
"type": "string",
},
},
},
},
},
},
},
{
name: "real world question tool schema",
input: map[string]interface{}{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": map[string]interface{}{
"questions": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"options": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{
"ref": "QuestionOption",
"type": "object",
"properties": map[string]interface{}{
"label": map[string]interface{}{
"type": "string",
},
},
},
},
},
},
},
},
},
expected: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"questions": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"options": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"label": map[string]interface{}{
"type": "string",
},
},
},
},
},
},
},
},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := cleanFunctionParameters(tt.input)
if !reflect.DeepEqual(result, tt.expected) {
t.Errorf("cleanFunctionParameters() = %v, want %v", result, tt.expected)
}
})
}
}
Loading
Loading