Skip to content

Commit 3a4124f

Browse files
committed
fix: dedupe function_call_output/tool_search_output call_id in Codex executor input to prevent duplicate tool results with multi-key rotation
1 parent 5c9431a commit 3a4124f

1 file changed

Lines changed: 64 additions & 0 deletions

File tree

internal/runtime/executor/codex_executor.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,11 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
183183
body = normalizeCodexInstructions(body)
184184
body = ensureImageGenerationTool(body, baseModel, auth)
185185

186+
// 防御性去重:翻译链中可能因多 Key / 多层处理导致 input 数组里
187+
// 同一个 call_id 的 function_call_output 或 tool_search_output 被重复写入。
188+
// 在所有请求体变换完成后做一次最终去重,避免模型收到重复工具结果。
189+
body = dedupeToolOutputs(body)
190+
186191
url := strings.TrimSuffix(baseURL, "/") + "/responses"
187192
httpReq, err := e.cacheHelper(ctx, from, url, req, body)
188193
if err != nil {
@@ -426,6 +431,9 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
426431
body = normalizeCodexInstructions(body)
427432
body = ensureImageGenerationTool(body, baseModel, auth)
428433

434+
// 防御性去重:同 Execute 方法,防止 input 中工具输出重复。
435+
body = dedupeToolOutputs(body)
436+
429437
url := strings.TrimSuffix(baseURL, "/") + "/responses"
430438
httpReq, err := e.cacheHelper(ctx, from, url, req, body)
431439
if err != nil {
@@ -1040,3 +1048,59 @@ func codexConfigLookupAttrs(auth *cliproxyauth.Auth) (apiKey, baseURL string) {
10401048
}
10411049
return strings.TrimSpace(auth.Attributes["api_key"]), strings.TrimSpace(auth.Attributes["base_url"])
10421050
}
1051+
1052+
// dedupeToolOutputs 移除 input 数组中 call_id 重复的 function_call_output
1053+
// 和 tool_search_output 项(保留首次出现),防止上游翻译链或重试逻辑
1054+
// 导致模型收到重复工具结果。
1055+
func dedupeToolOutputs(body []byte) []byte {
1056+
inputItems := gjson.GetBytes(body, "input")
1057+
if !inputItems.IsArray() {
1058+
return body
1059+
}
1060+
1061+
arr := inputItems.Array()
1062+
seenCallIDs := make(map[string]struct{}, len(arr))
1063+
dupes := make(map[int]bool)
1064+
1065+
for i, item := range arr {
1066+
typ := item.Get("type").String()
1067+
if typ != "function_call_output" && typ != "tool_search_output" {
1068+
continue
1069+
}
1070+
callID := strings.TrimSpace(item.Get("call_id").String())
1071+
if callID == "" {
1072+
continue
1073+
}
1074+
if _, exists := seenCallIDs[callID]; exists {
1075+
dupes[i] = true
1076+
continue
1077+
}
1078+
seenCallIDs[callID] = struct{}{}
1079+
}
1080+
1081+
if len(dupes) == 0 {
1082+
return body
1083+
}
1084+
1085+
// 重建 input 数组,跳过标记为重复的索引
1086+
filtered := make([]byte, 0, len(inputItems.Raw))
1087+
filtered = append(filtered, '[')
1088+
first := true
1089+
for i, item := range arr {
1090+
if dupes[i] {
1091+
continue
1092+
}
1093+
if !first {
1094+
filtered = append(filtered, ',')
1095+
}
1096+
filtered = append(filtered, []byte(item.Raw)...)
1097+
first = false
1098+
}
1099+
filtered = append(filtered, ']')
1100+
1101+
out, err := sjson.SetRawBytes(body, "input", filtered)
1102+
if err != nil {
1103+
return body
1104+
}
1105+
return out
1106+
}

0 commit comments

Comments
 (0)