service_tier_passthrough_test.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. package apicompat
  2. import (
  3. "encoding/json"
  4. "testing"
  5. "github.com/stretchr/testify/require"
  6. )
  7. // 上游响应中的 service_tier 必须如实回传,不被重写/丢弃:
  8. // 非流式(ResponsesResponse → ChatCompletionsResponse)与流式 chunk 均覆盖。
  9. func TestResponsesToChatCompletions_PreservesUpstreamServiceTier(t *testing.T) {
  10. resp := &ResponsesResponse{
  11. ID: "resp_1",
  12. Object: "response",
  13. Model: "gpt-5.5",
  14. Status: "completed",
  15. ServiceTier: "priority",
  16. Output: []ResponsesOutput{{
  17. Type: "message",
  18. Role: "assistant",
  19. Content: []ResponsesContentPart{{
  20. Type: "output_text",
  21. Text: "hi",
  22. }},
  23. }},
  24. Usage: &ResponsesUsage{InputTokens: 1, OutputTokens: 1},
  25. }
  26. chat := ResponsesToChatCompletions(resp, "gpt-5.5")
  27. require.Equal(t, "priority", chat.ServiceTier)
  28. // 序列化后字段仍在(omitempty 不丢非空值)。
  29. raw, err := json.Marshal(chat)
  30. require.NoError(t, err)
  31. require.Contains(t, string(raw), `"service_tier":"priority"`)
  32. }
  33. func TestResponsesToChatCompletions_OmitsMissingServiceTier(t *testing.T) {
  34. resp := &ResponsesResponse{ID: "resp_1", Model: "gpt-5.5", Status: "completed"}
  35. chat := ResponsesToChatCompletions(resp, "gpt-5.5")
  36. require.Empty(t, chat.ServiceTier)
  37. raw, err := json.Marshal(chat)
  38. require.NoError(t, err)
  39. require.NotContains(t, string(raw), "service_tier")
  40. }
  41. func TestResponsesEventToChatChunks_PreservesUpstreamServiceTier(t *testing.T) {
  42. state := NewResponsesEventToChatState()
  43. state.IncludeUsage = true
  44. created := &ResponsesStreamEvent{Type: "response.created"}
  45. require.NoError(t, json.Unmarshal([]byte(`{"type":"response.created","response":{"id":"resp_s1","model":"gpt-5.5","service_tier":"priority","status":"in_progress"}}`), created))
  46. chunks := ResponsesEventToChatChunks(created, state)
  47. require.NotEmpty(t, chunks)
  48. for _, chunk := range chunks {
  49. require.Equal(t, "priority", chunk.ServiceTier)
  50. }
  51. // 后续 delta chunk 继续携带(OpenAI 流式 chunk 的 service_tier 语义)。
  52. delta := &ResponsesStreamEvent{Type: "response.output_text.delta", Delta: "hi"}
  53. chunks = ResponsesEventToChatChunks(delta, state)
  54. require.NotEmpty(t, chunks)
  55. require.Equal(t, "priority", chunks[0].ServiceTier)
  56. // 终止事件同样携带。
  57. completed := &ResponsesStreamEvent{Type: "response.completed", Response: &ResponsesResponse{
  58. ID: "resp", Model: "gpt-5.5", Status: "completed",
  59. Usage: &ResponsesUsage{InputTokens: 1, OutputTokens: 1},
  60. }}
  61. chunks = ResponsesEventToChatChunks(completed, state)
  62. require.NotEmpty(t, chunks)
  63. for _, chunk := range chunks {
  64. require.Equal(t, "priority", chunk.ServiceTier)
  65. }
  66. }
  67. func TestResponsesEventToChatChunks_NoServiceTierStaysClean(t *testing.T) {
  68. state := NewResponsesEventToChatState()
  69. created := &ResponsesStreamEvent{Type: "response.created", Response: &ResponsesResponse{ID: "resp", Model: "gpt-5.5"}}
  70. chunks := ResponsesEventToChatChunks(created, state)
  71. require.NotEmpty(t, chunks)
  72. require.Empty(t, chunks[0].ServiceTier)
  73. raw, err := json.Marshal(chunks[0])
  74. require.NoError(t, err)
  75. require.NotContains(t, string(raw), "service_tier")
  76. }
  77. // 上游 JSON 反序列化时 service_tier 进入 ResponsesResponse(缓冲桥读取链路)。
  78. func TestResponsesResponse_UnmarshalPreservesServiceTier(t *testing.T) {
  79. var resp ResponsesResponse
  80. require.NoError(t, json.Unmarshal([]byte(`{"id":"resp_1","object":"response","model":"gpt-5.5","status":"completed","service_tier":"flex","output":[]}`), &resp))
  81. require.Equal(t, "flex", resp.ServiceTier)
  82. }
  83. // ---------------------------------------------------------------------------
  84. // 反向转换(Chat-only fallback):CC 响应/流 chunk 的 service_tier 保留到
  85. // Responses 形态,客户端与计费都能拿到上游回显。
  86. // ---------------------------------------------------------------------------
  87. func TestChatCompletionsResponseToResponses_PreservesServiceTier(t *testing.T) {
  88. cc := &ChatCompletionsResponse{
  89. ID: "chatcmpl-1",
  90. Model: "gpt-5.5",
  91. ServiceTier: "default",
  92. Choices: []ChatChoice{{
  93. Index: 0,
  94. Message: ChatMessage{Role: "assistant", Content: json.RawMessage(`"hi"`)},
  95. FinishReason: "stop",
  96. }},
  97. Usage: &ChatUsage{PromptTokens: 1, CompletionTokens: 1, TotalTokens: 2},
  98. }
  99. resp := ChatCompletionsResponseToResponses(cc, "gpt-5.5", nil, nil, false, nil)
  100. require.Equal(t, "default", resp.ServiceTier)
  101. raw, err := json.Marshal(resp)
  102. require.NoError(t, err)
  103. require.Contains(t, string(raw), `"service_tier":"default"`)
  104. }
  105. func TestChatCompletionsResponseToResponses_NilRespOmitsServiceTier(t *testing.T) {
  106. resp := ChatCompletionsResponseToResponses(nil, "gpt-5.5", nil, nil, false, nil)
  107. require.Empty(t, resp.ServiceTier)
  108. }
  109. func TestChatCompletionsChunkToResponsesEvents_PreservesServiceTier(t *testing.T) {
  110. state := NewChatCompletionsToResponsesStreamState("gpt-5.5")
  111. chunk := &ChatCompletionsChunk{
  112. ID: "chatcmpl-2",
  113. Model: "gpt-5.5",
  114. ServiceTier: "flex",
  115. Choices: []ChatChunkChoice{{
  116. Index: 0,
  117. Delta: ChatDelta{Content: strPtr("hi")},
  118. }},
  119. }
  120. events := ChatCompletionsChunkToResponsesEvents(chunk, state)
  121. require.NotEmpty(t, events)
  122. // response.created 携带 service_tier。
  123. created := findEvent(events, "response.created")
  124. require.NotNil(t, created)
  125. require.NotNil(t, created.Response)
  126. require.Equal(t, "flex", created.Response.ServiceTier)
  127. // 终止事件同样携带。
  128. final := FinalizeChatCompletionsResponsesStream(state)
  129. completed := findEvent(final, "response.completed")
  130. require.NotNil(t, completed)
  131. require.NotNil(t, completed.Response)
  132. require.Equal(t, "flex", completed.Response.ServiceTier)
  133. }
  134. func TestChatCompletionsChunkToResponsesEvents_NoTierStaysClean(t *testing.T) {
  135. state := NewChatCompletionsToResponsesStreamState("gpt-5.5")
  136. chunk := &ChatCompletionsChunk{ID: "chatcmpl-3", Model: "gpt-5.5"}
  137. events := ChatCompletionsChunkToResponsesEvents(chunk, state)
  138. created := findEvent(events, "response.created")
  139. require.NotNil(t, created)
  140. require.Empty(t, created.Response.ServiceTier)
  141. raw, err := json.Marshal(created)
  142. require.NoError(t, err)
  143. require.NotContains(t, string(raw), "service_tier")
  144. }
  145. func findEvent(events []ResponsesStreamEvent, eventType string) *ResponsesStreamEvent {
  146. for i := range events {
  147. if events[i].Type == eventType {
  148. return &events[i]
  149. }
  150. }
  151. return nil
  152. }