streaming_stop_reason_test.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. package apicompat
  2. import (
  3. "testing"
  4. "github.com/stretchr/testify/assert"
  5. "github.com/stretchr/testify/require"
  6. )
  7. func TestAnthropicStreamingMaxTokens_MapsToIncomplete(t *testing.T) {
  8. state := NewAnthropicEventToResponsesState()
  9. AnthropicEventToResponsesEvents(&AnthropicStreamEvent{
  10. Type: "message_start",
  11. Message: &AnthropicResponse{ID: "msg_test", Model: "claude-opus-4-6", Role: "assistant"},
  12. }, state)
  13. AnthropicEventToResponsesEvents(&AnthropicStreamEvent{
  14. Type: "message_delta",
  15. Delta: &AnthropicDelta{
  16. StopReason: "max_tokens",
  17. },
  18. Usage: &AnthropicUsage{OutputTokens: 4096},
  19. }, state)
  20. require.Equal(t, "max_tokens", state.StopReason)
  21. events := AnthropicEventToResponsesEvents(&AnthropicStreamEvent{
  22. Type: "message_stop",
  23. }, state)
  24. var completed *ResponsesStreamEvent
  25. for i := range events {
  26. if events[i].Type == "response.completed" || events[i].Type == "response.incomplete" {
  27. completed = &events[i]
  28. break
  29. }
  30. }
  31. require.NotNil(t, completed, "should have terminal event")
  32. assert.Equal(t, "response.incomplete", completed.Type)
  33. require.NotNil(t, completed.Response)
  34. assert.Equal(t, "incomplete", completed.Response.Status)
  35. require.NotNil(t, completed.Response.IncompleteDetails)
  36. assert.Equal(t, "max_output_tokens", completed.Response.IncompleteDetails.Reason)
  37. }
  38. func TestAnthropicStreamingMaxTokens_FinalizeMapsToIncompleteWithoutMessageStop(t *testing.T) {
  39. state := NewAnthropicEventToResponsesState()
  40. AnthropicEventToResponsesEvents(&AnthropicStreamEvent{
  41. Type: "message_start",
  42. Message: &AnthropicResponse{ID: "msg_test", Model: "claude-opus-4-6", Role: "assistant"},
  43. }, state)
  44. AnthropicEventToResponsesEvents(&AnthropicStreamEvent{
  45. Type: "message_delta",
  46. Delta: &AnthropicDelta{StopReason: "max_tokens"},
  47. Usage: &AnthropicUsage{OutputTokens: 4096},
  48. }, state)
  49. events := FinalizeAnthropicResponsesStream(state)
  50. require.Len(t, events, 1)
  51. assert.Equal(t, "response.incomplete", events[0].Type)
  52. require.NotNil(t, events[0].Response)
  53. assert.Equal(t, "incomplete", events[0].Response.Status)
  54. require.NotNil(t, events[0].Response.IncompleteDetails)
  55. assert.Equal(t, "max_output_tokens", events[0].Response.IncompleteDetails.Reason)
  56. assert.Empty(t, FinalizeAnthropicResponsesStream(state), "repeated finalization must be idempotent")
  57. }
  58. func TestAnthropicStreamingEndTurn_MapsToCompleted(t *testing.T) {
  59. state := NewAnthropicEventToResponsesState()
  60. AnthropicEventToResponsesEvents(&AnthropicStreamEvent{
  61. Type: "message_start",
  62. Message: &AnthropicResponse{ID: "msg_test", Model: "claude-opus-4-6", Role: "assistant"},
  63. }, state)
  64. AnthropicEventToResponsesEvents(&AnthropicStreamEvent{
  65. Type: "message_delta",
  66. Delta: &AnthropicDelta{StopReason: "end_turn"},
  67. Usage: &AnthropicUsage{OutputTokens: 100},
  68. }, state)
  69. events := AnthropicEventToResponsesEvents(&AnthropicStreamEvent{
  70. Type: "message_stop",
  71. }, state)
  72. var completed *ResponsesStreamEvent
  73. for i := range events {
  74. if events[i].Type == "response.completed" {
  75. completed = &events[i]
  76. break
  77. }
  78. }
  79. require.NotNil(t, completed)
  80. assert.Equal(t, "completed", completed.Response.Status)
  81. assert.Nil(t, completed.Response.IncompleteDetails)
  82. }
  83. func TestResponsesToChatCompletions_ContentFilter(t *testing.T) {
  84. resp := &ResponsesResponse{
  85. ID: "resp_cf",
  86. Status: "incomplete",
  87. IncompleteDetails: &ResponsesIncompleteDetails{
  88. Reason: "content_filter",
  89. },
  90. Output: []ResponsesOutput{{
  91. Type: "message",
  92. Content: []ResponsesContentPart{{Type: "output_text", Text: "partial"}},
  93. }},
  94. Usage: &ResponsesUsage{InputTokens: 10, OutputTokens: 5},
  95. }
  96. cc := ResponsesToChatCompletions(resp, "gpt-5.5")
  97. require.Len(t, cc.Choices, 1)
  98. assert.Equal(t, "content_filter", cc.Choices[0].FinishReason)
  99. }
  100. func TestResponsesToChatCompletionsStreaming_ContentFilter(t *testing.T) {
  101. state := NewResponsesEventToChatState()
  102. state.ID = "resp_cf"
  103. state.Model = "gpt-5.5"
  104. state.SentRole = true
  105. events := ResponsesEventToChatChunks(&ResponsesStreamEvent{
  106. Type: "response.completed",
  107. Response: &ResponsesResponse{
  108. ID: "resp_cf",
  109. Status: "incomplete",
  110. IncompleteDetails: &ResponsesIncompleteDetails{
  111. Reason: "content_filter",
  112. },
  113. },
  114. }, state)
  115. hasContentFilter := false
  116. for _, chunk := range events {
  117. for _, choice := range chunk.Choices {
  118. if choice.FinishReason != nil && *choice.FinishReason == "content_filter" {
  119. hasContentFilter = true
  120. }
  121. }
  122. }
  123. assert.True(t, hasContentFilter, "streaming content_filter should map to finish_reason content_filter")
  124. }