gateway_cached_tokens_test.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. package service
  2. import (
  3. "encoding/json"
  4. "testing"
  5. "github.com/stretchr/testify/assert"
  6. "github.com/stretchr/testify/require"
  7. "github.com/tidwall/gjson"
  8. "github.com/tidwall/sjson"
  9. )
  10. // ---------- reconcileCachedTokens 单元测试 ----------
  11. func TestReconcileCachedTokens_NilUsage(t *testing.T) {
  12. assert.False(t, reconcileCachedTokens(nil))
  13. }
  14. func TestReconcileCachedTokens_AlreadyHasCacheRead(t *testing.T) {
  15. // 已有标准字段,不应覆盖
  16. usage := map[string]any{
  17. "cache_read_input_tokens": float64(100),
  18. "cached_tokens": float64(50),
  19. }
  20. assert.False(t, reconcileCachedTokens(usage))
  21. assert.Equal(t, float64(100), usage["cache_read_input_tokens"])
  22. }
  23. func TestReconcileCachedTokens_KimiStyle(t *testing.T) {
  24. // Kimi 风格:cache_read_input_tokens=0,cached_tokens>0
  25. usage := map[string]any{
  26. "input_tokens": float64(23),
  27. "cache_creation_input_tokens": float64(0),
  28. "cache_read_input_tokens": float64(0),
  29. "cached_tokens": float64(23),
  30. }
  31. assert.True(t, reconcileCachedTokens(usage))
  32. assert.Equal(t, float64(23), usage["cache_read_input_tokens"])
  33. }
  34. func TestReconcileCachedTokens_NoCachedTokens(t *testing.T) {
  35. // 无 cached_tokens 字段(原生 Claude)
  36. usage := map[string]any{
  37. "input_tokens": float64(100),
  38. "cache_read_input_tokens": float64(0),
  39. "cache_creation_input_tokens": float64(0),
  40. }
  41. assert.False(t, reconcileCachedTokens(usage))
  42. assert.Equal(t, float64(0), usage["cache_read_input_tokens"])
  43. }
  44. func TestReconcileCachedTokens_CachedTokensZero(t *testing.T) {
  45. // cached_tokens 为 0,不应覆盖
  46. usage := map[string]any{
  47. "cache_read_input_tokens": float64(0),
  48. "cached_tokens": float64(0),
  49. }
  50. assert.False(t, reconcileCachedTokens(usage))
  51. assert.Equal(t, float64(0), usage["cache_read_input_tokens"])
  52. }
  53. func TestReconcileCachedTokens_MissingCacheReadField(t *testing.T) {
  54. // cache_read_input_tokens 字段完全不存在,cached_tokens > 0
  55. usage := map[string]any{
  56. "cached_tokens": float64(42),
  57. }
  58. assert.True(t, reconcileCachedTokens(usage))
  59. assert.Equal(t, float64(42), usage["cache_read_input_tokens"])
  60. }
  61. // ---------- 流式 message_start 事件 reconcile 测试 ----------
  62. func TestStreamingReconcile_MessageStart(t *testing.T) {
  63. // 模拟 Kimi 返回的 message_start SSE 事件
  64. eventJSON := `{
  65. "type": "message_start",
  66. "message": {
  67. "id": "msg_123",
  68. "type": "message",
  69. "role": "assistant",
  70. "model": "kimi",
  71. "usage": {
  72. "input_tokens": 23,
  73. "cache_creation_input_tokens": 0,
  74. "cache_read_input_tokens": 0,
  75. "cached_tokens": 23
  76. }
  77. }
  78. }`
  79. var event map[string]any
  80. require.NoError(t, json.Unmarshal([]byte(eventJSON), &event))
  81. eventType, _ := event["type"].(string)
  82. require.Equal(t, "message_start", eventType)
  83. // 模拟 processSSEEvent 中的 reconcile 逻辑
  84. if msg, ok := event["message"].(map[string]any); ok {
  85. if u, ok := msg["usage"].(map[string]any); ok {
  86. reconcileCachedTokens(u)
  87. }
  88. }
  89. // 验证 cache_read_input_tokens 已被填充
  90. msg, ok := event["message"].(map[string]any)
  91. require.True(t, ok)
  92. usage, ok := msg["usage"].(map[string]any)
  93. require.True(t, ok)
  94. assert.Equal(t, float64(23), usage["cache_read_input_tokens"])
  95. // 验证重新序列化后 JSON 也包含正确值
  96. data, err := json.Marshal(event)
  97. require.NoError(t, err)
  98. assert.Equal(t, int64(23), gjson.GetBytes(data, "message.usage.cache_read_input_tokens").Int())
  99. }
  100. func TestStreamingReconcile_MessageStart_NativeClaude(t *testing.T) {
  101. // 原生 Claude 不返回 cached_tokens,reconcile 不应改变任何值
  102. eventJSON := `{
  103. "type": "message_start",
  104. "message": {
  105. "usage": {
  106. "input_tokens": 100,
  107. "cache_creation_input_tokens": 50,
  108. "cache_read_input_tokens": 30
  109. }
  110. }
  111. }`
  112. var event map[string]any
  113. require.NoError(t, json.Unmarshal([]byte(eventJSON), &event))
  114. if msg, ok := event["message"].(map[string]any); ok {
  115. if u, ok := msg["usage"].(map[string]any); ok {
  116. reconcileCachedTokens(u)
  117. }
  118. }
  119. msg, ok := event["message"].(map[string]any)
  120. require.True(t, ok)
  121. usage, ok := msg["usage"].(map[string]any)
  122. require.True(t, ok)
  123. assert.Equal(t, float64(30), usage["cache_read_input_tokens"])
  124. }
  125. // ---------- 流式 message_delta 事件 reconcile 测试 ----------
  126. func TestStreamingReconcile_MessageDelta(t *testing.T) {
  127. // 模拟 Kimi 返回的 message_delta SSE 事件
  128. eventJSON := `{
  129. "type": "message_delta",
  130. "usage": {
  131. "output_tokens": 7,
  132. "cache_read_input_tokens": 0,
  133. "cached_tokens": 15
  134. }
  135. }`
  136. var event map[string]any
  137. require.NoError(t, json.Unmarshal([]byte(eventJSON), &event))
  138. eventType, _ := event["type"].(string)
  139. require.Equal(t, "message_delta", eventType)
  140. // 模拟 processSSEEvent 中的 reconcile 逻辑
  141. usage, ok := event["usage"].(map[string]any)
  142. require.True(t, ok)
  143. reconcileCachedTokens(usage)
  144. assert.Equal(t, float64(15), usage["cache_read_input_tokens"])
  145. }
  146. func TestStreamingReconcile_MessageDelta_NativeClaude(t *testing.T) {
  147. // 原生 Claude 的 message_delta 通常没有 cached_tokens
  148. eventJSON := `{
  149. "type": "message_delta",
  150. "usage": {
  151. "output_tokens": 50
  152. }
  153. }`
  154. var event map[string]any
  155. require.NoError(t, json.Unmarshal([]byte(eventJSON), &event))
  156. usage, ok := event["usage"].(map[string]any)
  157. require.True(t, ok)
  158. reconcileCachedTokens(usage)
  159. _, hasCacheRead := usage["cache_read_input_tokens"]
  160. assert.False(t, hasCacheRead, "不应为原生 Claude 响应注入 cache_read_input_tokens")
  161. }
  162. // ---------- 非流式响应 reconcile 测试 ----------
  163. func TestNonStreamingReconcile_KimiResponse(t *testing.T) {
  164. // 模拟 Kimi 非流式响应
  165. body := []byte(`{
  166. "id": "msg_123",
  167. "type": "message",
  168. "role": "assistant",
  169. "content": [{"type": "text", "text": "hello"}],
  170. "model": "kimi",
  171. "usage": {
  172. "input_tokens": 23,
  173. "output_tokens": 7,
  174. "cache_creation_input_tokens": 0,
  175. "cache_read_input_tokens": 0,
  176. "cached_tokens": 23,
  177. "prompt_tokens": 23,
  178. "completion_tokens": 7
  179. }
  180. }`)
  181. // 模拟 handleNonStreamingResponse 中的逻辑
  182. var response struct {
  183. Usage ClaudeUsage `json:"usage"`
  184. }
  185. require.NoError(t, json.Unmarshal(body, &response))
  186. // reconcile
  187. if response.Usage.CacheReadInputTokens == 0 {
  188. cachedTokens := gjson.GetBytes(body, "usage.cached_tokens").Int()
  189. if cachedTokens > 0 {
  190. response.Usage.CacheReadInputTokens = int(cachedTokens)
  191. if newBody, err := sjson.SetBytes(body, "usage.cache_read_input_tokens", cachedTokens); err == nil {
  192. body = newBody
  193. }
  194. }
  195. }
  196. // 验证内部 usage(计费用)
  197. assert.Equal(t, 23, response.Usage.CacheReadInputTokens)
  198. assert.Equal(t, 23, response.Usage.InputTokens)
  199. assert.Equal(t, 7, response.Usage.OutputTokens)
  200. // 验证返回给客户端的 JSON body
  201. assert.Equal(t, int64(23), gjson.GetBytes(body, "usage.cache_read_input_tokens").Int())
  202. }
  203. func TestNonStreamingReconcile_NativeClaude(t *testing.T) {
  204. // 原生 Claude 响应:cache_read_input_tokens 已有值
  205. body := []byte(`{
  206. "usage": {
  207. "input_tokens": 100,
  208. "output_tokens": 50,
  209. "cache_creation_input_tokens": 20,
  210. "cache_read_input_tokens": 30
  211. }
  212. }`)
  213. var response struct {
  214. Usage ClaudeUsage `json:"usage"`
  215. }
  216. require.NoError(t, json.Unmarshal(body, &response))
  217. // CacheReadInputTokens == 30,条件不成立,整个 reconcile 分支不会执行
  218. assert.NotZero(t, response.Usage.CacheReadInputTokens)
  219. assert.Equal(t, 30, response.Usage.CacheReadInputTokens)
  220. }
  221. func TestNonStreamingReconcile_NoCachedTokens(t *testing.T) {
  222. // 没有 cached_tokens 字段
  223. body := []byte(`{
  224. "usage": {
  225. "input_tokens": 100,
  226. "output_tokens": 50,
  227. "cache_creation_input_tokens": 0,
  228. "cache_read_input_tokens": 0
  229. }
  230. }`)
  231. var response struct {
  232. Usage ClaudeUsage `json:"usage"`
  233. }
  234. require.NoError(t, json.Unmarshal(body, &response))
  235. if response.Usage.CacheReadInputTokens == 0 {
  236. cachedTokens := gjson.GetBytes(body, "usage.cached_tokens").Int()
  237. if cachedTokens > 0 {
  238. response.Usage.CacheReadInputTokens = int(cachedTokens)
  239. if newBody, err := sjson.SetBytes(body, "usage.cache_read_input_tokens", cachedTokens); err == nil {
  240. body = newBody
  241. }
  242. }
  243. }
  244. // cache_read_input_tokens 应保持为 0
  245. assert.Equal(t, 0, response.Usage.CacheReadInputTokens)
  246. assert.Equal(t, int64(0), gjson.GetBytes(body, "usage.cache_read_input_tokens").Int())
  247. }