You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

246 lines
7.5 KiB

  1. package claude
  2. import (
  3. "io"
  4. "net/http"
  5. "net/http/httptest"
  6. "strings"
  7. "testing"
  8. "github.com/QuantumNous/new-api/constant"
  9. "github.com/QuantumNous/new-api/dto"
  10. relaycommon "github.com/QuantumNous/new-api/relay/common"
  11. "github.com/QuantumNous/new-api/types"
  12. "github.com/gin-gonic/gin"
  13. "github.com/stretchr/testify/require"
  14. )
  15. func TestFormatClaudeResponseInfo_MessageStart(t *testing.T) {
  16. claudeInfo := &ClaudeResponseInfo{
  17. Usage: &dto.Usage{},
  18. }
  19. claudeResponse := &dto.ClaudeResponse{
  20. Type: "message_start",
  21. Message: &dto.ClaudeMediaMessage{
  22. Id: "msg_123",
  23. Model: "claude-3-5-sonnet",
  24. Usage: &dto.ClaudeUsage{
  25. InputTokens: 100,
  26. OutputTokens: 1,
  27. CacheCreationInputTokens: 50,
  28. CacheReadInputTokens: 30,
  29. },
  30. },
  31. }
  32. ok := FormatClaudeResponseInfo(claudeResponse, nil, claudeInfo)
  33. if !ok {
  34. t.Fatal("expected true")
  35. }
  36. if claudeInfo.Usage.PromptTokens != 100 {
  37. t.Errorf("PromptTokens = %d, want 100", claudeInfo.Usage.PromptTokens)
  38. }
  39. if claudeInfo.Usage.PromptTokensDetails.CachedTokens != 30 {
  40. t.Errorf("CachedTokens = %d, want 30", claudeInfo.Usage.PromptTokensDetails.CachedTokens)
  41. }
  42. if claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens != 50 {
  43. t.Errorf("CachedCreationTokens = %d, want 50", claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens)
  44. }
  45. if claudeInfo.ResponseId != "msg_123" {
  46. t.Errorf("ResponseId = %s, want msg_123", claudeInfo.ResponseId)
  47. }
  48. if claudeInfo.Model != "claude-3-5-sonnet" {
  49. t.Errorf("Model = %s, want claude-3-5-sonnet", claudeInfo.Model)
  50. }
  51. }
  52. func TestFormatClaudeResponseInfo_MessageDelta_FullUsage(t *testing.T) {
  53. // message_start 先积累 usage
  54. claudeInfo := &ClaudeResponseInfo{
  55. Usage: &dto.Usage{
  56. PromptTokens: 100,
  57. PromptTokensDetails: dto.InputTokenDetails{
  58. CachedTokens: 30,
  59. CachedCreationTokens: 50,
  60. },
  61. CompletionTokens: 1,
  62. },
  63. }
  64. // message_delta 带完整 usage(原生 Anthropic 场景)
  65. claudeResponse := &dto.ClaudeResponse{
  66. Type: "message_delta",
  67. Usage: &dto.ClaudeUsage{
  68. InputTokens: 100,
  69. OutputTokens: 200,
  70. CacheCreationInputTokens: 50,
  71. CacheReadInputTokens: 30,
  72. },
  73. }
  74. ok := FormatClaudeResponseInfo(claudeResponse, nil, claudeInfo)
  75. if !ok {
  76. t.Fatal("expected true")
  77. }
  78. if claudeInfo.Usage.PromptTokens != 100 {
  79. t.Errorf("PromptTokens = %d, want 100", claudeInfo.Usage.PromptTokens)
  80. }
  81. if claudeInfo.Usage.CompletionTokens != 200 {
  82. t.Errorf("CompletionTokens = %d, want 200", claudeInfo.Usage.CompletionTokens)
  83. }
  84. if claudeInfo.Usage.TotalTokens != 300 {
  85. t.Errorf("TotalTokens = %d, want 300", claudeInfo.Usage.TotalTokens)
  86. }
  87. if !claudeInfo.Done {
  88. t.Error("expected Done = true")
  89. }
  90. }
  91. func TestFormatClaudeResponseInfo_MessageDelta_OnlyOutputTokens(t *testing.T) {
  92. // 模拟 Bedrock: message_start 已积累 usage
  93. claudeInfo := &ClaudeResponseInfo{
  94. Usage: &dto.Usage{
  95. PromptTokens: 100,
  96. PromptTokensDetails: dto.InputTokenDetails{
  97. CachedTokens: 30,
  98. CachedCreationTokens: 50,
  99. },
  100. CompletionTokens: 1,
  101. ClaudeCacheCreation5mTokens: 10,
  102. ClaudeCacheCreation1hTokens: 20,
  103. },
  104. }
  105. // Bedrock 的 message_delta 只有 output_tokens,缺少 input_tokens 和 cache 字段
  106. claudeResponse := &dto.ClaudeResponse{
  107. Type: "message_delta",
  108. Usage: &dto.ClaudeUsage{
  109. OutputTokens: 200,
  110. // InputTokens, CacheCreationInputTokens, CacheReadInputTokens 都是 0
  111. },
  112. }
  113. ok := FormatClaudeResponseInfo(claudeResponse, nil, claudeInfo)
  114. if !ok {
  115. t.Fatal("expected true")
  116. }
  117. // PromptTokens 应保持 message_start 的值(因为 message_delta 的 InputTokens=0,不更新)
  118. if claudeInfo.Usage.PromptTokens != 100 {
  119. t.Errorf("PromptTokens = %d, want 100", claudeInfo.Usage.PromptTokens)
  120. }
  121. if claudeInfo.Usage.CompletionTokens != 200 {
  122. t.Errorf("CompletionTokens = %d, want 200", claudeInfo.Usage.CompletionTokens)
  123. }
  124. if claudeInfo.Usage.TotalTokens != 300 {
  125. t.Errorf("TotalTokens = %d, want 300", claudeInfo.Usage.TotalTokens)
  126. }
  127. // cache 字段应保持 message_start 的值
  128. if claudeInfo.Usage.PromptTokensDetails.CachedTokens != 30 {
  129. t.Errorf("CachedTokens = %d, want 30", claudeInfo.Usage.PromptTokensDetails.CachedTokens)
  130. }
  131. if claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens != 50 {
  132. t.Errorf("CachedCreationTokens = %d, want 50", claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens)
  133. }
  134. if claudeInfo.Usage.ClaudeCacheCreation5mTokens != 10 {
  135. t.Errorf("ClaudeCacheCreation5mTokens = %d, want 10", claudeInfo.Usage.ClaudeCacheCreation5mTokens)
  136. }
  137. if claudeInfo.Usage.ClaudeCacheCreation1hTokens != 20 {
  138. t.Errorf("ClaudeCacheCreation1hTokens = %d, want 20", claudeInfo.Usage.ClaudeCacheCreation1hTokens)
  139. }
  140. if !claudeInfo.Done {
  141. t.Error("expected Done = true")
  142. }
  143. }
  144. func TestFormatClaudeResponseInfo_NilClaudeInfo(t *testing.T) {
  145. claudeResponse := &dto.ClaudeResponse{Type: "message_start"}
  146. ok := FormatClaudeResponseInfo(claudeResponse, nil, nil)
  147. if ok {
  148. t.Error("expected false for nil claudeInfo")
  149. }
  150. }
  151. func TestFormatClaudeResponseInfo_ContentBlockDelta(t *testing.T) {
  152. text := "hello"
  153. claudeInfo := &ClaudeResponseInfo{
  154. Usage: &dto.Usage{},
  155. ResponseText: strings.Builder{},
  156. }
  157. claudeResponse := &dto.ClaudeResponse{
  158. Type: "content_block_delta",
  159. Delta: &dto.ClaudeMediaMessage{
  160. Text: &text,
  161. },
  162. }
  163. ok := FormatClaudeResponseInfo(claudeResponse, nil, claudeInfo)
  164. if !ok {
  165. t.Fatal("expected true")
  166. }
  167. if claudeInfo.ResponseText.String() != "hello" {
  168. t.Errorf("ResponseText = %q, want %q", claudeInfo.ResponseText.String(), "hello")
  169. }
  170. }
  171. func TestClaudeStreamHandlerReturnsErrorOnInitialErrorEvent(t *testing.T) {
  172. gin.SetMode(gin.TestMode)
  173. oldTimeout := constant.StreamingTimeout
  174. constant.StreamingTimeout = 30
  175. t.Cleanup(func() {
  176. constant.StreamingTimeout = oldTimeout
  177. })
  178. w := httptest.NewRecorder()
  179. c, _ := gin.CreateTestContext(w)
  180. c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil)
  181. errorEvent := `{"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}`
  182. resp := &http.Response{
  183. StatusCode: http.StatusOK,
  184. Body: io.NopCloser(strings.NewReader("data: " + errorEvent + "\n\n")),
  185. }
  186. info := &relaycommon.RelayInfo{
  187. RelayFormat: types.RelayFormatClaude,
  188. ChannelMeta: &relaycommon.ChannelMeta{
  189. UpstreamModelName: "claude-test",
  190. },
  191. }
  192. usage, err := ClaudeStreamHandler(c, resp, info)
  193. require.Nil(t, usage)
  194. require.Error(t, err)
  195. require.Empty(t, w.Body.String())
  196. }
  197. func TestClaudeStreamHandlerDoesNotRetryAfterNormalStreamData(t *testing.T) {
  198. gin.SetMode(gin.TestMode)
  199. oldTimeout := constant.StreamingTimeout
  200. constant.StreamingTimeout = 30
  201. t.Cleanup(func() {
  202. constant.StreamingTimeout = oldTimeout
  203. })
  204. w := httptest.NewRecorder()
  205. c, _ := gin.CreateTestContext(w)
  206. c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil)
  207. normalEvent := `{"type":"message_start","message":{"id":"msg_test","model":"claude-test","usage":{"input_tokens":1,"output_tokens":1}}}`
  208. errorEvent := `{"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}`
  209. resp := &http.Response{
  210. StatusCode: http.StatusOK,
  211. Body: io.NopCloser(strings.NewReader("data: " + normalEvent + "\n\ndata: " + errorEvent + "\n\n")),
  212. }
  213. info := &relaycommon.RelayInfo{
  214. RelayFormat: types.RelayFormatClaude,
  215. ChannelMeta: &relaycommon.ChannelMeta{
  216. UpstreamModelName: "claude-test",
  217. },
  218. }
  219. usage, err := ClaudeStreamHandler(c, resp, info)
  220. require.Nil(t, err)
  221. require.NotNil(t, usage)
  222. require.Contains(t, w.Body.String(), "message_start")
  223. require.NotContains(t, w.Body.String(), "overloaded_error")
  224. }