Conversation
- 实现了 Redis 缓存用于存储和检索 Vertex AI 的 thought_signature - 添加了 Redis 配置选项和客户端初始化逻辑 - 实现了函数参数清理功能以移除不支持的 JSON Schema 字段 - 添加了测试用例验证函数参数清理功能 - 实现了多轮 function calling 场景下的 thought_signature 自动关联 - 添加了 Vertex AI OpenAI 兼容模式的增强支持 - 优化了请求体转换和 Redis 异步处理流程 Change-Id: Iaa828b5ac0825df66555eace68baa69d516be801
|
@MonkeyCode-AI review 一下 @MonkeyCode-AI review |
|
MonkeyCode-AI 正在分析任务... MonkeyCode-AI is analyzing the task... |
MonkeyCode-AI
left a comment
There was a problem hiding this comment.
我是 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 | 💡 Suggestion | |
|---|---|---|
| 2 | 2 | 0 |
| @@ -594,22 +844,48 @@ func (v *vertexProvider) buildChatCompletionResponse(ctx wrapper.HttpContext, re | |||
| FinishReason: util.Ptr(candidate.FinishReason), | |||
There was a problem hiding this comment.
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 { | |||
There was a problem hiding this comment.
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)。
| 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) | |||
| } | |||
There was a problem hiding this comment.
Warning
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 |
There was a problem hiding this comment.
Warning
toolCallIdToFunctionName 在遍历 messages 时遇到 assistant 的 ToolCalls 才填充;遇到 tool 角色消息时直接 lookup。若 tool 消息出现在对应 assistant/tool_calls 消息之前(例如上下文裁剪、重排、或客户端构造异常),functionName 会为空并仍发送 functionResponse.name="",这在 Vertex/Gemini 的 function calling 语义下可能导致请求失败或不可预期行为。
建议: 在处理 messages 前先扫描构建完整映射;并在 functionName 为空时直接返回错误或跳过该 tool message 转换(避免发送空 name)。
Ⅰ. Describe what this PR did
本 PR 修复了 ai-proxy 插件在将 OpenAI 格式的 function calling 请求转换为 Vertex AI / Gemini 格式时的多个问题:
id字段导致客户端解析失败的问题thought_signature导致请求失败的问题问题一:JSON Schema 元数据字段
当客户端使用 OpenAI 格式发送包含
tools的请求时,function 的parameters字段可能包含标准 JSON Schema 元数据字段(如$schema、$ref、ref等)。这些字段在 OpenAI API 中是允许的,但 Vertex AI / Gemini API 使用的是 OpenAPI 3.0 Schema 规范的子集,不支持这些字段,导致请求失败并返回错误:或
问题二:Tool Call ID 缺失
客户端收到响应后报错:
原因是 Vertex AI 响应中的
functionCall不包含id字段,但 OpenAI 格式要求每个tool_call必须有唯一的id。问题三:Thought Signature 缺失(Gemini 3 模型)
使用 Gemini 3 模型进行多轮工具调用时,出现错误:
背景说明:
根据 Google 官方文档,
thought_signature是模型内部推理过程的加密表示。Gemini 3 模型对thought_signature有严格的验证要求:关键规则:
functionCall时,响应中会包含thoughtSignature字段thoughtSignature附加到对应的functionCallpart 上(不是functionResponse)functionCall需要thoughtSignature问题原因:
thought_signature)thought_signature,WASM 插件的不同实例也无法共享内存状态解决方案:
使用 Redis 缓存
thought_signature,实现跨 WASM 实例的状态共享官方文档依据
1. FunctionDeclaration 文档
2. Schema 文档
3. Function Calling Reference 文档
type- 数据类型(STRING, INTEGER, BOOLEAN, NUMBER, ARRAY, OBJECT)description- 描述enum- 枚举值items- 数组元素 schemaproperties- 对象属性required- 必需属性nullable- 是否可为 null4. Thought Signatures 文档
5. Content API Reference
thoughtSignature字段: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):$schema、$id、$ref、ref、$defs、definitions、$comment等修改 Vertex Provider 和 Gemini Provider:
buildVertexChatRequest和buildGeminiChatRequest中调用cleanFunctionParameters清理参数2. Tool Call ID 生成
修改
buildChatCompletionResponse和buildChatCompletionStreamResponse(provider/vertex.go):uuid.New().String()为每个toolCall生成唯一的idcall_{uuid},如call_32553908-c148-4a0c-8c30-d7be45848e633. Thought Signature 缓存(Gemini 3 支持)
新增 Redis 缓存机制(
provider/vertex.go):storeThoughtSignature:将响应中的thoughtSignature存入 Redishigress-vertex-thought-sig:{tool_call_id}fetchThoughtSignaturesFromRedis:批量从 Redis 获取thoughtSignaturetransformRequestBodyAfterRedis:在 Redis 获取完成后执行请求体转换getThoughtSignatureFromContext:从请求上下文获取已缓存的thoughtSignature修改
buildVertexChatRequest:ToolCalls时,从 Redis 缓存获取thoughtSignaturethoughtSignature附加到functionCallpart 上(符合 Google 文档要求)修改
buildChatCompletionResponse和buildChatCompletionStreamResponse:FunctionCall和ThoughtSignaturethoughtSignature存入 Redis 以供后续请求使用新增
vertexPart结构体字段:ThoughtSignature stringjson:"thoughtSignature,omitempty"`` 字段新增配置选项(
provider/provider.go):vertexEnableThoughtSigCache:是否启用 thought_signature 缓存vertexThoughtSigCacheTTL:缓存 TTL(秒)redisConfig:Redis 连接配置4. 添加单元测试
provider/model_test.go:示例
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:higress-vertex-thought-sig:call_abc123<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 请求的functionCallpart 上:{ "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 场景下的以下问题:
id字段导致客户端解析失败thought_signature返回 400 错误Ⅲ. Why don't you add test cases (unit test/integration test)?
已添加
cleanFunctionParameters的单元测试,位于provider/model_test.go,覆盖以下场景:$schema清理$schema、$id、$comment、definitions)$schema清理type、description、properties、required、enum等)$defs字段清理$前缀的ref字段清理Thought Signature 缓存功能需要 Redis 环境,建议通过集成测试验证。
Ⅳ. Describe how to verify it
方式一:运行单元测试
方式二:验证 JSON Schema 清理
$schema的请求:Unknown name "$schema"错误方式三:验证 Thought Signature 缓存(Gemini 3 模型)
使用 Gemini 3 模型进行多轮工具调用:
验证:
missing a thought_signature错误Ⅴ. Special notes for reviews
向后兼容:此修改不影响现有功能
递归处理:
cleanFunctionParameters函数递归处理嵌套结构,确保所有层级的不支持字段都被清理同时修复 Gemini Provider:JSON Schema 清理逻辑同时应用于 Gemini Provider
Redis 依赖:Thought Signature 缓存功能需要 Redis 服务
异步处理流程:Thought Signature 缓存涉及复杂的异步处理
Gemini 3 兼容性:此功能专为 Gemini 3 模型设计,解决其严格的 thought_signature 验证要求
Ⅵ. AI Coding Tool Usage Checklist (if applicable)
Please check all applicable items:
AI Coding Summary
问题分析:
JSON Schema 清理:OpenAI API 对 function parameters 中的 JSON Schema 支持比较宽松,允许
$schema、$ref等元数据字段;Vertex AI / Gemini API 基于 OpenAPI 3.0 规范,只支持简化的 JSON Schema 字段Tool Call ID 缺失:Vertex AI 响应中的
functionCall不包含id字段,但 OpenAI 格式要求每个tool_call必须有唯一的idThought Signature 缺失:Gemini 3 模型对
thought_signature有严格的验证要求,必须在后续请求中携带之前响应中的签名;标准 OpenAI SDK 不会传递自定义字段,且 WASM 实例间无法共享内存状态解决方案:
JSON Schema 清理:创建
cleanFunctionParameters递归清理函数,在协议转换时过滤不支持的字段Tool Call ID 生成:使用
uuid.New().String()为每个toolCall生成唯一的idThought Signature 缓存:使用 Redis 缓存
thought_signature,实现跨 WASM 实例的状态共享thoughtSignature并存入 RedisthoughtSignature并附加到functionCallpart主要变更:
provider/model.go- 新增cleanFunctionParameters函数provider/vertex.go- 主要修改:cleanFunctionParameters清理 JSON Schematool_call_idprovider/gemini.go- 调用cleanFunctionParameters清理 JSON Schemaprovider/provider.go- 新增 Redis 配置和 thought_signature 缓存配置provider/model_test.go- 新增cleanFunctionParameters单元测试