fix(openai): 增强 Codex 工具过滤和参数标准化
- codex_transform: 过滤无效工具,支持 Responses-style 和 ChatCompletions-style 格式 - tool_corrector: 添加 fetch 工具映射,修正 bash/edit 参数命名规范
This commit is contained in:
@@ -394,19 +394,35 @@ func normalizeCodexTools(reqBody map[string]any) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
modified := false
|
modified := false
|
||||||
for idx, tool := range tools {
|
validTools := make([]any, 0, len(tools))
|
||||||
|
|
||||||
|
for _, tool := range tools {
|
||||||
toolMap, ok := tool.(map[string]any)
|
toolMap, ok := tool.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
// Keep unknown structure as-is to avoid breaking upstream behavior.
|
||||||
|
validTools = append(validTools, tool)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
toolType, _ := toolMap["type"].(string)
|
toolType, _ := toolMap["type"].(string)
|
||||||
if strings.TrimSpace(toolType) != "function" {
|
toolType = strings.TrimSpace(toolType)
|
||||||
|
if toolType != "function" {
|
||||||
|
validTools = append(validTools, toolMap)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
function, ok := toolMap["function"].(map[string]any)
|
// OpenAI Responses-style tools use top-level name/parameters.
|
||||||
if !ok {
|
if name, ok := toolMap["name"].(string); ok && strings.TrimSpace(name) != "" {
|
||||||
|
validTools = append(validTools, toolMap)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChatCompletions-style tools use {type:"function", function:{...}}.
|
||||||
|
functionValue, hasFunction := toolMap["function"]
|
||||||
|
function, ok := functionValue.(map[string]any)
|
||||||
|
if !hasFunction || functionValue == nil || !ok || function == nil {
|
||||||
|
// Drop invalid function tools.
|
||||||
|
modified = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,11 +451,11 @@ func normalizeCodexTools(reqBody map[string]any) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tools[idx] = toolMap
|
validTools = append(validTools, toolMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
if modified {
|
if modified {
|
||||||
reqBody["tools"] = tools
|
reqBody["tools"] = validTools
|
||||||
}
|
}
|
||||||
|
|
||||||
return modified
|
return modified
|
||||||
|
|||||||
@@ -129,6 +129,37 @@ func TestFilterCodexInput_RemovesItemReferenceWhenNotPreserved(t *testing.T) {
|
|||||||
require.False(t, hasID)
|
require.False(t, hasID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApplyCodexOAuthTransform_NormalizeCodexTools_PreservesResponsesFunctionTools(t *testing.T) {
|
||||||
|
setupCodexCache(t)
|
||||||
|
|
||||||
|
reqBody := map[string]any{
|
||||||
|
"model": "gpt-5.1",
|
||||||
|
"tools": []any{
|
||||||
|
map[string]any{
|
||||||
|
"type": "function",
|
||||||
|
"name": "bash",
|
||||||
|
"description": "desc",
|
||||||
|
"parameters": map[string]any{"type": "object"},
|
||||||
|
},
|
||||||
|
map[string]any{
|
||||||
|
"type": "function",
|
||||||
|
"function": nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
applyCodexOAuthTransform(reqBody)
|
||||||
|
|
||||||
|
tools, ok := reqBody["tools"].([]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Len(t, tools, 1)
|
||||||
|
|
||||||
|
first, ok := tools[0].(map[string]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, "function", first["type"])
|
||||||
|
require.Equal(t, "bash", first["name"])
|
||||||
|
}
|
||||||
|
|
||||||
func TestApplyCodexOAuthTransform_EmptyInput(t *testing.T) {
|
func TestApplyCodexOAuthTransform_EmptyInput(t *testing.T) {
|
||||||
// 空 input 应保持为空且不触发异常。
|
// 空 input 应保持为空且不触发异常。
|
||||||
setupCodexCache(t)
|
setupCodexCache(t)
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ var codexToolNameMapping = map[string]string{
|
|||||||
"executeBash": "bash",
|
"executeBash": "bash",
|
||||||
"exec_bash": "bash",
|
"exec_bash": "bash",
|
||||||
"execBash": "bash",
|
"execBash": "bash",
|
||||||
|
|
||||||
|
// Some clients output generic fetch names.
|
||||||
|
"fetch": "webfetch",
|
||||||
|
"web_fetch": "webfetch",
|
||||||
|
"webFetch": "webfetch",
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToolCorrectionStats 记录工具修正的统计信息(导出用于 JSON 序列化)
|
// ToolCorrectionStats 记录工具修正的统计信息(导出用于 JSON 序列化)
|
||||||
@@ -208,27 +213,67 @@ func (c *CodexToolCorrector) correctToolParameters(toolName string, functionCall
|
|||||||
// 根据工具名称应用特定的参数修正规则
|
// 根据工具名称应用特定的参数修正规则
|
||||||
switch toolName {
|
switch toolName {
|
||||||
case "bash":
|
case "bash":
|
||||||
// 移除 workdir 参数(OpenCode 不支持)
|
// OpenCode bash 支持 workdir;有些来源会输出 work_dir。
|
||||||
if _, exists := argsMap["workdir"]; exists {
|
if _, hasWorkdir := argsMap["workdir"]; !hasWorkdir {
|
||||||
delete(argsMap, "workdir")
|
if workDir, exists := argsMap["work_dir"]; exists {
|
||||||
corrected = true
|
argsMap["workdir"] = workDir
|
||||||
log.Printf("[CodexToolCorrector] Removed 'workdir' parameter from bash tool")
|
delete(argsMap, "work_dir")
|
||||||
}
|
corrected = true
|
||||||
if _, exists := argsMap["work_dir"]; exists {
|
log.Printf("[CodexToolCorrector] Renamed 'work_dir' to 'workdir' in bash tool")
|
||||||
delete(argsMap, "work_dir")
|
}
|
||||||
corrected = true
|
} else {
|
||||||
log.Printf("[CodexToolCorrector] Removed 'work_dir' parameter from bash tool")
|
if _, exists := argsMap["work_dir"]; exists {
|
||||||
|
delete(argsMap, "work_dir")
|
||||||
|
corrected = true
|
||||||
|
log.Printf("[CodexToolCorrector] Removed duplicate 'work_dir' parameter from bash tool")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case "edit":
|
case "edit":
|
||||||
// OpenCode edit 使用 old_string/new_string,Codex 可能使用其他名称
|
// OpenCode edit 参数为 filePath/oldString/newString(camelCase)。
|
||||||
// 这里可以添加参数名称的映射逻辑
|
if _, exists := argsMap["filePath"]; !exists {
|
||||||
if _, exists := argsMap["file_path"]; !exists {
|
if filePath, exists := argsMap["file_path"]; exists {
|
||||||
if path, exists := argsMap["path"]; exists {
|
argsMap["filePath"] = filePath
|
||||||
argsMap["file_path"] = path
|
delete(argsMap, "file_path")
|
||||||
|
corrected = true
|
||||||
|
log.Printf("[CodexToolCorrector] Renamed 'file_path' to 'filePath' in edit tool")
|
||||||
|
} else if filePath, exists := argsMap["path"]; exists {
|
||||||
|
argsMap["filePath"] = filePath
|
||||||
delete(argsMap, "path")
|
delete(argsMap, "path")
|
||||||
corrected = true
|
corrected = true
|
||||||
log.Printf("[CodexToolCorrector] Renamed 'path' to 'file_path' in edit tool")
|
log.Printf("[CodexToolCorrector] Renamed 'path' to 'filePath' in edit tool")
|
||||||
|
} else if filePath, exists := argsMap["file"]; exists {
|
||||||
|
argsMap["filePath"] = filePath
|
||||||
|
delete(argsMap, "file")
|
||||||
|
corrected = true
|
||||||
|
log.Printf("[CodexToolCorrector] Renamed 'file' to 'filePath' in edit tool")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := argsMap["oldString"]; !exists {
|
||||||
|
if oldString, exists := argsMap["old_string"]; exists {
|
||||||
|
argsMap["oldString"] = oldString
|
||||||
|
delete(argsMap, "old_string")
|
||||||
|
corrected = true
|
||||||
|
log.Printf("[CodexToolCorrector] Renamed 'old_string' to 'oldString' in edit tool")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := argsMap["newString"]; !exists {
|
||||||
|
if newString, exists := argsMap["new_string"]; exists {
|
||||||
|
argsMap["newString"] = newString
|
||||||
|
delete(argsMap, "new_string")
|
||||||
|
corrected = true
|
||||||
|
log.Printf("[CodexToolCorrector] Renamed 'new_string' to 'newString' in edit tool")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := argsMap["replaceAll"]; !exists {
|
||||||
|
if replaceAll, exists := argsMap["replace_all"]; exists {
|
||||||
|
argsMap["replaceAll"] = replaceAll
|
||||||
|
delete(argsMap, "replace_all")
|
||||||
|
corrected = true
|
||||||
|
log.Printf("[CodexToolCorrector] Renamed 'replace_all' to 'replaceAll' in edit tool")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -416,22 +416,23 @@ func TestCorrectToolParameters(t *testing.T) {
|
|||||||
expected map[string]bool // key: 期待存在的参数, value: true表示应该存在
|
expected map[string]bool // key: 期待存在的参数, value: true表示应该存在
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "remove workdir from bash tool",
|
name: "rename work_dir to workdir in bash tool",
|
||||||
input: `{
|
input: `{
|
||||||
"tool_calls": [{
|
"tool_calls": [{
|
||||||
"function": {
|
"function": {
|
||||||
"name": "bash",
|
"name": "bash",
|
||||||
"arguments": "{\"command\":\"ls\",\"workdir\":\"/tmp\"}"
|
"arguments": "{\"command\":\"ls\",\"work_dir\":\"/tmp\"}"
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
}`,
|
}`,
|
||||||
expected: map[string]bool{
|
expected: map[string]bool{
|
||||||
"command": true,
|
"command": true,
|
||||||
"workdir": false,
|
"workdir": true,
|
||||||
|
"work_dir": false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "rename path to file_path in edit tool",
|
name: "rename snake_case edit params to camelCase",
|
||||||
input: `{
|
input: `{
|
||||||
"tool_calls": [{
|
"tool_calls": [{
|
||||||
"function": {
|
"function": {
|
||||||
@@ -441,10 +442,12 @@ func TestCorrectToolParameters(t *testing.T) {
|
|||||||
}]
|
}]
|
||||||
}`,
|
}`,
|
||||||
expected: map[string]bool{
|
expected: map[string]bool{
|
||||||
"file_path": true,
|
"filePath": true,
|
||||||
"path": false,
|
"path": false,
|
||||||
"old_string": true,
|
"oldString": true,
|
||||||
"new_string": true,
|
"old_string": false,
|
||||||
|
"newString": true,
|
||||||
|
"new_string": false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user