Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 

334 řádky
10 KiB

  1. package claude
  2. import (
  3. "encoding/json"
  4. "strings"
  5. "testing"
  6. "github.com/QuantumNous/new-api/dto"
  7. "github.com/stretchr/testify/require"
  8. )
  9. func TestFormatClaudeResponseInfo_MessageStart(t *testing.T) {
  10. claudeInfo := &ClaudeResponseInfo{
  11. Usage: &dto.Usage{},
  12. }
  13. claudeResponse := &dto.ClaudeResponse{
  14. Type: "message_start",
  15. Message: &dto.ClaudeMediaMessage{
  16. Id: "msg_123",
  17. Model: "claude-3-5-sonnet",
  18. Usage: &dto.ClaudeUsage{
  19. InputTokens: 100,
  20. OutputTokens: 1,
  21. CacheCreationInputTokens: 50,
  22. CacheReadInputTokens: 30,
  23. },
  24. },
  25. }
  26. ok := FormatClaudeResponseInfo(claudeResponse, nil, claudeInfo)
  27. if !ok {
  28. t.Fatal("expected true")
  29. }
  30. if claudeInfo.Usage.PromptTokens != 100 {
  31. t.Errorf("PromptTokens = %d, want 100", claudeInfo.Usage.PromptTokens)
  32. }
  33. if claudeInfo.Usage.PromptTokensDetails.CachedTokens != 30 {
  34. t.Errorf("CachedTokens = %d, want 30", claudeInfo.Usage.PromptTokensDetails.CachedTokens)
  35. }
  36. if claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens != 50 {
  37. t.Errorf("CachedCreationTokens = %d, want 50", claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens)
  38. }
  39. if claudeInfo.ResponseId != "msg_123" {
  40. t.Errorf("ResponseId = %s, want msg_123", claudeInfo.ResponseId)
  41. }
  42. if claudeInfo.Model != "claude-3-5-sonnet" {
  43. t.Errorf("Model = %s, want claude-3-5-sonnet", claudeInfo.Model)
  44. }
  45. }
  46. func TestFormatClaudeResponseInfo_MessageDelta_FullUsage(t *testing.T) {
  47. // message_start 先积累 usage
  48. claudeInfo := &ClaudeResponseInfo{
  49. Usage: &dto.Usage{
  50. PromptTokens: 100,
  51. PromptTokensDetails: dto.InputTokenDetails{
  52. CachedTokens: 30,
  53. CachedCreationTokens: 50,
  54. },
  55. CompletionTokens: 1,
  56. },
  57. }
  58. // message_delta 带完整 usage(原生 Anthropic 场景)
  59. claudeResponse := &dto.ClaudeResponse{
  60. Type: "message_delta",
  61. Usage: &dto.ClaudeUsage{
  62. InputTokens: 100,
  63. OutputTokens: 200,
  64. CacheCreationInputTokens: 50,
  65. CacheReadInputTokens: 30,
  66. },
  67. }
  68. ok := FormatClaudeResponseInfo(claudeResponse, nil, claudeInfo)
  69. if !ok {
  70. t.Fatal("expected true")
  71. }
  72. if claudeInfo.Usage.PromptTokens != 100 {
  73. t.Errorf("PromptTokens = %d, want 100", claudeInfo.Usage.PromptTokens)
  74. }
  75. if claudeInfo.Usage.CompletionTokens != 200 {
  76. t.Errorf("CompletionTokens = %d, want 200", claudeInfo.Usage.CompletionTokens)
  77. }
  78. if claudeInfo.Usage.TotalTokens != 300 {
  79. t.Errorf("TotalTokens = %d, want 300", claudeInfo.Usage.TotalTokens)
  80. }
  81. if !claudeInfo.Done {
  82. t.Error("expected Done = true")
  83. }
  84. }
  85. func TestFormatClaudeResponseInfo_MessageDelta_OnlyOutputTokens(t *testing.T) {
  86. // 模拟 Bedrock: message_start 已积累 usage
  87. claudeInfo := &ClaudeResponseInfo{
  88. Usage: &dto.Usage{
  89. PromptTokens: 100,
  90. PromptTokensDetails: dto.InputTokenDetails{
  91. CachedTokens: 30,
  92. CachedCreationTokens: 50,
  93. },
  94. CompletionTokens: 1,
  95. ClaudeCacheCreation5mTokens: 10,
  96. ClaudeCacheCreation1hTokens: 20,
  97. },
  98. }
  99. // Bedrock 的 message_delta 只有 output_tokens,缺少 input_tokens 和 cache 字段
  100. claudeResponse := &dto.ClaudeResponse{
  101. Type: "message_delta",
  102. Usage: &dto.ClaudeUsage{
  103. OutputTokens: 200,
  104. // InputTokens, CacheCreationInputTokens, CacheReadInputTokens 都是 0
  105. },
  106. }
  107. ok := FormatClaudeResponseInfo(claudeResponse, nil, claudeInfo)
  108. if !ok {
  109. t.Fatal("expected true")
  110. }
  111. // PromptTokens 应保持 message_start 的值(因为 message_delta 的 InputTokens=0,不更新)
  112. if claudeInfo.Usage.PromptTokens != 100 {
  113. t.Errorf("PromptTokens = %d, want 100", claudeInfo.Usage.PromptTokens)
  114. }
  115. if claudeInfo.Usage.CompletionTokens != 200 {
  116. t.Errorf("CompletionTokens = %d, want 200", claudeInfo.Usage.CompletionTokens)
  117. }
  118. if claudeInfo.Usage.TotalTokens != 300 {
  119. t.Errorf("TotalTokens = %d, want 300", claudeInfo.Usage.TotalTokens)
  120. }
  121. // cache 字段应保持 message_start 的值
  122. if claudeInfo.Usage.PromptTokensDetails.CachedTokens != 30 {
  123. t.Errorf("CachedTokens = %d, want 30", claudeInfo.Usage.PromptTokensDetails.CachedTokens)
  124. }
  125. if claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens != 50 {
  126. t.Errorf("CachedCreationTokens = %d, want 50", claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens)
  127. }
  128. if claudeInfo.Usage.ClaudeCacheCreation5mTokens != 10 {
  129. t.Errorf("ClaudeCacheCreation5mTokens = %d, want 10", claudeInfo.Usage.ClaudeCacheCreation5mTokens)
  130. }
  131. if claudeInfo.Usage.ClaudeCacheCreation1hTokens != 20 {
  132. t.Errorf("ClaudeCacheCreation1hTokens = %d, want 20", claudeInfo.Usage.ClaudeCacheCreation1hTokens)
  133. }
  134. if !claudeInfo.Done {
  135. t.Error("expected Done = true")
  136. }
  137. }
  138. func TestFormatClaudeResponseInfo_NilClaudeInfo(t *testing.T) {
  139. claudeResponse := &dto.ClaudeResponse{Type: "message_start"}
  140. ok := FormatClaudeResponseInfo(claudeResponse, nil, nil)
  141. if ok {
  142. t.Error("expected false for nil claudeInfo")
  143. }
  144. }
  145. func TestFormatClaudeResponseInfo_ContentBlockDelta(t *testing.T) {
  146. text := "hello"
  147. claudeInfo := &ClaudeResponseInfo{
  148. Usage: &dto.Usage{},
  149. ResponseText: strings.Builder{},
  150. }
  151. claudeResponse := &dto.ClaudeResponse{
  152. Type: "content_block_delta",
  153. Delta: &dto.ClaudeMediaMessage{
  154. Text: &text,
  155. },
  156. }
  157. ok := FormatClaudeResponseInfo(claudeResponse, nil, claudeInfo)
  158. if !ok {
  159. t.Fatal("expected true")
  160. }
  161. if claudeInfo.ResponseText.String() != "hello" {
  162. t.Errorf("ResponseText = %q, want %q", claudeInfo.ResponseText.String(), "hello")
  163. }
  164. }
  165. func TestRequestOpenAI2ClaudeMessage_AutoCacheControl(t *testing.T) {
  166. req := dto.GeneralOpenAIRequest{
  167. Model: "claude-sonnet-4-20250514",
  168. Messages: []dto.Message{
  169. {Role: "user", Content: "Hello"},
  170. },
  171. MaxTokens: 1024,
  172. }
  173. claudeReq, err := RequestOpenAI2ClaudeMessage(nil, req)
  174. require.NoError(t, err)
  175. require.NotNil(t, claudeReq)
  176. require.Equal(t, `{"type":"ephemeral"}`, string(claudeReq.CacheControl))
  177. }
  178. func TestRequestOpenAI2ClaudeMessage_SystemStringWithAutoCache(t *testing.T) {
  179. req := dto.GeneralOpenAIRequest{
  180. Model: "claude-sonnet-4-20250514",
  181. Messages: []dto.Message{
  182. {Role: "system", Content: "You are a helpful assistant."},
  183. {Role: "user", Content: "Hello"},
  184. },
  185. MaxTokens: 1024,
  186. }
  187. claudeReq, err := RequestOpenAI2ClaudeMessage(nil, req)
  188. require.NoError(t, err)
  189. require.Equal(t, `{"type":"ephemeral"}`, string(claudeReq.CacheControl))
  190. systemMedia, ok := claudeReq.System.([]dto.ClaudeMediaMessage)
  191. require.True(t, ok, "system should be []ClaudeMediaMessage")
  192. require.Len(t, systemMedia, 1)
  193. require.Equal(t, "You are a helpful assistant.", *systemMedia[0].Text)
  194. }
  195. func TestRequestOpenAI2ClaudeMessage_SystemComplexWithCacheControl(t *testing.T) {
  196. // 通过 JSON 反序列化构造请求,模拟真实场景
  197. jsonReq := `{
  198. "model": "claude-sonnet-4-20250514",
  199. "messages": [
  200. {"role": "system", "content": [{"type": "text", "text": "System instruction", "cache_control": {"type": "ephemeral"}}]},
  201. {"role": "user", "content": "Hello"}
  202. ],
  203. "max_tokens": 1024
  204. }`
  205. var req dto.GeneralOpenAIRequest
  206. require.NoError(t, json.Unmarshal([]byte(jsonReq), &req))
  207. claudeReq, err := RequestOpenAI2ClaudeMessage(nil, req)
  208. require.NoError(t, err)
  209. // system 复合内容从 JSON 反序列化后 Content 是 []any
  210. // ParseContent 会解析 map[string]any,但不会提取 cache_control(已知限制)
  211. // 所以这里验证 system 被正确传递,cache_control 在当前实现中不会透传
  212. require.NotNil(t, claudeReq.System)
  213. // 顶层 cache_control 仍然注入
  214. require.Equal(t, `{"type":"ephemeral"}`, string(claudeReq.CacheControl))
  215. }
  216. func TestRequestOpenAI2ClaudeMessage_UserMessageCacheControlPassthrough(t *testing.T) {
  217. // 通过 SetMediaContent 直接设置(如 OpenRouter 等非标准路径)
  218. msg := dto.Message{Role: "user"}
  219. msg.SetMediaContent([]dto.MediaContent{
  220. {Type: "text", Text: "Hello", CacheControl: json.RawMessage(`{"type":"ephemeral"}`)},
  221. })
  222. req := dto.GeneralOpenAIRequest{
  223. Model: "claude-sonnet-4-20250514",
  224. Messages: []dto.Message{msg},
  225. MaxTokens: 1024,
  226. }
  227. claudeReq, err := RequestOpenAI2ClaudeMessage(nil, req)
  228. require.NoError(t, err)
  229. require.Len(t, claudeReq.Messages, 1)
  230. // content block CacheControl 透传(仅在 Content 为 []MediaContent 时有效)
  231. content, ok := claudeReq.Messages[0].Content.([]dto.ClaudeMediaMessage)
  232. require.True(t, ok)
  233. require.Len(t, content, 1)
  234. require.Equal(t, `{"type":"ephemeral"}`, string(content[0].CacheControl))
  235. }
  236. func TestRequestOpenAI2ClaudeMessage_ToolUseWithAutoCache(t *testing.T) {
  237. req := dto.GeneralOpenAIRequest{
  238. Model: "claude-sonnet-4-20250514",
  239. Messages: []dto.Message{
  240. {Role: "user", Content: "What's the weather?"},
  241. {
  242. Role: "assistant",
  243. Content: "",
  244. ToolCalls: json.RawMessage(`[{"id":"call_1","type":"function","function":{"name":"get_weather","arguments":"{\"city\":\"NYC\"}"}}]`),
  245. },
  246. {
  247. Role: "tool",
  248. Content: `{"temp":72}`,
  249. ToolCallId: "call_1",
  250. },
  251. },
  252. MaxTokens: 1024,
  253. }
  254. claudeReq, err := RequestOpenAI2ClaudeMessage(nil, req)
  255. require.NoError(t, err)
  256. require.Equal(t, `{"type":"ephemeral"}`, string(claudeReq.CacheControl))
  257. // user + assistant(with tool_use) + user(tool_result) = 3 messages
  258. require.Len(t, claudeReq.Messages, 3)
  259. }
  260. func TestRequestOpenAI2ClaudeMessage_MinimalRequestNoPanic(t *testing.T) {
  261. req := dto.GeneralOpenAIRequest{
  262. Model: "claude-sonnet-4-20250514",
  263. Messages: []dto.Message{},
  264. }
  265. claudeReq, err := RequestOpenAI2ClaudeMessage(nil, req)
  266. require.NoError(t, err)
  267. require.NotNil(t, claudeReq)
  268. require.Equal(t, `{"type":"ephemeral"}`, string(claudeReq.CacheControl))
  269. }
  270. func TestRequestOpenAI2ClaudeMessage_FullSerialization(t *testing.T) {
  271. req := dto.GeneralOpenAIRequest{
  272. Model: "claude-sonnet-4-20250514",
  273. Messages: []dto.Message{
  274. {Role: "system", Content: "You are helpful."},
  275. {Role: "user", Content: "Hello"},
  276. },
  277. MaxTokens: 1024,
  278. }
  279. claudeReq, err := RequestOpenAI2ClaudeMessage(nil, req)
  280. require.NoError(t, err)
  281. jsonBytes, err := json.Marshal(claudeReq)
  282. require.NoError(t, err)
  283. jsonStr := string(jsonBytes)
  284. require.Contains(t, jsonStr, `"cache_control":{"type":"ephemeral"}`)
  285. require.Contains(t, jsonStr, `"model":"claude-sonnet-4-20250514"`)
  286. require.Contains(t, jsonStr, `"max_tokens":1024`)
  287. }
  288. func TestClaudeRequest_EmptyCacheControl_Omitted(t *testing.T) {
  289. req := dto.ClaudeRequest{
  290. Model: "claude-sonnet-4-20250514",
  291. MaxTokens: 1024,
  292. }
  293. jsonBytes, err := json.Marshal(req)
  294. require.NoError(t, err)
  295. jsonStr := string(jsonBytes)
  296. require.NotContains(t, jsonStr, "cache_control")
  297. }