openai-native-usage.spec.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. import { describe, it, expect, beforeEach } from "vitest"
  2. import { OpenAiNativeHandler } from "../openai-native"
  3. import { openAiNativeModels } from "@roo-code/types"
  4. describe("OpenAiNativeHandler - normalizeUsage", () => {
  5. let handler: OpenAiNativeHandler
  6. const mockModel = {
  7. id: "gpt-4o",
  8. info: openAiNativeModels["gpt-4o"],
  9. }
  10. beforeEach(() => {
  11. handler = new OpenAiNativeHandler({
  12. openAiNativeApiKey: "test-key",
  13. })
  14. })
  15. describe("detailed token shapes (Responses API)", () => {
  16. it("should handle detailed shapes with cached and miss tokens", () => {
  17. const usage = {
  18. input_tokens: 100,
  19. output_tokens: 50,
  20. input_tokens_details: {
  21. cached_tokens: 30,
  22. cache_miss_tokens: 70,
  23. },
  24. }
  25. const result = (handler as any).normalizeUsage(usage, mockModel)
  26. expect(result).toMatchObject({
  27. type: "usage",
  28. inputTokens: 100,
  29. outputTokens: 50,
  30. cacheReadTokens: 30,
  31. cacheWriteTokens: 0, // miss tokens are NOT cache writes
  32. })
  33. })
  34. it("should derive total input tokens from details when totals are missing", () => {
  35. const usage = {
  36. // No input_tokens or prompt_tokens
  37. output_tokens: 50,
  38. input_tokens_details: {
  39. cached_tokens: 30,
  40. cache_miss_tokens: 70,
  41. },
  42. }
  43. const result = (handler as any).normalizeUsage(usage, mockModel)
  44. expect(result).toMatchObject({
  45. type: "usage",
  46. inputTokens: 100, // Derived from 30 + 70
  47. outputTokens: 50,
  48. cacheReadTokens: 30,
  49. cacheWriteTokens: 0, // miss tokens are NOT cache writes
  50. })
  51. })
  52. it("should handle prompt_tokens_details variant", () => {
  53. const usage = {
  54. prompt_tokens: 100,
  55. completion_tokens: 50,
  56. prompt_tokens_details: {
  57. cached_tokens: 30,
  58. cache_miss_tokens: 70,
  59. },
  60. }
  61. const result = (handler as any).normalizeUsage(usage, mockModel)
  62. expect(result).toMatchObject({
  63. type: "usage",
  64. inputTokens: 100,
  65. outputTokens: 50,
  66. cacheReadTokens: 30,
  67. cacheWriteTokens: 0, // miss tokens are NOT cache writes
  68. })
  69. })
  70. it("should handle cache_creation_input_tokens for actual cache writes", () => {
  71. const usage = {
  72. input_tokens: 100,
  73. output_tokens: 50,
  74. cache_creation_input_tokens: 20,
  75. input_tokens_details: {
  76. cached_tokens: 30,
  77. cache_miss_tokens: 50, // 50 miss + 30 cached + 20 creation = 100 total
  78. },
  79. }
  80. const result = (handler as any).normalizeUsage(usage, mockModel)
  81. expect(result).toMatchObject({
  82. type: "usage",
  83. inputTokens: 100,
  84. outputTokens: 50,
  85. cacheReadTokens: 30,
  86. cacheWriteTokens: 20, // Actual cache writes from cache_creation_input_tokens
  87. })
  88. })
  89. it("should handle reasoning tokens in output details", () => {
  90. const usage = {
  91. input_tokens: 100,
  92. output_tokens: 150,
  93. output_tokens_details: {
  94. reasoning_tokens: 50,
  95. },
  96. }
  97. const result = (handler as any).normalizeUsage(usage, mockModel)
  98. expect(result).toMatchObject({
  99. type: "usage",
  100. inputTokens: 100,
  101. outputTokens: 150,
  102. reasoningTokens: 50,
  103. })
  104. })
  105. })
  106. describe("legacy field names", () => {
  107. it("should handle cache_creation_input_tokens and cache_read_input_tokens", () => {
  108. const usage = {
  109. input_tokens: 100,
  110. output_tokens: 50,
  111. cache_creation_input_tokens: 20,
  112. cache_read_input_tokens: 30,
  113. }
  114. const result = (handler as any).normalizeUsage(usage, mockModel)
  115. expect(result).toMatchObject({
  116. type: "usage",
  117. inputTokens: 100,
  118. outputTokens: 50,
  119. cacheReadTokens: 30,
  120. cacheWriteTokens: 20,
  121. })
  122. })
  123. it("should handle cache_write_tokens and cache_read_tokens", () => {
  124. const usage = {
  125. input_tokens: 100,
  126. output_tokens: 50,
  127. cache_write_tokens: 20,
  128. cache_read_tokens: 30,
  129. }
  130. const result = (handler as any).normalizeUsage(usage, mockModel)
  131. expect(result).toMatchObject({
  132. type: "usage",
  133. inputTokens: 100,
  134. outputTokens: 50,
  135. cacheReadTokens: 30,
  136. cacheWriteTokens: 20,
  137. })
  138. })
  139. it("should handle cached_tokens field", () => {
  140. const usage = {
  141. input_tokens: 100,
  142. output_tokens: 50,
  143. cached_tokens: 30,
  144. }
  145. const result = (handler as any).normalizeUsage(usage, mockModel)
  146. expect(result).toMatchObject({
  147. type: "usage",
  148. inputTokens: 100,
  149. outputTokens: 50,
  150. cacheReadTokens: 30,
  151. })
  152. })
  153. it("should handle prompt_tokens and completion_tokens", () => {
  154. const usage = {
  155. prompt_tokens: 100,
  156. completion_tokens: 50,
  157. }
  158. const result = (handler as any).normalizeUsage(usage, mockModel)
  159. expect(result).toMatchObject({
  160. type: "usage",
  161. inputTokens: 100,
  162. outputTokens: 50,
  163. cacheReadTokens: 0,
  164. cacheWriteTokens: 0,
  165. })
  166. })
  167. })
  168. describe("SSE-only events", () => {
  169. it("should handle SSE events with minimal usage data", () => {
  170. const usage = {
  171. input_tokens: 100,
  172. output_tokens: 50,
  173. }
  174. const result = (handler as any).normalizeUsage(usage, mockModel)
  175. expect(result).toMatchObject({
  176. type: "usage",
  177. inputTokens: 100,
  178. outputTokens: 50,
  179. cacheReadTokens: 0,
  180. cacheWriteTokens: 0,
  181. })
  182. })
  183. it("should handle SSE events with no cache information", () => {
  184. const usage = {
  185. prompt_tokens: 100,
  186. completion_tokens: 50,
  187. }
  188. const result = (handler as any).normalizeUsage(usage, mockModel)
  189. expect(result).toMatchObject({
  190. type: "usage",
  191. inputTokens: 100,
  192. outputTokens: 50,
  193. cacheReadTokens: 0,
  194. cacheWriteTokens: 0,
  195. })
  196. })
  197. })
  198. describe("edge cases", () => {
  199. it("should handle undefined usage", () => {
  200. const result = (handler as any).normalizeUsage(undefined, mockModel)
  201. expect(result).toBeUndefined()
  202. })
  203. it("should handle null usage", () => {
  204. const result = (handler as any).normalizeUsage(null, mockModel)
  205. expect(result).toBeUndefined()
  206. })
  207. it("should handle empty usage object", () => {
  208. const usage = {}
  209. const result = (handler as any).normalizeUsage(usage, mockModel)
  210. expect(result).toMatchObject({
  211. type: "usage",
  212. inputTokens: 0,
  213. outputTokens: 0,
  214. cacheReadTokens: 0,
  215. cacheWriteTokens: 0,
  216. })
  217. })
  218. it("should handle missing details but with cache fields", () => {
  219. const usage = {
  220. input_tokens: 100,
  221. output_tokens: 50,
  222. cache_read_input_tokens: 30,
  223. // No input_tokens_details
  224. }
  225. const result = (handler as any).normalizeUsage(usage, mockModel)
  226. expect(result).toMatchObject({
  227. type: "usage",
  228. inputTokens: 100,
  229. outputTokens: 50,
  230. cacheReadTokens: 30,
  231. cacheWriteTokens: 0,
  232. })
  233. })
  234. it("should use all available cache information with proper fallbacks", () => {
  235. const usage = {
  236. input_tokens: 100,
  237. output_tokens: 50,
  238. cached_tokens: 20, // Legacy field (will be used as fallback)
  239. input_tokens_details: {
  240. cached_tokens: 30, // Detailed shape
  241. cache_miss_tokens: 70,
  242. },
  243. }
  244. const result = (handler as any).normalizeUsage(usage, mockModel)
  245. // The implementation uses nullish coalescing, so it will use the first non-nullish value:
  246. // cache_read_input_tokens ?? cache_read_tokens ?? cached_tokens ?? cachedFromDetails
  247. // Since none of the first two exist, it falls back to cached_tokens (20) before cachedFromDetails
  248. expect(result).toMatchObject({
  249. type: "usage",
  250. inputTokens: 100,
  251. outputTokens: 50,
  252. cacheReadTokens: 20, // From cached_tokens (legacy field comes before details in fallback chain)
  253. cacheWriteTokens: 0, // miss tokens are NOT cache writes
  254. })
  255. })
  256. it("should use detailed shapes when legacy fields are not present", () => {
  257. const usage = {
  258. input_tokens: 100,
  259. output_tokens: 50,
  260. // No cached_tokens legacy field
  261. input_tokens_details: {
  262. cached_tokens: 30,
  263. cache_miss_tokens: 70,
  264. },
  265. }
  266. const result = (handler as any).normalizeUsage(usage, mockModel)
  267. expect(result).toMatchObject({
  268. type: "usage",
  269. inputTokens: 100,
  270. outputTokens: 50,
  271. cacheReadTokens: 30, // From details since no legacy field exists
  272. cacheWriteTokens: 0, // miss tokens are NOT cache writes
  273. })
  274. })
  275. it("should handle totals missing with only partial details", () => {
  276. const usage = {
  277. // No input_tokens or prompt_tokens
  278. output_tokens: 50,
  279. input_tokens_details: {
  280. cached_tokens: 30,
  281. // No cache_miss_tokens
  282. },
  283. }
  284. const result = (handler as any).normalizeUsage(usage, mockModel)
  285. expect(result).toMatchObject({
  286. type: "usage",
  287. inputTokens: 30, // Derived from cached_tokens only
  288. outputTokens: 50,
  289. cacheReadTokens: 30,
  290. cacheWriteTokens: 0,
  291. })
  292. })
  293. })
  294. describe("cost calculation", () => {
  295. it("should pass total input tokens to calculateApiCostOpenAI", () => {
  296. const usage = {
  297. input_tokens: 100,
  298. output_tokens: 50,
  299. cache_read_input_tokens: 30,
  300. cache_creation_input_tokens: 20,
  301. }
  302. const result = (handler as any).normalizeUsage(usage, mockModel)
  303. expect(result).toHaveProperty("totalCost")
  304. expect(result.totalCost).toBeGreaterThan(0)
  305. // calculateApiCostOpenAI handles subtracting cache tokens internally
  306. // It will compute: 100 - 30 - 20 = 50 uncached input tokens
  307. })
  308. it("should handle cost calculation with no cache reads", () => {
  309. const usage = {
  310. input_tokens: 100,
  311. output_tokens: 50,
  312. }
  313. const result = (handler as any).normalizeUsage(usage, mockModel)
  314. expect(result).toHaveProperty("totalCost")
  315. expect(result.totalCost).toBeGreaterThan(0)
  316. // Cost should be calculated with full input tokens since no cache reads
  317. })
  318. })
  319. })