Preserve multi-tool context in OpenAI messages continuation
Claude Code can send one assistant turn with multiple tool_use blocks followed by a user turn containing matching tool_result blocks. The OpenAI /v1/messages compatibility path trimmed continuation input to the last user turn plus adjacent tool outputs, which could leave a function_call_output without its earlier function_call when previous_response_id was attached. This keeps all function_call items needed by retained function_call_output entries so the upstream Responses API can resolve every call_id. Constraint: Applies only to the OpenAI /v1/messages -> Responses compatibility continuation path. Rejected: Disable previous_response_id for all tool outputs | loses continuation and cache benefits for valid turns. Confidence: high Scope-risk: narrow Directive: Do not trim function_call_output entries without preserving their matching function_call call_id context. Tested: go test ./internal/service -run 'TestForwardAsAnthropic_(PreviousResponseIDKeepsMultiToolCallContext|AttachesPreviousResponseIDForCompatContinuation|OAuthPreservesClaudeCodeToolCallID)' -count=1 Tested: go test ./internal/service -run 'TestForwardAsAnthropic|TestApplyAnthropicCompatFullReplayGuard|TestOpenAICompat|Test.*ToolContinuation' -count=1 Tested: go test ./internal/pkg/apicompat -count=1 Related: #2337
This commit is contained in:
@@ -37,10 +37,7 @@ func trimAnthropicCompatResponsesInputToLatestTurn(req *apicompat.ResponsesReque
|
||||
return
|
||||
}
|
||||
|
||||
start := len(items) - 1
|
||||
for start > 0 && items[start].Type == "function_call_output" {
|
||||
start--
|
||||
}
|
||||
start := latestAnthropicCompatResponsesInputTurnStart(items)
|
||||
trimmed := append([]apicompat.ResponsesInputItem(nil), items[start:]...)
|
||||
if len(trimmed) == len(items) {
|
||||
return
|
||||
@@ -50,6 +47,63 @@ func trimAnthropicCompatResponsesInputToLatestTurn(req *apicompat.ResponsesReque
|
||||
}
|
||||
}
|
||||
|
||||
func latestAnthropicCompatResponsesInputTurnStart(items []apicompat.ResponsesInputItem) int {
|
||||
if len(items) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
start := len(items) - 1
|
||||
last := items[start]
|
||||
switch {
|
||||
case last.Type == "function_call_output":
|
||||
for start > 0 && items[start-1].Type == "function_call_output" {
|
||||
start--
|
||||
}
|
||||
case last.Type == "message" && last.Role == "user":
|
||||
for start > 0 && items[start-1].Type == "function_call_output" {
|
||||
start--
|
||||
}
|
||||
default:
|
||||
return start
|
||||
}
|
||||
|
||||
return expandAnthropicCompatResponsesInputToolCallStart(items, start)
|
||||
}
|
||||
|
||||
func expandAnthropicCompatResponsesInputToolCallStart(items []apicompat.ResponsesInputItem, start int) int {
|
||||
if start < 0 || start >= len(items) {
|
||||
return start
|
||||
}
|
||||
|
||||
needed := make(map[string]struct{})
|
||||
for i := start; i < len(items); i++ {
|
||||
if items[i].Type != "function_call_output" {
|
||||
continue
|
||||
}
|
||||
callID := strings.TrimSpace(items[i].CallID)
|
||||
if callID != "" {
|
||||
needed[callID] = struct{}{}
|
||||
}
|
||||
}
|
||||
if len(needed) == 0 {
|
||||
return start
|
||||
}
|
||||
|
||||
expandedStart := start
|
||||
for i := start - 1; i >= 0 && len(needed) > 0; i-- {
|
||||
if items[i].Type != "function_call" {
|
||||
continue
|
||||
}
|
||||
callID := strings.TrimSpace(items[i].CallID)
|
||||
if _, ok := needed[callID]; !ok {
|
||||
continue
|
||||
}
|
||||
delete(needed, callID)
|
||||
expandedStart = i
|
||||
}
|
||||
return expandedStart
|
||||
}
|
||||
|
||||
func isOpenAICompatPreviousResponseNotFound(statusCode int, upstreamMsg string, upstreamBody []byte) bool {
|
||||
if statusCode != http.StatusBadRequest && statusCode != http.StatusNotFound {
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user