feat(openai): 添加Codex工具调用自动修正功能

实现了完整的Codex工具调用拦截和自动修正系统,解决OpenCode使用Codex模型时的工具调用兼容性问题。

**核心功能:**

1. **工具名称自动映射**
   - apply_patch/applyPatch → edit
   - update_plan/updatePlan → todowrite
   - read_plan/readPlan → todoread
   - search_files/searchFiles → grep
   - list_files/listFiles → glob
   - read_file/readFile → read
   - write_file/writeFile → write
   - execute_bash/executeBash/exec_bash/execBash → bash

2. **工具参数自动修正**
   - bash: 自动移除不支持的 workdir/work_dir 参数
   - edit: 自动将 path 参数重命名为 file_path
   - 支持 JSON 字符串和对象两种参数格式

3. **流式响应集成**
   - 在 SSE 数据流中实时修正工具调用
   - 支持多种 JSON 结构(tool_calls, function_call, delta, choices等)
   - 不影响响应性能和用户体验

4. **统计和监控**
   - 记录每次工具修正的详细信息
   - 提供修正统计数据查询
   - 便于问题排查和性能优化

**实现文件:**
- `openai_tool_corrector.go`: 工具修正核心逻辑(250行)
- `openai_tool_corrector_test.go`: 完整的单元测试(380+行)
- `openai_gateway_service.go`: 流式响应集成
- `openai_gateway_service_tool_correction_test.go`: 集成测试

**测试覆盖:**
- 工具名称映射测试(18个映射规则)
- 参数修正测试(bash workdir、edit path等)
- SSE数据修正测试(多种JSON结构)
- 统计功能测试
- 所有测试通过 

**解决的问题:**
修复了 OpenCode 使用 sub2api 中转 Codex 时,因工具名称和参数不兼容导致的工具调用失败问题。
Codex 模型有时会忽略指令文件中的工具映射说明,导致调用不存在的工具(如 apply_patch)。
现在通过流式响应拦截,自动将错误的工具调用修正为 OpenCode 兼容的格式。

**参考文档:**
- OpenCode 工具规范: https://opencode.ai/docs/
- Codex Bridge 指令: backend/internal/service/prompts/codex_opencode_bridge.txt
This commit is contained in:
IanShaw027
2026-01-15 23:52:50 +08:00
parent b36f3db9de
commit 539b41f421
4 changed files with 873 additions and 0 deletions

View File

@@ -94,6 +94,7 @@ type OpenAIGatewayService struct {
httpUpstream HTTPUpstream
deferredService *DeferredService
openAITokenProvider *OpenAITokenProvider
toolCorrector *CodexToolCorrector
}
// NewOpenAIGatewayService creates a new OpenAIGatewayService
@@ -128,6 +129,7 @@ func NewOpenAIGatewayService(
httpUpstream: httpUpstream,
deferredService: deferredService,
openAITokenProvider: openAITokenProvider,
toolCorrector: NewCodexToolCorrector(),
}
}
@@ -1106,6 +1108,11 @@ func (s *OpenAIGatewayService) handleStreamingResponse(ctx context.Context, resp
line = s.replaceModelInSSELine(line, mappedModel, originalModel)
}
// Correct Codex tool calls if needed (apply_patch -> edit, etc.)
if correctedData, corrected := s.toolCorrector.CorrectToolCallsInSSEData(data); corrected {
line = "data: " + correctedData
}
// Forward line
if _, err := fmt.Fprintf(w, "%s\n", line); err != nil {
sendErrorEvent("write_failed")
@@ -1193,6 +1200,20 @@ func (s *OpenAIGatewayService) replaceModelInSSELine(line, fromModel, toModel st
return line
}
// correctToolCallsInResponseBody 修正响应体中的工具调用
func (s *OpenAIGatewayService) correctToolCallsInResponseBody(body []byte) []byte {
if len(body) == 0 {
return body
}
bodyStr := string(body)
corrected, changed := s.toolCorrector.CorrectToolCallsInSSEData(bodyStr)
if changed {
return []byte(corrected)
}
return body
}
func (s *OpenAIGatewayService) parseSSEUsage(data string, usage *OpenAIUsage) {
// Parse response.completed event for usage (OpenAI Responses format)
var event struct {
@@ -1296,6 +1317,8 @@ func (s *OpenAIGatewayService) handleOAuthSSEToJSON(resp *http.Response, c *gin.
if originalModel != mappedModel {
body = s.replaceModelInResponseBody(body, mappedModel, originalModel)
}
// Correct tool calls in final response
body = s.correctToolCallsInResponseBody(body)
} else {
usage = s.parseSSEUsageFromBody(bodyText)
if originalModel != mappedModel {