anthropic-usage-parsing.test.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import { describe, expect, test } from "vitest";
  2. import { parseUsageFromResponseText } from "@/app/v1/_lib/proxy/response-handler";
  3. function buildSse(events: Array<{ event: string; data: unknown }>): string {
  4. return events
  5. .flatMap(({ event, data }) => [`event: ${event}`, `data: ${JSON.stringify(data)}`, ""])
  6. .join("\n");
  7. }
  8. describe("parseUsageFromResponseText (Anthropic/Claude SSE usage)", () => {
  9. test("prefers message_delta and falls back to message_start for missing fields", () => {
  10. const sse = buildSse([
  11. {
  12. event: "message_start",
  13. data: {
  14. type: "message_start",
  15. message: {
  16. usage: {
  17. input_tokens: 12,
  18. cache_creation_input_tokens: 1641,
  19. cache_read_input_tokens: 171876,
  20. output_tokens: 1,
  21. cache_creation: { ephemeral_1h_input_tokens: 1641 },
  22. },
  23. },
  24. },
  25. },
  26. {
  27. event: "message_delta",
  28. data: {
  29. type: "message_delta",
  30. delta: { stop_reason: "end_turn" },
  31. usage: {
  32. input_tokens: 9,
  33. cache_creation_input_tokens: 458843,
  34. cache_read_input_tokens: 14999,
  35. output_tokens: 2273,
  36. },
  37. },
  38. },
  39. ]);
  40. const { usageMetrics, usageRecord } = parseUsageFromResponseText(sse, "anthropic");
  41. expect(usageRecord).not.toBeNull();
  42. expect(usageMetrics).toMatchObject({
  43. input_tokens: 9,
  44. output_tokens: 2273,
  45. cache_creation_input_tokens: 458843,
  46. cache_read_input_tokens: 14999,
  47. cache_creation_1h_input_tokens: 1641,
  48. cache_ttl: "1h",
  49. });
  50. });
  51. test("falls back to message_start when message_delta only provides output_tokens", () => {
  52. const sse = buildSse([
  53. {
  54. event: "message_start",
  55. data: {
  56. type: "message_start",
  57. message: {
  58. usage: {
  59. input_tokens: 12,
  60. cache_creation_input_tokens: 1641,
  61. cache_read_input_tokens: 171876,
  62. output_tokens: 1,
  63. cache_creation: { ephemeral_1h_input_tokens: 1641 },
  64. },
  65. },
  66. },
  67. },
  68. {
  69. event: "message_delta",
  70. data: {
  71. type: "message_delta",
  72. delta: { stop_reason: "end_turn" },
  73. usage: { output_tokens: 2273 },
  74. },
  75. },
  76. ]);
  77. const { usageMetrics, usageRecord } = parseUsageFromResponseText(sse, "anthropic");
  78. expect(usageRecord).not.toBeNull();
  79. expect(usageMetrics).toMatchObject({
  80. input_tokens: 12,
  81. output_tokens: 2273,
  82. cache_creation_input_tokens: 1641,
  83. cache_read_input_tokens: 171876,
  84. cache_creation_1h_input_tokens: 1641,
  85. cache_ttl: "1h",
  86. });
  87. });
  88. test("handles message_delta-only streams", () => {
  89. const sse = buildSse([
  90. {
  91. event: "message_delta",
  92. data: {
  93. type: "message_delta",
  94. delta: { stop_reason: "end_turn" },
  95. usage: {
  96. input_tokens: 9,
  97. cache_creation_input_tokens: 458843,
  98. cache_read_input_tokens: 14999,
  99. output_tokens: 2273,
  100. },
  101. },
  102. },
  103. ]);
  104. const { usageMetrics, usageRecord } = parseUsageFromResponseText(sse, "anthropic");
  105. expect(usageRecord).not.toBeNull();
  106. expect(usageMetrics).toMatchObject({
  107. input_tokens: 9,
  108. output_tokens: 2273,
  109. cache_creation_input_tokens: 458843,
  110. cache_read_input_tokens: 14999,
  111. });
  112. });
  113. });