compaction.test.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. import { describe, expect, test } from "bun:test"
  2. import path from "path"
  3. import { SessionCompaction } from "../../src/session/compaction"
  4. import { Token } from "../../src/util/token"
  5. import { Instance } from "../../src/project/instance"
  6. import { Log } from "../../src/util/log"
  7. import { tmpdir } from "../fixture/fixture"
  8. import { Session } from "../../src/session"
  9. import type { Provider } from "../../src/provider/provider"
  10. Log.init({ print: false })
  11. function createModel(opts: { context: number; output: number; cost?: Provider.Model["cost"] }): Provider.Model {
  12. return {
  13. id: "test-model",
  14. providerID: "test",
  15. name: "Test",
  16. limit: {
  17. context: opts.context,
  18. output: opts.output,
  19. },
  20. cost: opts.cost ?? { input: 0, output: 0, cache: { read: 0, write: 0 } },
  21. capabilities: {
  22. toolcall: true,
  23. attachment: false,
  24. reasoning: false,
  25. temperature: true,
  26. input: { text: true, image: false, audio: false, video: false },
  27. output: { text: true, image: false, audio: false, video: false },
  28. },
  29. api: { npm: "@ai-sdk/anthropic" },
  30. options: {},
  31. } as Provider.Model
  32. }
  33. describe("session.compaction.isOverflow", () => {
  34. test("returns true when token count exceeds usable context", async () => {
  35. await using tmp = await tmpdir()
  36. await Instance.provide({
  37. directory: tmp.path,
  38. fn: async () => {
  39. const model = createModel({ context: 100_000, output: 32_000 })
  40. const tokens = { input: 75_000, output: 5_000, reasoning: 0, cache: { read: 0, write: 0 } }
  41. expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(true)
  42. },
  43. })
  44. })
  45. test("returns false when token count within usable context", async () => {
  46. await using tmp = await tmpdir()
  47. await Instance.provide({
  48. directory: tmp.path,
  49. fn: async () => {
  50. const model = createModel({ context: 200_000, output: 32_000 })
  51. const tokens = { input: 100_000, output: 10_000, reasoning: 0, cache: { read: 0, write: 0 } }
  52. expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(false)
  53. },
  54. })
  55. })
  56. test("includes cache.read in token count", async () => {
  57. await using tmp = await tmpdir()
  58. await Instance.provide({
  59. directory: tmp.path,
  60. fn: async () => {
  61. const model = createModel({ context: 100_000, output: 32_000 })
  62. const tokens = { input: 50_000, output: 10_000, reasoning: 0, cache: { read: 10_000, write: 0 } }
  63. expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(true)
  64. },
  65. })
  66. })
  67. test("returns false when model context limit is 0", async () => {
  68. await using tmp = await tmpdir()
  69. await Instance.provide({
  70. directory: tmp.path,
  71. fn: async () => {
  72. const model = createModel({ context: 0, output: 32_000 })
  73. const tokens = { input: 100_000, output: 10_000, reasoning: 0, cache: { read: 0, write: 0 } }
  74. expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(false)
  75. },
  76. })
  77. })
  78. test("returns false when compaction.auto is disabled", async () => {
  79. await using tmp = await tmpdir({
  80. init: async (dir) => {
  81. await Bun.write(
  82. path.join(dir, "opencode.json"),
  83. JSON.stringify({
  84. compaction: { auto: false },
  85. }),
  86. )
  87. },
  88. })
  89. await Instance.provide({
  90. directory: tmp.path,
  91. fn: async () => {
  92. const model = createModel({ context: 100_000, output: 32_000 })
  93. const tokens = { input: 75_000, output: 5_000, reasoning: 0, cache: { read: 0, write: 0 } }
  94. expect(await SessionCompaction.isOverflow({ tokens, model })).toBe(false)
  95. },
  96. })
  97. })
  98. })
  99. describe("util.token.estimate", () => {
  100. test("estimates tokens from text (4 chars per token)", () => {
  101. const text = "x".repeat(4000)
  102. expect(Token.estimate(text)).toBe(1000)
  103. })
  104. test("estimates tokens from larger text", () => {
  105. const text = "y".repeat(20_000)
  106. expect(Token.estimate(text)).toBe(5000)
  107. })
  108. test("returns 0 for empty string", () => {
  109. expect(Token.estimate("")).toBe(0)
  110. })
  111. })
  112. describe("session.getUsage", () => {
  113. test("normalizes standard usage to token format", () => {
  114. const model = createModel({ context: 100_000, output: 32_000 })
  115. const result = Session.getUsage({
  116. model,
  117. usage: {
  118. inputTokens: 1000,
  119. outputTokens: 500,
  120. totalTokens: 1500,
  121. },
  122. })
  123. expect(result.tokens.input).toBe(1000)
  124. expect(result.tokens.output).toBe(500)
  125. expect(result.tokens.reasoning).toBe(0)
  126. expect(result.tokens.cache.read).toBe(0)
  127. expect(result.tokens.cache.write).toBe(0)
  128. })
  129. test("extracts cached tokens to cache.read", () => {
  130. const model = createModel({ context: 100_000, output: 32_000 })
  131. const result = Session.getUsage({
  132. model,
  133. usage: {
  134. inputTokens: 1000,
  135. outputTokens: 500,
  136. totalTokens: 1500,
  137. cachedInputTokens: 200,
  138. },
  139. })
  140. expect(result.tokens.input).toBe(800)
  141. expect(result.tokens.cache.read).toBe(200)
  142. })
  143. test("handles anthropic cache write metadata", () => {
  144. const model = createModel({ context: 100_000, output: 32_000 })
  145. const result = Session.getUsage({
  146. model,
  147. usage: {
  148. inputTokens: 1000,
  149. outputTokens: 500,
  150. totalTokens: 1500,
  151. },
  152. metadata: {
  153. anthropic: {
  154. cacheCreationInputTokens: 300,
  155. },
  156. },
  157. })
  158. expect(result.tokens.cache.write).toBe(300)
  159. })
  160. test("does not subtract cached tokens for anthropic provider", () => {
  161. const model = createModel({ context: 100_000, output: 32_000 })
  162. const result = Session.getUsage({
  163. model,
  164. usage: {
  165. inputTokens: 1000,
  166. outputTokens: 500,
  167. totalTokens: 1500,
  168. cachedInputTokens: 200,
  169. },
  170. metadata: {
  171. anthropic: {},
  172. },
  173. })
  174. expect(result.tokens.input).toBe(1000)
  175. expect(result.tokens.cache.read).toBe(200)
  176. })
  177. test("handles reasoning tokens", () => {
  178. const model = createModel({ context: 100_000, output: 32_000 })
  179. const result = Session.getUsage({
  180. model,
  181. usage: {
  182. inputTokens: 1000,
  183. outputTokens: 500,
  184. totalTokens: 1500,
  185. reasoningTokens: 100,
  186. },
  187. })
  188. expect(result.tokens.reasoning).toBe(100)
  189. })
  190. test("handles undefined optional values gracefully", () => {
  191. const model = createModel({ context: 100_000, output: 32_000 })
  192. const result = Session.getUsage({
  193. model,
  194. usage: {
  195. inputTokens: 0,
  196. outputTokens: 0,
  197. totalTokens: 0,
  198. },
  199. })
  200. expect(result.tokens.input).toBe(0)
  201. expect(result.tokens.output).toBe(0)
  202. expect(result.tokens.reasoning).toBe(0)
  203. expect(result.tokens.cache.read).toBe(0)
  204. expect(result.tokens.cache.write).toBe(0)
  205. expect(Number.isNaN(result.cost)).toBe(false)
  206. })
  207. test("calculates cost correctly", () => {
  208. const model = createModel({
  209. context: 100_000,
  210. output: 32_000,
  211. cost: {
  212. input: 3,
  213. output: 15,
  214. cache: { read: 0.3, write: 3.75 },
  215. },
  216. })
  217. const result = Session.getUsage({
  218. model,
  219. usage: {
  220. inputTokens: 1_000_000,
  221. outputTokens: 100_000,
  222. totalTokens: 1_100_000,
  223. },
  224. })
  225. expect(result.cost).toBe(3 + 1.5)
  226. })
  227. })