diff --git a/backend/internal/pkg/apicompat/anthropic_to_responses_response.go b/backend/internal/pkg/apicompat/anthropic_to_responses_response.go index 661b47cebe..9ca20e1cdb 100644 --- a/backend/internal/pkg/apicompat/anthropic_to_responses_response.go +++ b/backend/internal/pkg/apicompat/anthropic_to_responses_response.go @@ -211,8 +211,8 @@ func FinalizeAnthropicResponsesStream(state *AnthropicEventToResponsesState) []R // Close any open item events = append(events, closeCurrentResponsesItem(state)...) - // Emit response.completed - events = append(events, makeResponsesCompletedEvent(state, "completed", nil)) + status, incompleteDetails := anthropicResponsesStreamTerminalState(state.StopReason) + events = append(events, makeResponsesCompletedEvent(state, status, incompleteDetails)) state.CompletedSent = true return events } @@ -434,13 +434,7 @@ func anthToResHandleMessageStop(state *AnthropicEventToResponsesState) []Respons var events []ResponsesStreamEvent events = append(events, closeCurrentResponsesItem(state)...) - status := "completed" - var incompleteDetails *ResponsesIncompleteDetails - if state.StopReason == "max_tokens" { - status = "incomplete" - incompleteDetails = &ResponsesIncompleteDetails{Reason: "max_output_tokens"} - } - + status, incompleteDetails := anthropicResponsesStreamTerminalState(state.StopReason) events = append(events, makeResponsesCompletedEvent(state, status, incompleteDetails)) state.CompletedSent = true return events @@ -448,6 +442,13 @@ func anthToResHandleMessageStop(state *AnthropicEventToResponsesState) []Respons // --- helper functions --- +func anthropicResponsesStreamTerminalState(stopReason string) (string, *ResponsesIncompleteDetails) { + if stopReason == "max_tokens" { + return "incomplete", &ResponsesIncompleteDetails{Reason: "max_output_tokens"} + } + return "completed", nil +} + func closeCurrentResponsesItem(state *AnthropicEventToResponsesState) []ResponsesStreamEvent { if state.CurrentItemType == "" { return nil diff --git a/backend/internal/pkg/apicompat/streaming_stop_reason_test.go b/backend/internal/pkg/apicompat/streaming_stop_reason_test.go index c2889f0251..7147e90105 100644 --- a/backend/internal/pkg/apicompat/streaming_stop_reason_test.go +++ b/backend/internal/pkg/apicompat/streaming_stop_reason_test.go @@ -44,6 +44,29 @@ func TestAnthropicStreamingMaxTokens_MapsToIncomplete(t *testing.T) { assert.Equal(t, "max_output_tokens", completed.Response.IncompleteDetails.Reason) } +func TestAnthropicStreamingMaxTokens_FinalizeMapsToIncompleteWithoutMessageStop(t *testing.T) { + state := NewAnthropicEventToResponsesState() + + AnthropicEventToResponsesEvents(&AnthropicStreamEvent{ + Type: "message_start", + Message: &AnthropicResponse{ID: "msg_test", Model: "claude-opus-4-6", Role: "assistant"}, + }, state) + AnthropicEventToResponsesEvents(&AnthropicStreamEvent{ + Type: "message_delta", + Delta: &AnthropicDelta{StopReason: "max_tokens"}, + Usage: &AnthropicUsage{OutputTokens: 4096}, + }, state) + + events := FinalizeAnthropicResponsesStream(state) + require.Len(t, events, 1) + assert.Equal(t, "response.incomplete", events[0].Type) + require.NotNil(t, events[0].Response) + assert.Equal(t, "incomplete", events[0].Response.Status) + require.NotNil(t, events[0].Response.IncompleteDetails) + assert.Equal(t, "max_output_tokens", events[0].Response.IncompleteDetails.Reason) + assert.Empty(t, FinalizeAnthropicResponsesStream(state), "repeated finalization must be idempotent") +} + func TestAnthropicStreamingEndTurn_MapsToCompleted(t *testing.T) { state := NewAnthropicEventToResponsesState()