Skip to content

VertexAI转换为openai协议支持FunctionCall#3456

Open
wydream wants to merge 2 commits intoalibaba:mainfrom
wydream:feat/vertex_tool_call
Open

VertexAI转换为openai协议支持FunctionCall#3456
wydream wants to merge 2 commits intoalibaba:mainfrom
wydream:feat/vertex_tool_call

Conversation

@wydream
Copy link
Collaborator

@wydream wydream commented Feb 5, 2026

Ⅰ. Describe what this PR did

本 PR 修复了 ai-proxy 插件在将 OpenAI 格式的 function calling 请求转换为 Vertex AI / Gemini 格式时的多个问题:

  1. JSON Schema 元数据字段清理 - 解决不支持的 JSON Schema 字段导致请求失败的问题
  2. Tool Call ID 生成 - 解决响应中缺少 id 字段导致客户端解析失败的问题
  3. Thought Signature 缓存 - 解决 Gemini 3 模型在多轮工具调用时因缺少 thought_signature 导致请求失败的问题

问题一:JSON Schema 元数据字段

当客户端使用 OpenAI 格式发送包含 tools 的请求时,function 的 parameters 字段可能包含标准 JSON Schema 元数据字段(如 $schema$refref 等)。这些字段在 OpenAI API 中是允许的,但 Vertex AI / Gemini API 使用的是 OpenAPI 3.0 Schema 规范的子集,不支持这些字段,导致请求失败并返回错误:

Invalid JSON payload received. Unknown name "$schema" at 'tools[0].function_declarations[0].parameters': Cannot find field.

Schema.ref 'QuestionOption' was set alongside unsupported fields.

问题二:Tool Call ID 缺失

客户端收到响应后报错:

Error: AI_InvalidResponseDataError: Expected 'id' to be a string.

原因是 Vertex AI 响应中的 functionCall 不包含 id 字段,但 OpenAI 格式要求每个 tool_call 必须有唯一的 id


问题三:Thought Signature 缺失(Gemini 3 模型)

使用 Gemini 3 模型进行多轮工具调用时,出现错误:

Error: Unable to submit request because function call `grep` in the 4. content block is missing a `thought_signature`.

背景说明

根据 Google 官方文档thought_signature 是模型内部推理过程的加密表示。Gemini 3 模型对 thought_signature 有严格的验证要求:

Gemini 3 models enforce stricter validation on thought signatures than previous Gemini versions because they improve model performance for function calling. To ensure the model maintains full context across multiple turns of a conversation, you must return the thought signatures from previous responses in your subsequent requests.

关键规则:

  • 当模型返回 functionCall 时,响应中会包含 thoughtSignature 字段
  • 在后续请求中,必须将 thoughtSignature 附加到对应的 functionCall part 上(不是 functionResponse
  • 对于并行函数调用,只有第一个 functionCall 需要 thoughtSignature

问题原因

  1. 标准 OpenAI SDK 不会在请求中携带自定义字段(如 thought_signature
  2. 即使客户端保存了 thought_signature,WASM 插件的不同实例也无法共享内存状态

解决方案

使用 Redis 缓存 thought_signature,实现跨 WASM 实例的状态共享

官方文档依据

1. FunctionDeclaration 文档

2. Schema 文档

3. Function Calling Reference 文档

4. Thought Signatures 文档

5. Content API Reference

6. 不支持的 JSON Schema 字段

标准 JSON Schema 的以下元数据字段不在 Vertex AI Schema 的支持列表中:

  • $schema - JSON Schema 版本声明
  • $id - Schema 标识符
  • $ref / ref - Schema 引用(注:Vertex AI 有自己的 ref 字段格式,但与标准 JSON Schema 的 $ref 不兼容)
  • $defs / definitions - Schema 定义块
  • $comment - 注释
  • $vocabulary, $anchor, $dynamicRef, $dynamicAnchor - 其他 JSON Schema 元数据

主要变更

1. JSON Schema 清理

新增 cleanFunctionParameters 函数provider/model.go):

  • 递归清理 function parameters 中不被 Vertex AI / Gemini 支持的 JSON Schema 字段
  • 支持清理嵌套的 map 和数组结构
  • 清理的字段包括:$schema$id$refref$defsdefinitions$comment

修改 Vertex Provider 和 Gemini Provider

  • buildVertexChatRequestbuildGeminiChatRequest 中调用 cleanFunctionParameters 清理参数

2. Tool Call ID 生成

修改 buildChatCompletionResponsebuildChatCompletionStreamResponseprovider/vertex.go):

  • 使用 uuid.New().String() 为每个 toolCall 生成唯一的 id
  • 格式:call_{uuid},如 call_32553908-c148-4a0c-8c30-d7be45848e63

3. Thought Signature 缓存(Gemini 3 支持)

新增 Redis 缓存机制provider/vertex.go):

  • storeThoughtSignature:将响应中的 thoughtSignature 存入 Redis

    • Key 格式:higress-vertex-thought-sig:{tool_call_id}
    • 支持配置 TTL(默认 3600 秒)
  • fetchThoughtSignaturesFromRedis:批量从 Redis 获取 thoughtSignature

    • 支持异步回调模式
    • 自动暂停请求等待 Redis 响应
  • transformRequestBodyAfterRedis:在 Redis 获取完成后执行请求体转换

    • 正确处理 Express Mode 和标准模式的认证流程
  • getThoughtSignatureFromContext:从请求上下文获取已缓存的 thoughtSignature

修改 buildVertexChatRequest

  • 在处理 ToolCalls 时,从 Redis 缓存获取 thoughtSignature
  • thoughtSignature 附加到 functionCall part 上(符合 Google 文档要求)

修改 buildChatCompletionResponsebuildChatCompletionStreamResponse

  • 遍历所有 parts 查找 FunctionCallThoughtSignature
  • thoughtSignature 存入 Redis 以供后续请求使用

新增 vertexPart 结构体字段

  • 添加 ThoughtSignature string json:"thoughtSignature,omitempty"`` 字段

新增配置选项provider/provider.go):

  • vertexEnableThoughtSigCache:是否启用 thought_signature 缓存
  • vertexThoughtSigCacheTTL:缓存 TTL(秒)
  • redisConfig:Redis 连接配置

4. 添加单元测试

provider/model_test.go

  • 覆盖 JSON Schema 清理的各种场景

示例

JSON Schema 清理示例

转换前(OpenAI 格式):

{
  "parameters": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
      "options": {
        "type": "array",
        "items": {
          "ref": "QuestionOption",
          "type": "object",
          "properties": {
            "label": { "type": "string" }
          }
        }
      }
    }
  }
}

转换后(Vertex AI 格式):

{
  "parameters": {
    "type": "object",
    "properties": {
      "options": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "label": { "type": "string" }
          }
        }
      }
    }
  }
}

Thought Signature 处理流程

步骤 1:模型返回 functionCall 时

{
  "candidates": [{
    "content": {
      "role": "model",
      "parts": [{
        "functionCall": {
          "name": "grep",
          "args": {"pattern": "foo"}
        },
        "thoughtSignature": "<BASE64_SIGNATURE>"
      }]
    }
  }]
}

插件生成 tool_call_id(如 call_abc123),并将 thoughtSignature 存入 Redis:

  • Key: higress-vertex-thought-sig:call_abc123
  • Value: <BASE64_SIGNATURE>

步骤 2:客户端发送工具响应时

{
  "messages": [
    {"role": "assistant", "tool_calls": [{"id": "call_abc123", "function": {"name": "grep"}}]},
    {"role": "tool", "tool_call_id": "call_abc123", "content": "result..."}
  ]
}

插件从 Redis 获取 thoughtSignature,并附加到 Vertex AI 请求的 functionCall part 上:

{
  "contents": [
    {
      "role": "model",
      "parts": [{
        "functionCall": {"name": "grep", "args": {"pattern": "foo"}},
        "thoughtSignature": "<BASE64_SIGNATURE>"
      }]
    },
    {
      "role": "user",
      "parts": [{
        "functionResponse": {"name": "grep", "response": {"output": "result..."}}
      }]
    }
  ]
}

Ⅱ. Does this pull request fix one issue?

修复了 Vertex AI / Gemini Provider 在 function calling 场景下的以下问题:

  1. 包含 JSON Schema 元数据字段的请求返回错误
  2. 响应中缺少 id 字段导致客户端解析失败
  3. Gemini 3 模型在多轮工具调用时因缺少 thought_signature 返回 400 错误

Ⅲ. Why don't you add test cases (unit test/integration test)?

已添加 cleanFunctionParameters 的单元测试,位于 provider/model_test.go,覆盖以下场景:

  • ✅ nil 输入处理
  • ✅ 空 map 处理
  • ✅ 根级别 $schema 清理
  • ✅ 多个不支持字段的清理($schema$id$commentdefinitions
  • ✅ 嵌套属性中的 $schema 清理
  • ✅ 数组中 map 元素的清理
  • ✅ 保留有效字段(typedescriptionpropertiesrequiredenum 等)
  • $defs 字段清理
  • ✅ 不带 $ 前缀的 ref 字段清理
  • ✅ 真实 tool schema 的完整清理测试

Thought Signature 缓存功能需要 Redis 环境,建议通过集成测试验证。

Ⅳ. Describe how to verify it

方式一:运行单元测试

cd plugins/wasm-go/extensions/ai-proxy
go test -gcflags="all=-N -l" -v -run TestCleanFunctionParameters ./provider

方式二:验证 JSON Schema 清理

  1. 配置 Vertex AI Provider
provider:
  type: vertex
  apiTokens:
    - "YOUR_API_KEY"
  1. 发送包含 $schema 的请求
curl -X POST http://your-gateway/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gemini-2.5-flash",
    "messages": [{"role": "user", "content": "Hello"}],
    "tools": [{
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "Get weather info",
        "parameters": {
          "$schema": "http://json-schema.org/draft-07/schema#",
          "type": "object",
          "properties": {
            "location": {"type": "string"}
          }
        }
      }
    }]
  }'
  1. 验证
    • 请求不再返回 Unknown name "$schema" 错误
    • Function calling 正常工作

方式三:验证 Thought Signature 缓存(Gemini 3 模型)

  1. 配置启用 Thought Signature 缓存
provider:
  type: vertex
  apiTokens:
    - "YOUR_API_KEY"
  vertexEnableThoughtSigCache: true
  vertexThoughtSigCacheTTL: 3600
  redis:
    serviceName: "redis.static"
    servicePort: 6379
  1. 使用 Gemini 3 模型进行多轮工具调用

    • 第一次请求触发工具调用
    • 发送工具响应
    • 继续对话,触发更多工具调用
  2. 验证

    • 不再返回 missing a thought_signature 错误
    • 多轮工具调用正常工作

Ⅴ. Special notes for reviews

  1. 向后兼容:此修改不影响现有功能

    • JSON Schema 清理只是在协议转换时过滤掉不支持的字段
    • Tool Call ID 生成对客户端透明
    • Thought Signature 缓存默认禁用,需要显式配置启用
  2. 递归处理cleanFunctionParameters 函数递归处理嵌套结构,确保所有层级的不支持字段都被清理

  3. 同时修复 Gemini Provider:JSON Schema 清理逻辑同时应用于 Gemini Provider

  4. Redis 依赖:Thought Signature 缓存功能需要 Redis 服务

    • 如果未配置 Redis 或 Redis 不可用,功能自动降级(跳过缓存)
    • 使用异步回调模式,不阻塞请求处理
  5. 异步处理流程:Thought Signature 缓存涉及复杂的异步处理

    • 请求阶段:暂停请求 → 从 Redis 获取签名 → 转换请求体 → 获取 OAuth token → 恢复请求
    • 响应阶段:解析响应 → 异步存储签名到 Redis(不阻塞响应)
  6. Gemini 3 兼容性:此功能专为 Gemini 3 模型设计,解决其严格的 thought_signature 验证要求

Ⅵ. AI Coding Tool Usage Checklist (if applicable)

Please check all applicable items:

  • For regular updates/changes (not new plugins):
    • I have provided the prompts/instructions I gave to the AI Coding tool below
    • I have included the AI Coding summary below

AI Coding Summary

问题分析:

  1. JSON Schema 清理:OpenAI API 对 function parameters 中的 JSON Schema 支持比较宽松,允许 $schema$ref 等元数据字段;Vertex AI / Gemini API 基于 OpenAPI 3.0 规范,只支持简化的 JSON Schema 字段

  2. Tool Call ID 缺失:Vertex AI 响应中的 functionCall 不包含 id 字段,但 OpenAI 格式要求每个 tool_call 必须有唯一的 id

  3. Thought Signature 缺失:Gemini 3 模型对 thought_signature 有严格的验证要求,必须在后续请求中携带之前响应中的签名;标准 OpenAI SDK 不会传递自定义字段,且 WASM 实例间无法共享内存状态

解决方案:

  1. JSON Schema 清理:创建 cleanFunctionParameters 递归清理函数,在协议转换时过滤不支持的字段

  2. Tool Call ID 生成:使用 uuid.New().String() 为每个 toolCall 生成唯一的 id

  3. Thought Signature 缓存:使用 Redis 缓存 thought_signature,实现跨 WASM 实例的状态共享

    • 响应阶段:提取 thoughtSignature 并存入 Redis
    • 请求阶段:从 Redis 获取 thoughtSignature 并附加到 functionCall part
    • 正确处理异步 Redis 操作和 OAuth token 获取的流程

主要变更:

  1. provider/model.go - 新增 cleanFunctionParameters 函数
  2. provider/vertex.go - 主要修改:
    • 调用 cleanFunctionParameters 清理 JSON Schema
    • 生成 tool_call_id
    • 新增 Redis 缓存相关函数
    • 修改请求/响应处理逻辑以支持 thought_signature
  3. provider/gemini.go - 调用 cleanFunctionParameters 清理 JSON Schema
  4. provider/provider.go - 新增 Redis 配置和 thought_signature 缓存配置
  5. provider/model_test.go - 新增 cleanFunctionParameters 单元测试

- 实现了 Redis 缓存用于存储和检索 Vertex AI 的 thought_signature
- 添加了 Redis 配置选项和客户端初始化逻辑
- 实现了函数参数清理功能以移除不支持的 JSON Schema 字段
- 添加了测试用例验证函数参数清理功能
- 实现了多轮 function calling 场景下的 thought_signature 自动关联
- 添加了 Vertex AI OpenAI 兼容模式的增强支持
- 优化了请求体转换和 Redis 异步处理流程

Change-Id: Iaa828b5ac0825df66555eace68baa69d516be801
@safe1ine
Copy link

safe1ine commented Feb 6, 2026

@MonkeyCode-AI review 一下


@MonkeyCode-AI review

@MonkeyCode-AI
Copy link

MonkeyCode-AI commented Feb 6, 2026

MonkeyCode-AI 正在分析任务...


MonkeyCode-AI is analyzing the task...

Copy link

@MonkeyCode-AI MonkeyCode-AI left a comment

Choose a reason for hiding this comment

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

我是 MonkeyCode AI 编程助手,你可以在 GitHub 仓库的 PR 中 at @MonkeyCode-AI 来呼唤我。

任务执行细节请参考: https://monkeycode-ai.com/tasks/public?id=abf5537d-e7dc-4310-b11c-484711d61254

代码审查结果

整体实现覆盖了 JSON Schema 清理、tool_call id 生成与 Vertex(Gemini3) thoughtSignature 缓存链路,但当前 Vertex 的 thoughtSignature 关联与 tool_call_id->function_name 映射存在明显正确性风险,且引入了生产环境不应保留的 DEBUG 日志与一处 Gemini 侧工具调用构造逻辑错误。

✨ 代码亮点

  • cleanFunctionParameters 递归清理嵌套 map/array,并配套了单元测试覆盖多种真实场景
  • Vertex provider 支持并行 tool_calls(处理 message.ToolCalls 的全部元素),比原先只处理第一个更符合 OpenAI 协议
🚨 Critical ⚠️ Warning 💡 Suggestion
2 2 0

@@ -594,22 +844,48 @@ func (v *vertexProvider) buildChatCompletionResponse(ctx wrapper.HttpContext, re
FinishReason: util.Ptr(candidate.FinishReason),

Choose a reason for hiding this comment

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

Caution

🚨 thoughtSignature 缓存使用了新生成的 toolCallId,导致后续请求无法按客户端传回的 tool_call_id 命中缓存

buildChatCompletionResponse/buildChatCompletionStreamResponse 中为 Vertex 返回的 functionCall 生成了新的 OpenAI tool_call_id(call_uuid),并以该 id 作为 Redis key 存储 thoughtSignature。但后续请求从 OpenAI 消息中提取的是客户端回传的 tool_call_id(来自先前响应)。若代理在响应里生成的 tool_call_id 与后续请求中携带的一致才能命中缓存;当前虽然响应里确实返回了生成的 id,但你在构造下一次 Vertex 请求时,把 thoughtSignature 绑定到 message.ToolCalls(assistant 消息)里的 tc.Id。问题在于:该 tc.Id 是 OpenAI SDK 会回传的 id 没错,但你在 Vertex request 中对 tool 角色 message(tool response)需要根据 tool_call_id 找函数名时,用的是本次 buildVertexChatRequest 内新建的 toolCallIdToFunctionName map,只在遍历到 assistant 的 tool_calls 时填充;如果输入消息顺序为 [tool message] 在 [assistant tool_calls message] 之前(或上下文裁剪/重排),会导致 tool response 找不到函数名。同时,更关键的是:thoughtSignature 实际需要与“Vertex 返回的 functionCall part”对应;当前把 thoughtSignature 存到 Redis 的 key 选择为 OpenAI tool_call_id 是可行方案,但必须保证后续请求里的 tc.Id 与最初生成并返回给客户端的 id 一致、且 tool response 的函数名能稳定解析。当前实现对消息顺序有隐含假设,易导致 thoughtSignature 获取不到或 functionResponse.name 为空,从而触发 Gemini3 校验失败。

建议: 1) 建立 tool_call_id->function_name 映射时不要依赖单次遍历顺序:应在遍历 messages 前,先扫描所有 assistant/tool_calls 消息构建全量映射,再处理 tool 消息;或在遇到 tool 消息时回扫历史 messages 查找匹配的 tool_call_id。2) thoughtSignature 的存取 key 建议同时带上会话/请求维度信息(如 responseId 或自定义 conversation id)以避免 id 碰撞或跨会话串扰。3) 若 functionName 解析不到,应明确降级策略:要么直接不发送 functionResponse(返回错误给客户端),要么回填为 message.ToolCallId 对应的 name(通过回扫/缓存保证可得),避免发送 name="" 给 Vertex。

@@ -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
}

@@ -855,8 +1159,28 @@ func (v *vertexProvider) buildVertexChatRequest(request *chatCompletionRequest)
}

Choose a reason for hiding this comment

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

Warning

⚠️ 生产代码残留 DEBUG 打印完整 tool parameters,可能泄露敏感信息且增加日志噪音

buildVertexChatRequest 中对每个 tool 的 parameters 在清理前后进行 json.Marshal 并 log.Debugf 输出。parameters 可能包含敏感结构信息或非常大,且在高并发下会显著增加 CPU/内存与日志量。

建议: 删除该 DEBUG 日志;如确有排障需求,建议受配置开关控制并做长度截断/脱敏(例如最多打印前 1KB)。

}
shouldAddDummyModelMessage := false
var lastFunctionName string
// Map to track tool_call_id -> function_name for tool response messages

Choose a reason for hiding this comment

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

Warning

⚠️ tool_call_id -> function_name 映射仅在单次 buildVertexChatRequest 内临时构建,且依赖消息顺序

toolCallIdToFunctionName 在遍历 messages 时遇到 assistant 的 ToolCalls 才填充;遇到 tool 角色消息时直接 lookup。若 tool 消息出现在对应 assistant/tool_calls 消息之前(例如上下文裁剪、重排、或客户端构造异常),functionName 会为空并仍发送 functionResponse.name="",这在 Vertex/Gemini 的 function calling 语义下可能导致请求失败或不可预期行为。

建议: 在处理 messages 前先扫描构建完整映射;并在 functionName 为空时直接返回错误或跳过该 tool message 转换(避免发送空 name)。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants