mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-20 04:57:01 +08:00
fix: keep tool call replay when assistant text interleaves call and result
trimReplayDanglingAssistantToolCalls only collected tool results that immediately followed the assistant tool-call message. Models such as gpt-5.3-codex-spark may emit the function_call item before the explanation text within one response, so history replay order becomes assistant[tool_call] -> assistant[text] -> tool[result]. The call was misjudged as dangling and stripped while the tool result survived, producing a function_call_output without a matching function_call that the Responses API rejects with 400. - widen the response collection window to skip interleaved plain assistant text messages, and drop orphan tool results in the same pass - synthesize a placeholder function_call (or drop the output when the tool name is unknown) in normalizeOpenAIResponsesInput so conversations already persisted with corrupted history can resume
This commit is contained in:
@@ -1196,57 +1196,82 @@ func mergeReplayReasoningMetadata(last *modeladapter.Message, current modeladapt
|
||||
}
|
||||
}
|
||||
|
||||
// replayToolResponseWindowEnd 返回 assistant tool-call 消息的响应收集窗口右边界(不含)。
|
||||
// 窗口内除了 tool 结果消息,还允许出现同轮穿插的纯文本 assistant 消息:
|
||||
// 部分模型(如 gpt-5.3-codex-spark)会在同一条响应里先输出 function_call 再输出说明文本,
|
||||
// 落盘顺序为 tool_call → assistant_text → tool_result,若只收集紧邻的 tool 消息,
|
||||
// 会把有结果回放的调用误判为悬空。
|
||||
func replayToolResponseWindowEnd(messages []modeladapter.Message, index int) int {
|
||||
end := index + 1
|
||||
for end < len(messages) {
|
||||
candidate := messages[end]
|
||||
switch {
|
||||
case strings.TrimSpace(candidate.Role) == "tool":
|
||||
end++
|
||||
case strings.TrimSpace(candidate.Role) == "assistant" && len(candidate.ToolCalls) == 0:
|
||||
end++
|
||||
default:
|
||||
return end
|
||||
}
|
||||
}
|
||||
return end
|
||||
}
|
||||
|
||||
func trimReplayDanglingAssistantToolCalls(messages []modeladapter.Message) []modeladapter.Message {
|
||||
if len(messages) == 0 {
|
||||
return nil
|
||||
}
|
||||
trimmed := make([]modeladapter.Message, 0, len(messages))
|
||||
for index := 0; index < len(messages); index++ {
|
||||
message := cloneReplayModelMessage(messages[index])
|
||||
survivingToolCallIDs := make(map[string]struct{})
|
||||
for index, message := range messages {
|
||||
if strings.TrimSpace(message.Role) != "assistant" || len(message.ToolCalls) == 0 {
|
||||
continue
|
||||
}
|
||||
responded := make(map[string]struct{}, len(message.ToolCalls))
|
||||
for scan := index + 1; scan < replayToolResponseWindowEnd(messages, index); scan++ {
|
||||
if strings.TrimSpace(messages[scan].Role) != "tool" {
|
||||
continue
|
||||
}
|
||||
if toolCallID := strings.TrimSpace(messages[scan].ToolCallID); toolCallID != "" {
|
||||
responded[toolCallID] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, toolCall := range message.ToolCalls {
|
||||
if toolCallID := strings.TrimSpace(toolCall.ID); toolCallID != "" {
|
||||
if _, ok := responded[toolCallID]; ok {
|
||||
survivingToolCallIDs[toolCallID] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
trimmed := make([]modeladapter.Message, 0, len(messages))
|
||||
for _, item := range messages {
|
||||
message := cloneReplayModelMessage(item)
|
||||
if strings.TrimSpace(message.Role) == "assistant" && len(message.ToolCalls) > 0 {
|
||||
nextToolCalls := make([]modeladapter.ToolCallDescriptor, 0, len(message.ToolCalls))
|
||||
for _, toolCall := range message.ToolCalls {
|
||||
if _, ok := survivingToolCallIDs[strings.TrimSpace(toolCall.ID)]; !ok {
|
||||
continue
|
||||
}
|
||||
toolCall.Index = len(nextToolCalls)
|
||||
nextToolCalls = append(nextToolCalls, toolCall)
|
||||
}
|
||||
if len(nextToolCalls) == 0 {
|
||||
if strings.TrimSpace(message.Content) == "" && len(message.ContentParts) == 0 && !hasReplayableReasoningPayload(message.ReasoningContent, message.ReasoningSignature, message.ReasoningSignatureSource) {
|
||||
continue
|
||||
}
|
||||
message.ToolCalls = nil
|
||||
} else {
|
||||
message.ToolCalls = nextToolCalls
|
||||
}
|
||||
trimmed = append(trimmed, message)
|
||||
continue
|
||||
}
|
||||
|
||||
end := index + 1
|
||||
responded := make(map[string]struct{}, len(message.ToolCalls))
|
||||
for end < len(messages) && strings.TrimSpace(messages[end].Role) == "tool" {
|
||||
toolCallID := strings.TrimSpace(messages[end].ToolCallID)
|
||||
if toolCallID != "" {
|
||||
responded[toolCallID] = struct{}{}
|
||||
}
|
||||
end++
|
||||
}
|
||||
|
||||
nextToolCalls := make([]modeladapter.ToolCallDescriptor, 0, len(message.ToolCalls))
|
||||
allowedToolCallIDs := make(map[string]struct{}, len(message.ToolCalls))
|
||||
for _, toolCall := range message.ToolCalls {
|
||||
toolCallID := strings.TrimSpace(toolCall.ID)
|
||||
if _, ok := responded[toolCallID]; !ok {
|
||||
if strings.TrimSpace(message.Role) == "tool" && strings.TrimSpace(message.ToolCallID) != "" {
|
||||
if _, ok := survivingToolCallIDs[strings.TrimSpace(message.ToolCallID)]; !ok {
|
||||
continue
|
||||
}
|
||||
item := toolCall
|
||||
item.Index = len(nextToolCalls)
|
||||
nextToolCalls = append(nextToolCalls, item)
|
||||
allowedToolCallIDs[toolCallID] = struct{}{}
|
||||
}
|
||||
|
||||
if len(nextToolCalls) > 0 {
|
||||
message.ToolCalls = nextToolCalls
|
||||
trimmed = append(trimmed, message)
|
||||
for toolIndex := index + 1; toolIndex < end; toolIndex++ {
|
||||
toolMessage := cloneReplayModelMessage(messages[toolIndex])
|
||||
if _, ok := allowedToolCallIDs[strings.TrimSpace(toolMessage.ToolCallID)]; !ok {
|
||||
continue
|
||||
}
|
||||
trimmed = append(trimmed, toolMessage)
|
||||
}
|
||||
} else if strings.TrimSpace(message.Content) != "" || len(message.ContentParts) > 0 || hasReplayableReasoningPayload(message.ReasoningContent, message.ReasoningSignature, message.ReasoningSignatureSource) {
|
||||
message.ToolCalls = nil
|
||||
trimmed = append(trimmed, message)
|
||||
}
|
||||
|
||||
index = end - 1
|
||||
trimmed = append(trimmed, message)
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user