rafcode 2.4.1-0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CLAUDE.md +4 -4
  2. package/RAF/ahwqwq-model-whisperer/decisions.md +22 -0
  3. package/RAF/ahwqwq-model-whisperer/input.md +5 -0
  4. package/RAF/ahwqwq-model-whisperer/outcomes/01-show-model-on-task-line.md +49 -0
  5. package/RAF/ahwqwq-model-whisperer/outcomes/02-use-claude-cost-estimation.md +107 -0
  6. package/RAF/ahwqwq-model-whisperer/outcomes/03-add-plan-resume-flag.md +87 -0
  7. package/RAF/ahwqwq-model-whisperer/plans/01-show-model-on-task-line.md +45 -0
  8. package/RAF/ahwqwq-model-whisperer/plans/02-use-claude-cost-estimation.md +115 -0
  9. package/RAF/ahwqwq-model-whisperer/plans/03-add-plan-resume-flag.md +70 -0
  10. package/RAF/ahwvrz-legacy-sunset/decisions.md +10 -0
  11. package/RAF/ahwvrz-legacy-sunset/input.md +10 -0
  12. package/RAF/ahwvrz-legacy-sunset/outcomes/01-remove-migrate-command.md +30 -0
  13. package/RAF/ahwvrz-legacy-sunset/outcomes/02-fix-resume-worktree-resolution.md +62 -0
  14. package/RAF/ahwvrz-legacy-sunset/plans/01-remove-migrate-command.md +65 -0
  15. package/RAF/ahwvrz-legacy-sunset/plans/02-fix-resume-worktree-resolution.md +72 -0
  16. package/README.md +0 -17
  17. package/dist/commands/do.js +13 -15
  18. package/dist/commands/do.js.map +1 -1
  19. package/dist/commands/plan.d.ts.map +1 -1
  20. package/dist/commands/plan.js +98 -2
  21. package/dist/commands/plan.js.map +1 -1
  22. package/dist/core/claude-runner.d.ts +8 -0
  23. package/dist/core/claude-runner.d.ts.map +1 -1
  24. package/dist/core/claude-runner.js +72 -0
  25. package/dist/core/claude-runner.js.map +1 -1
  26. package/dist/index.js +0 -2
  27. package/dist/index.js.map +1 -1
  28. package/dist/parsers/stream-renderer.d.ts +2 -0
  29. package/dist/parsers/stream-renderer.d.ts.map +1 -1
  30. package/dist/parsers/stream-renderer.js +2 -0
  31. package/dist/parsers/stream-renderer.js.map +1 -1
  32. package/dist/prompts/amend.d.ts.map +1 -1
  33. package/dist/prompts/amend.js +3 -1
  34. package/dist/prompts/amend.js.map +1 -1
  35. package/dist/prompts/planning.d.ts.map +1 -1
  36. package/dist/prompts/planning.js +4 -1
  37. package/dist/prompts/planning.js.map +1 -1
  38. package/dist/types/config.d.ts +4 -28
  39. package/dist/types/config.d.ts.map +1 -1
  40. package/dist/types/config.js +0 -24
  41. package/dist/types/config.js.map +1 -1
  42. package/dist/utils/config.d.ts +1 -26
  43. package/dist/utils/config.d.ts.map +1 -1
  44. package/dist/utils/config.js +2 -98
  45. package/dist/utils/config.js.map +1 -1
  46. package/dist/utils/terminal-symbols.d.ts +7 -16
  47. package/dist/utils/terminal-symbols.d.ts.map +1 -1
  48. package/dist/utils/terminal-symbols.js +16 -42
  49. package/dist/utils/terminal-symbols.js.map +1 -1
  50. package/dist/utils/token-tracker.d.ts +4 -30
  51. package/dist/utils/token-tracker.d.ts.map +1 -1
  52. package/dist/utils/token-tracker.js +17 -98
  53. package/dist/utils/token-tracker.js.map +1 -1
  54. package/package.json +1 -1
  55. package/src/commands/do.ts +14 -15
  56. package/src/commands/plan.ts +112 -1
  57. package/src/core/claude-runner.ts +81 -0
  58. package/src/index.ts +0 -2
  59. package/src/parsers/stream-renderer.ts +4 -0
  60. package/src/prompts/amend.ts +3 -1
  61. package/src/prompts/config-docs.md +1 -72
  62. package/src/prompts/planning.ts +4 -1
  63. package/src/types/config.ts +4 -57
  64. package/src/utils/config.ts +2 -112
  65. package/src/utils/terminal-symbols.ts +16 -46
  66. package/src/utils/token-tracker.ts +19 -113
  67. package/tests/unit/claude-runner.test.ts +1 -0
  68. package/tests/unit/config-command.test.ts +4 -13
  69. package/tests/unit/config.test.ts +6 -148
  70. package/tests/unit/plan-resume-worktree-resolution.test.ts +153 -0
  71. package/tests/unit/stream-renderer.test.ts +82 -0
  72. package/tests/unit/terminal-symbols.test.ts +86 -124
  73. package/tests/unit/token-tracker.test.ts +159 -679
  74. package/src/commands/migrate.ts +0 -269
  75. package/tests/unit/migrate-command.test.ts +0 -197
@@ -1,5 +1,5 @@
1
1
  import { TokenTracker, CostBreakdown, accumulateUsage, sumCostBreakdowns } from '../../src/utils/token-tracker.js';
2
- import { UsageData, PricingConfig, DEFAULT_CONFIG } from '../../src/types/config.js';
2
+ import { UsageData } from '../../src/types/config.js';
3
3
 
4
4
  function makeUsage(overrides: Partial<UsageData> = {}): UsageData {
5
5
  return {
@@ -8,251 +8,185 @@ function makeUsage(overrides: Partial<UsageData> = {}): UsageData {
8
8
  cacheReadInputTokens: 0,
9
9
  cacheCreationInputTokens: 0,
10
10
  modelUsage: {},
11
+ totalCostUsd: 0, // Default to 0 instead of undefined
11
12
  ...overrides,
12
13
  };
13
14
  }
14
15
 
15
- const testPricing: PricingConfig = DEFAULT_CONFIG.pricing;
16
-
17
16
  describe('TokenTracker', () => {
18
- describe('calculateCost', () => {
19
- it('should calculate cost for opus model usage', () => {
20
- const tracker = new TokenTracker(testPricing);
17
+ describe('constructor', () => {
18
+ it('should create tracker without parameters', () => {
19
+ const tracker = new TokenTracker();
20
+ expect(tracker).toBeDefined();
21
+ });
22
+ });
23
+
24
+ describe('addTask with Claude-provided costs', () => {
25
+ it('should sum totalCostUsd from single attempt', () => {
26
+ const tracker = new TokenTracker();
21
27
  const usage = makeUsage({
22
28
  inputTokens: 1_000_000,
23
29
  outputTokens: 500_000,
24
- cacheReadInputTokens: 200_000,
25
- cacheCreationInputTokens: 100_000,
26
- modelUsage: {
27
- 'claude-opus-4-6': {
28
- inputTokens: 1_000_000,
29
- outputTokens: 500_000,
30
- cacheReadInputTokens: 200_000,
31
- cacheCreationInputTokens: 100_000,
32
- },
33
- },
30
+ totalCostUsd: 52.5,
34
31
  });
35
32
 
36
- const cost = tracker.calculateCost(usage);
37
- expect(cost.inputCost).toBeCloseTo(15); // 1M * $15/MTok
38
- expect(cost.outputCost).toBeCloseTo(37.5); // 0.5M * $75/MTok
39
- expect(cost.cacheReadCost).toBeCloseTo(0.3); // 0.2M * $1.5/MTok
40
- expect(cost.cacheCreateCost).toBeCloseTo(1.875); // 0.1M * $18.75/MTok
41
- expect(cost.totalCost).toBeCloseTo(15 + 37.5 + 0.3 + 1.875);
33
+ const entry = tracker.addTask('01', [usage]);
34
+ expect(entry.cost.totalCost).toBe(52.5);
35
+ expect(entry.usage.inputTokens).toBe(1_000_000);
36
+ expect(entry.usage.outputTokens).toBe(500_000);
42
37
  });
43
38
 
44
- it('should calculate cost for sonnet model usage', () => {
45
- const tracker = new TokenTracker(testPricing);
46
- const usage = makeUsage({
39
+ it('should sum totalCostUsd from multiple attempts', () => {
40
+ const tracker = new TokenTracker();
41
+ const attempt1 = makeUsage({
47
42
  inputTokens: 1_000_000,
48
- outputTokens: 1_000_000,
49
- modelUsage: {
50
- 'claude-sonnet-4-5-20250929': {
51
- inputTokens: 1_000_000,
52
- outputTokens: 1_000_000,
53
- cacheReadInputTokens: 0,
54
- cacheCreationInputTokens: 0,
55
- },
56
- },
43
+ outputTokens: 500_000,
44
+ totalCostUsd: 25.0,
45
+ });
46
+ const attempt2 = makeUsage({
47
+ inputTokens: 500_000,
48
+ outputTokens: 250_000,
49
+ totalCostUsd: 12.5,
57
50
  });
58
51
 
59
- const cost = tracker.calculateCost(usage);
60
- expect(cost.inputCost).toBeCloseTo(3); // 1M * $3/MTok
61
- expect(cost.outputCost).toBeCloseTo(15); // 1M * $15/MTok
62
- expect(cost.totalCost).toBeCloseTo(18);
52
+ const entry = tracker.addTask('01', [attempt1, attempt2]);
53
+ expect(entry.cost.totalCost).toBe(37.5);
54
+ expect(entry.usage.inputTokens).toBe(1_500_000);
55
+ expect(entry.usage.outputTokens).toBe(750_000);
63
56
  });
64
57
 
65
- it('should calculate cost for haiku model usage', () => {
66
- const tracker = new TokenTracker(testPricing);
58
+ it('should handle missing totalCostUsd gracefully', () => {
59
+ const tracker = new TokenTracker();
60
+ // makeUsage() now defaults totalCostUsd to 0
67
61
  const usage = makeUsage({
68
- inputTokens: 2_000_000,
69
- outputTokens: 1_000_000,
70
- modelUsage: {
71
- 'claude-haiku-4-5-20251001': {
72
- inputTokens: 2_000_000,
73
- outputTokens: 1_000_000,
74
- cacheReadInputTokens: 0,
75
- cacheCreationInputTokens: 0,
76
- },
77
- },
62
+ inputTokens: 1_000_000,
63
+ outputTokens: 500_000,
64
+ totalCostUsd: 0,
78
65
  });
79
66
 
80
- const cost = tracker.calculateCost(usage);
81
- expect(cost.inputCost).toBeCloseTo(2); // 2M * $1/MTok
82
- expect(cost.outputCost).toBeCloseTo(5); // 1M * $5/MTok
83
- expect(cost.totalCost).toBeCloseTo(7);
67
+ const entry = tracker.addTask('01', [usage]);
68
+ expect(entry.cost.totalCost).toBe(0);
84
69
  });
85
70
 
86
- it('should handle multi-model usage in a single task', () => {
87
- const tracker = new TokenTracker(testPricing);
88
- const usage = makeUsage({
89
- inputTokens: 2_000_000,
90
- outputTokens: 1_500_000,
91
- modelUsage: {
92
- 'claude-opus-4-6': {
93
- inputTokens: 1_000_000,
94
- outputTokens: 500_000,
95
- cacheReadInputTokens: 0,
96
- cacheCreationInputTokens: 0,
97
- },
98
- 'claude-haiku-4-5-20251001': {
99
- inputTokens: 1_000_000,
100
- outputTokens: 1_000_000,
101
- cacheReadInputTokens: 0,
102
- cacheCreationInputTokens: 0,
103
- },
104
- },
71
+ it('should handle mixed attempts with and without costs', () => {
72
+ const tracker = new TokenTracker();
73
+ const attempt1 = makeUsage({
74
+ inputTokens: 1_000_000,
75
+ outputTokens: 500_000,
76
+ totalCostUsd: 25.0,
77
+ });
78
+ const attempt2 = makeUsage({
79
+ inputTokens: 500_000,
80
+ outputTokens: 250_000,
81
+ totalCostUsd: 0, // explicitly 0 means no cost
105
82
  });
106
83
 
107
- const cost = tracker.calculateCost(usage);
108
- // Opus: 1M*$15 + 0.5M*$75 = $15 + $37.5
109
- // Haiku: 1M*$1 + 1M*$5 = $1 + $5
110
- expect(cost.inputCost).toBeCloseTo(16); // 15 + 1
111
- expect(cost.outputCost).toBeCloseTo(42.5); // 37.5 + 5
112
- expect(cost.totalCost).toBeCloseTo(58.5);
84
+ const entry = tracker.addTask('01', [attempt1, attempt2]);
85
+ expect(entry.cost.totalCost).toBe(25.0);
113
86
  });
114
87
 
115
- it('should fallback to sonnet pricing when no model breakdown', () => {
116
- const tracker = new TokenTracker(testPricing);
117
- const usage = makeUsage({
118
- inputTokens: 1_000_000,
119
- outputTokens: 1_000_000,
120
- modelUsage: {},
121
- });
88
+ it('should store attempts array in entry', () => {
89
+ const tracker = new TokenTracker();
90
+ const usage = makeUsage({ inputTokens: 100, totalCostUsd: 0.01 });
91
+ const entry = tracker.addTask('01', [usage]);
122
92
 
123
- const cost = tracker.calculateCost(usage);
124
- expect(cost.inputCost).toBeCloseTo(3); // sonnet fallback
125
- expect(cost.outputCost).toBeCloseTo(15);
126
- expect(cost.totalCost).toBeCloseTo(18);
93
+ expect(entry.attempts).toHaveLength(1);
94
+ expect(entry.attempts[0]).toEqual(usage);
127
95
  });
128
96
 
129
- it('should fallback to sonnet pricing for unknown model families', () => {
130
- const tracker = new TokenTracker(testPricing);
97
+ it('should handle zero cost', () => {
98
+ const tracker = new TokenTracker();
131
99
  const usage = makeUsage({
132
- inputTokens: 1_000_000,
133
- outputTokens: 1_000_000,
134
- modelUsage: {
135
- 'claude-unknown-3-0': {
136
- inputTokens: 1_000_000,
137
- outputTokens: 1_000_000,
138
- cacheReadInputTokens: 0,
139
- cacheCreationInputTokens: 0,
140
- },
141
- },
100
+ inputTokens: 100,
101
+ outputTokens: 50,
102
+ totalCostUsd: 0,
142
103
  });
143
104
 
144
- const cost = tracker.calculateCost(usage);
145
- expect(cost.inputCost).toBeCloseTo(3); // sonnet fallback
146
- expect(cost.outputCost).toBeCloseTo(15);
105
+ const entry = tracker.addTask('01', [usage]);
106
+ expect(entry.cost.totalCost).toBe(0);
147
107
  });
108
+ });
148
109
 
149
- it('should return zero cost for zero tokens', () => {
150
- const tracker = new TokenTracker(testPricing);
151
- const usage = makeUsage();
152
- const cost = tracker.calculateCost(usage);
153
- expect(cost.totalCost).toBe(0);
154
- });
110
+ describe('getTotals', () => {
111
+ it('should accumulate costs across multiple tasks', () => {
112
+ const tracker = new TokenTracker();
155
113
 
156
- it('should apply cache read discount correctly', () => {
157
- const tracker = new TokenTracker(testPricing);
158
- const usage = makeUsage({
159
- cacheReadInputTokens: 1_000_000,
160
- modelUsage: {
161
- 'claude-sonnet-4-5': {
162
- inputTokens: 0,
163
- outputTokens: 0,
164
- cacheReadInputTokens: 1_000_000,
165
- cacheCreationInputTokens: 0,
166
- },
167
- },
168
- });
114
+ tracker.addTask('01', [makeUsage({
115
+ inputTokens: 1_000_000,
116
+ outputTokens: 500_000,
117
+ totalCostUsd: 18.0,
118
+ })]);
169
119
 
170
- const cost = tracker.calculateCost(usage);
171
- // Cache read: 1M * $0.30/MTok = $0.30 (90% off $3 input price)
172
- expect(cost.cacheReadCost).toBeCloseTo(0.3);
173
- expect(cost.totalCost).toBeCloseTo(0.3);
120
+ tracker.addTask('02', [makeUsage({
121
+ inputTokens: 500_000,
122
+ outputTokens: 250_000,
123
+ totalCostUsd: 9.0,
124
+ })]);
125
+
126
+ const totals = tracker.getTotals();
127
+ expect(totals.cost.totalCost).toBe(27.0);
128
+ expect(totals.usage.inputTokens).toBe(1_500_000);
129
+ expect(totals.usage.outputTokens).toBe(750_000);
130
+ });
131
+
132
+ it('should return empty totals when no tasks added', () => {
133
+ const tracker = new TokenTracker();
134
+ const totals = tracker.getTotals();
135
+ expect(totals.usage.inputTokens).toBe(0);
136
+ expect(totals.usage.outputTokens).toBe(0);
137
+ expect(totals.cost.totalCost).toBe(0);
138
+ expect(Object.keys(totals.usage.modelUsage)).toHaveLength(0);
174
139
  });
175
- });
176
140
 
177
- describe('addTask and accumulation', () => {
178
141
  it('should accumulate usage across multiple tasks', () => {
179
- const tracker = new TokenTracker(testPricing);
142
+ const tracker = new TokenTracker();
180
143
 
181
144
  tracker.addTask('01', [makeUsage({
182
145
  inputTokens: 500_000,
183
146
  outputTokens: 200_000,
184
- modelUsage: {
185
- 'claude-opus-4-6': {
186
- inputTokens: 500_000,
187
- outputTokens: 200_000,
188
- cacheReadInputTokens: 0,
189
- cacheCreationInputTokens: 0,
190
- },
191
- },
147
+ totalCostUsd: 10.0,
192
148
  })]);
193
149
 
194
150
  tracker.addTask('02', [makeUsage({
195
151
  inputTokens: 300_000,
196
152
  outputTokens: 100_000,
197
- modelUsage: {
198
- 'claude-opus-4-6': {
199
- inputTokens: 300_000,
200
- outputTokens: 100_000,
201
- cacheReadInputTokens: 0,
202
- cacheCreationInputTokens: 0,
203
- },
204
- },
153
+ totalCostUsd: 5.0,
205
154
  })]);
206
155
 
207
156
  const totals = tracker.getTotals();
208
157
  expect(totals.usage.inputTokens).toBe(800_000);
209
158
  expect(totals.usage.outputTokens).toBe(300_000);
210
- expect(totals.usage.modelUsage['claude-opus-4-6']?.inputTokens).toBe(800_000);
211
- expect(totals.usage.modelUsage['claude-opus-4-6']?.outputTokens).toBe(300_000);
212
159
  });
213
160
 
214
- it('should accumulate costs across multiple tasks', () => {
215
- const tracker = new TokenTracker(testPricing);
161
+ it('should accumulate cache tokens', () => {
162
+ const tracker = new TokenTracker();
216
163
 
217
- const entry1 = tracker.addTask('01', [makeUsage({
218
- inputTokens: 1_000_000,
219
- outputTokens: 1_000_000,
220
- modelUsage: {
221
- 'claude-sonnet-4-5': {
222
- inputTokens: 1_000_000,
223
- outputTokens: 1_000_000,
224
- cacheReadInputTokens: 0,
225
- cacheCreationInputTokens: 0,
226
- },
227
- },
164
+ tracker.addTask('01', [makeUsage({
165
+ inputTokens: 100_000,
166
+ cacheReadInputTokens: 50_000,
167
+ cacheCreationInputTokens: 20_000,
168
+ totalCostUsd: 2.0,
228
169
  })]);
229
170
 
230
- const entry2 = tracker.addTask('02', [makeUsage({
231
- inputTokens: 1_000_000,
232
- outputTokens: 1_000_000,
233
- modelUsage: {
234
- 'claude-sonnet-4-5': {
235
- inputTokens: 1_000_000,
236
- outputTokens: 1_000_000,
237
- cacheReadInputTokens: 0,
238
- cacheCreationInputTokens: 0,
239
- },
240
- },
171
+ tracker.addTask('02', [makeUsage({
172
+ inputTokens: 100_000,
173
+ cacheReadInputTokens: 30_000,
174
+ cacheCreationInputTokens: 10_000,
175
+ totalCostUsd: 1.5,
241
176
  })]);
242
177
 
243
178
  const totals = tracker.getTotals();
244
- // Each task: $3 input + $15 output = $18
245
- expect(entry1.cost.totalCost).toBeCloseTo(18);
246
- expect(entry2.cost.totalCost).toBeCloseTo(18);
247
- expect(totals.cost.totalCost).toBeCloseTo(36);
179
+ expect(totals.usage.cacheReadInputTokens).toBe(80_000);
180
+ expect(totals.usage.cacheCreationInputTokens).toBe(30_000);
248
181
  });
249
182
 
250
183
  it('should accumulate multi-model usage across tasks', () => {
251
- const tracker = new TokenTracker(testPricing);
184
+ const tracker = new TokenTracker();
252
185
 
253
186
  tracker.addTask('01', [makeUsage({
254
187
  inputTokens: 1_000_000,
255
188
  outputTokens: 500_000,
189
+ totalCostUsd: 52.5,
256
190
  modelUsage: {
257
191
  'claude-opus-4-6': {
258
192
  inputTokens: 1_000_000,
@@ -266,6 +200,7 @@ describe('TokenTracker', () => {
266
200
  tracker.addTask('02', [makeUsage({
267
201
  inputTokens: 500_000,
268
202
  outputTokens: 200_000,
203
+ totalCostUsd: 6.0,
269
204
  modelUsage: {
270
205
  'claude-haiku-4-5-20251001': {
271
206
  inputTokens: 500_000,
@@ -280,120 +215,58 @@ describe('TokenTracker', () => {
280
215
  expect(totals.usage.modelUsage['claude-opus-4-6']?.inputTokens).toBe(1_000_000);
281
216
  expect(totals.usage.modelUsage['claude-haiku-4-5-20251001']?.inputTokens).toBe(500_000);
282
217
  });
218
+ });
283
219
 
284
- it('should return empty totals when no tasks added', () => {
285
- const tracker = new TokenTracker(testPricing);
286
- const totals = tracker.getTotals();
287
- expect(totals.usage.inputTokens).toBe(0);
288
- expect(totals.usage.outputTokens).toBe(0);
289
- expect(totals.cost.totalCost).toBe(0);
290
- expect(Object.keys(totals.usage.modelUsage)).toHaveLength(0);
291
- });
292
-
220
+ describe('getEntries', () => {
293
221
  it('should return per-task entries', () => {
294
- const tracker = new TokenTracker(testPricing);
295
- tracker.addTask('01', [makeUsage({ inputTokens: 100 })]);
296
- tracker.addTask('02', [makeUsage({ inputTokens: 200 })]);
222
+ const tracker = new TokenTracker();
223
+ tracker.addTask('01', [makeUsage({ inputTokens: 100, totalCostUsd: 0.01 })]);
224
+ tracker.addTask('02', [makeUsage({ inputTokens: 200, totalCostUsd: 0.02 })]);
297
225
 
298
226
  const entries = tracker.getEntries();
299
227
  expect(entries).toHaveLength(2);
300
228
  expect(entries[0].taskId).toBe('01');
301
229
  expect(entries[1].taskId).toBe('02');
230
+ expect(entries[0].cost.totalCost).toBe(0.01);
231
+ expect(entries[1].cost.totalCost).toBe(0.02);
302
232
  });
233
+ });
303
234
 
304
- it('addTask returns the entry with cost', () => {
305
- const tracker = new TokenTracker(testPricing);
306
- const entry = tracker.addTask('01', [makeUsage({
307
- inputTokens: 1_000_000,
308
- modelUsage: {
309
- 'claude-opus-4-6': {
310
- inputTokens: 1_000_000,
311
- outputTokens: 0,
312
- cacheReadInputTokens: 0,
313
- cacheCreationInputTokens: 0,
314
- },
315
- },
316
- })]);
317
-
318
- expect(entry.taskId).toBe('01');
319
- expect(entry.cost.inputCost).toBeCloseTo(15);
320
- expect(entry.cost.totalCost).toBeCloseTo(15);
321
- });
322
-
323
- it('should store attempts array in entry', () => {
324
- const tracker = new TokenTracker(testPricing);
325
- const usage = makeUsage({ inputTokens: 100 });
326
- const entry = tracker.addTask('01', [usage]);
327
-
328
- expect(entry.attempts).toHaveLength(1);
329
- expect(entry.attempts[0]).toEqual(usage);
330
- });
331
-
332
- it('should accumulate multiple attempts for a single task', () => {
333
- const tracker = new TokenTracker(testPricing);
235
+ describe('multi-attempt cost calculation', () => {
236
+ it('should handle retry with different costs', () => {
237
+ const tracker = new TokenTracker();
334
238
  const attempt1 = makeUsage({
335
- inputTokens: 500_000,
336
- outputTokens: 100_000,
337
- modelUsage: {
338
- 'claude-opus-4-6': {
339
- inputTokens: 500_000,
340
- outputTokens: 100_000,
341
- cacheReadInputTokens: 0,
342
- cacheCreationInputTokens: 0,
343
- },
344
- },
239
+ inputTokens: 1_000_000,
240
+ outputTokens: 500_000,
241
+ totalCostUsd: 52.5,
345
242
  });
346
243
  const attempt2 = makeUsage({
347
- inputTokens: 600_000,
348
- outputTokens: 200_000,
349
- modelUsage: {
350
- 'claude-opus-4-6': {
351
- inputTokens: 600_000,
352
- outputTokens: 200_000,
353
- cacheReadInputTokens: 0,
354
- cacheCreationInputTokens: 0,
355
- },
356
- },
244
+ inputTokens: 1_000_000,
245
+ outputTokens: 1_000_000,
246
+ totalCostUsd: 18.0,
357
247
  });
358
248
 
359
249
  const entry = tracker.addTask('01', [attempt1, attempt2]);
360
-
361
- expect(entry.usage.inputTokens).toBe(1_100_000);
362
- expect(entry.usage.outputTokens).toBe(300_000);
363
- expect(entry.usage.modelUsage['claude-opus-4-6']?.inputTokens).toBe(1_100_000);
364
- expect(entry.attempts).toHaveLength(2);
250
+ expect(entry.cost.totalCost).toBe(70.5);
365
251
  });
366
252
 
367
- it('should correctly accumulate multi-attempt costs', () => {
368
- const tracker = new TokenTracker(testPricing);
369
- const attempt1 = makeUsage({
370
- inputTokens: 1_000_000,
371
- modelUsage: {
372
- 'claude-sonnet-4-5': {
373
- inputTokens: 1_000_000,
374
- outputTokens: 0,
375
- cacheReadInputTokens: 0,
376
- cacheCreationInputTokens: 0,
377
- },
378
- },
379
- });
380
- const attempt2 = makeUsage({
381
- inputTokens: 1_000_000,
382
- modelUsage: {
383
- 'claude-sonnet-4-5': {
384
- inputTokens: 1_000_000,
385
- outputTokens: 0,
386
- cacheReadInputTokens: 0,
387
- cacheCreationInputTokens: 0,
388
- },
389
- },
390
- });
253
+ it('should include all attempt usage in grand totals', () => {
254
+ const tracker = new TokenTracker();
391
255
 
392
- const entry = tracker.addTask('01', [attempt1, attempt2]);
256
+ // Task 1: 2 attempts
257
+ tracker.addTask('01', [
258
+ makeUsage({ inputTokens: 500_000, totalCostUsd: 10.0 }),
259
+ makeUsage({ inputTokens: 500_000, totalCostUsd: 10.0 }),
260
+ ]);
261
+
262
+ // Task 2: 1 attempt
263
+ tracker.addTask('02', [
264
+ makeUsage({ inputTokens: 1_000_000, totalCostUsd: 20.0 }),
265
+ ]);
393
266
 
394
- // 2M tokens * $3/MTok = $6
395
- expect(entry.cost.inputCost).toBeCloseTo(6);
396
- expect(entry.cost.totalCost).toBeCloseTo(6);
267
+ const totals = tracker.getTotals();
268
+ expect(totals.usage.inputTokens).toBe(2_000_000);
269
+ expect(totals.cost.totalCost).toBe(40.0);
397
270
  });
398
271
  });
399
272
 
@@ -413,6 +286,7 @@ describe('TokenTracker', () => {
413
286
  outputTokens: 200,
414
287
  cacheReadInputTokens: 50,
415
288
  cacheCreationInputTokens: 25,
289
+ totalCostUsd: 1.5,
416
290
  modelUsage: {
417
291
  'claude-opus-4-6': {
418
292
  inputTokens: 100,
@@ -428,6 +302,7 @@ describe('TokenTracker', () => {
428
302
  expect(result.outputTokens).toBe(200);
429
303
  expect(result.cacheReadInputTokens).toBe(50);
430
304
  expect(result.cacheCreationInputTokens).toBe(25);
305
+ expect(result.totalCostUsd).toBe(1.5);
431
306
  expect(result.modelUsage['claude-opus-4-6']?.inputTokens).toBe(100);
432
307
  });
433
308
 
@@ -437,12 +312,14 @@ describe('TokenTracker', () => {
437
312
  outputTokens: 50,
438
313
  cacheReadInputTokens: 10,
439
314
  cacheCreationInputTokens: 5,
315
+ totalCostUsd: 0.5,
440
316
  });
441
317
  const attempt2 = makeUsage({
442
318
  inputTokens: 200,
443
319
  outputTokens: 100,
444
320
  cacheReadInputTokens: 20,
445
321
  cacheCreationInputTokens: 10,
322
+ totalCostUsd: 1.0,
446
323
  });
447
324
 
448
325
  const result = accumulateUsage([attempt1, attempt2]);
@@ -450,6 +327,7 @@ describe('TokenTracker', () => {
450
327
  expect(result.outputTokens).toBe(150);
451
328
  expect(result.cacheReadInputTokens).toBe(30);
452
329
  expect(result.cacheCreationInputTokens).toBe(15);
330
+ expect(result.totalCostUsd).toBe(1.5);
453
331
  });
454
332
 
455
333
  it('should merge modelUsage for same model across attempts', () => {
@@ -515,55 +393,6 @@ describe('TokenTracker', () => {
515
393
  expect(Object.keys(result.modelUsage)).toHaveLength(2);
516
394
  });
517
395
 
518
- it('should handle mixed model usage across attempts', () => {
519
- const attempt1 = makeUsage({
520
- inputTokens: 300,
521
- outputTokens: 150,
522
- modelUsage: {
523
- 'claude-opus-4-6': {
524
- inputTokens: 200,
525
- outputTokens: 100,
526
- cacheReadInputTokens: 0,
527
- cacheCreationInputTokens: 0,
528
- },
529
- 'claude-haiku-4-5': {
530
- inputTokens: 100,
531
- outputTokens: 50,
532
- cacheReadInputTokens: 0,
533
- cacheCreationInputTokens: 0,
534
- },
535
- },
536
- });
537
- const attempt2 = makeUsage({
538
- inputTokens: 400,
539
- outputTokens: 200,
540
- modelUsage: {
541
- 'claude-opus-4-6': {
542
- inputTokens: 100,
543
- outputTokens: 50,
544
- cacheReadInputTokens: 0,
545
- cacheCreationInputTokens: 0,
546
- },
547
- 'claude-sonnet-4-5': {
548
- inputTokens: 300,
549
- outputTokens: 150,
550
- cacheReadInputTokens: 0,
551
- cacheCreationInputTokens: 0,
552
- },
553
- },
554
- });
555
-
556
- const result = accumulateUsage([attempt1, attempt2]);
557
- expect(result.inputTokens).toBe(700);
558
- expect(result.outputTokens).toBe(350);
559
- // Opus: 200 + 100 = 300
560
- expect(result.modelUsage['claude-opus-4-6']?.inputTokens).toBe(300);
561
- // Haiku: only from attempt1
562
- expect(result.modelUsage['claude-haiku-4-5']?.inputTokens).toBe(100);
563
- // Sonnet: only from attempt2
564
- expect(result.modelUsage['claude-sonnet-4-5']?.inputTokens).toBe(300);
565
- });
566
-
567
396
  it('should not mutate input objects', () => {
568
397
  const attempt1 = makeUsage({
569
398
  inputTokens: 100,
@@ -594,202 +423,31 @@ describe('TokenTracker', () => {
594
423
  expect(attempt1.modelUsage['claude-opus-4-6']?.inputTokens).toBe(100);
595
424
  expect(attempt2.inputTokens).toBe(200);
596
425
  });
597
- });
598
426
 
599
- describe('multi-attempt cost calculation', () => {
600
- it('should calculate correct cost when retry uses different model', () => {
601
- const tracker = new TokenTracker(testPricing);
602
- // Attempt 1: Opus, Attempt 2: Sonnet (fallback)
427
+ it('should handle zero totalCostUsd in some attempts', () => {
603
428
  const attempt1 = makeUsage({
604
- inputTokens: 1_000_000,
605
- outputTokens: 500_000,
606
- modelUsage: {
607
- 'claude-opus-4-6': {
608
- inputTokens: 1_000_000,
609
- outputTokens: 500_000,
610
- cacheReadInputTokens: 0,
611
- cacheCreationInputTokens: 0,
612
- },
613
- },
614
- });
615
- const attempt2 = makeUsage({
616
- inputTokens: 1_000_000,
617
- outputTokens: 1_000_000,
618
- modelUsage: {
619
- 'claude-sonnet-4-5': {
620
- inputTokens: 1_000_000,
621
- outputTokens: 1_000_000,
622
- cacheReadInputTokens: 0,
623
- cacheCreationInputTokens: 0,
624
- },
625
- },
626
- });
627
-
628
- const entry = tracker.addTask('01', [attempt1, attempt2]);
629
-
630
- // Opus: 1M*$15 + 0.5M*$75 = $15 + $37.5 = $52.5
631
- // Sonnet: 1M*$3 + 1M*$15 = $3 + $15 = $18
632
- // Total: $52.5 + $18 = $70.5
633
- expect(entry.cost.inputCost).toBeCloseTo(18); // 15 + 3
634
- expect(entry.cost.outputCost).toBeCloseTo(52.5); // 37.5 + 15
635
- expect(entry.cost.totalCost).toBeCloseTo(70.5);
636
- });
637
-
638
- it('should include all attempt usage in grand totals', () => {
639
- const tracker = new TokenTracker(testPricing);
640
-
641
- // Task 1: 2 attempts
642
- tracker.addTask('01', [
643
- makeUsage({ inputTokens: 500_000 }),
644
- makeUsage({ inputTokens: 500_000 }),
645
- ]);
646
-
647
- // Task 2: 1 attempt
648
- tracker.addTask('02', [
649
- makeUsage({ inputTokens: 1_000_000 }),
650
- ]);
651
-
652
- const totals = tracker.getTotals();
653
- expect(totals.usage.inputTokens).toBe(2_000_000);
654
- });
655
- });
656
-
657
- describe('mixed-attempt cost calculation (aggregate + modelUsage)', () => {
658
- it('should correctly price attempts with mixed modelUsage presence', () => {
659
- const tracker = new TokenTracker(testPricing);
660
- // Attempt 1: has modelUsage (opus)
661
- const attempt1 = makeUsage({
662
- inputTokens: 1_000_000,
663
- outputTokens: 500_000,
664
- modelUsage: {
665
- 'claude-opus-4-6': {
666
- inputTokens: 1_000_000,
667
- outputTokens: 500_000,
668
- cacheReadInputTokens: 0,
669
- cacheCreationInputTokens: 0,
670
- },
671
- },
672
- });
673
- // Attempt 2: NO modelUsage (aggregate-only, should use sonnet fallback)
674
- const attempt2 = makeUsage({
675
- inputTokens: 1_000_000,
676
- outputTokens: 1_000_000,
677
- modelUsage: {}, // Empty - should fallback to sonnet pricing
678
- });
679
-
680
- const entry = tracker.addTask('01', [attempt1, attempt2]);
681
-
682
- // Attempt 1 (Opus): 1M*$15 + 0.5M*$75 = $15 + $37.5 = $52.5
683
- // Attempt 2 (Sonnet fallback): 1M*$3 + 1M*$15 = $3 + $15 = $18
684
- // Total: $52.5 + $18 = $70.5
685
- expect(entry.cost.inputCost).toBeCloseTo(18); // 15 + 3
686
- expect(entry.cost.outputCost).toBeCloseTo(52.5); // 37.5 + 15
687
- expect(entry.cost.totalCost).toBeCloseTo(70.5);
688
- });
689
-
690
- it('should not underreport cost when first attempt has no modelUsage', () => {
691
- const tracker = new TokenTracker(testPricing);
692
- // Attempt 1: aggregate-only (no modelUsage)
693
- const attempt1 = makeUsage({
694
- inputTokens: 1_000_000,
695
- outputTokens: 1_000_000,
696
- modelUsage: {},
697
- });
698
- // Attempt 2: has modelUsage
699
- const attempt2 = makeUsage({
700
- inputTokens: 1_000_000,
701
- outputTokens: 500_000,
702
- modelUsage: {
703
- 'claude-opus-4-6': {
704
- inputTokens: 1_000_000,
705
- outputTokens: 500_000,
706
- cacheReadInputTokens: 0,
707
- cacheCreationInputTokens: 0,
708
- },
709
- },
710
- });
711
-
712
- const entry = tracker.addTask('01', [attempt1, attempt2]);
713
-
714
- // Attempt 1 (Sonnet fallback): 1M*$3 + 1M*$15 = $18
715
- // Attempt 2 (Opus): 1M*$15 + 0.5M*$75 = $52.5
716
- // Total: $18 + $52.5 = $70.5
717
- expect(entry.cost.totalCost).toBeCloseTo(70.5);
718
- });
719
-
720
- it('should handle all aggregate-only attempts', () => {
721
- const tracker = new TokenTracker(testPricing);
722
- const attempt1 = makeUsage({
723
- inputTokens: 1_000_000,
724
- outputTokens: 1_000_000,
725
- modelUsage: {},
726
- });
727
- const attempt2 = makeUsage({
728
- inputTokens: 1_000_000,
729
- outputTokens: 1_000_000,
730
- modelUsage: {},
731
- });
732
-
733
- const entry = tracker.addTask('01', [attempt1, attempt2]);
734
-
735
- // Both use sonnet fallback: 2 * (1M*$3 + 1M*$15) = 2 * $18 = $36
736
- expect(entry.cost.totalCost).toBeCloseTo(36);
737
- });
738
-
739
- it('should include cache costs from aggregate-only attempts', () => {
740
- const tracker = new TokenTracker(testPricing);
741
- // Attempt 1: has modelUsage with cache
742
- const attempt1 = makeUsage({
743
- inputTokens: 500_000,
744
- outputTokens: 200_000,
745
- cacheReadInputTokens: 100_000,
746
- cacheCreationInputTokens: 50_000,
747
- modelUsage: {
748
- 'claude-opus-4-6': {
749
- inputTokens: 500_000,
750
- outputTokens: 200_000,
751
- cacheReadInputTokens: 100_000,
752
- cacheCreationInputTokens: 50_000,
753
- },
754
- },
429
+ inputTokens: 100,
430
+ totalCostUsd: 0.5,
755
431
  });
756
- // Attempt 2: aggregate-only with cache
757
432
  const attempt2 = makeUsage({
758
- inputTokens: 500_000,
759
- outputTokens: 200_000,
760
- cacheReadInputTokens: 100_000,
761
- cacheCreationInputTokens: 50_000,
762
- modelUsage: {},
433
+ inputTokens: 200,
434
+ totalCostUsd: 0, // explicitly 0
763
435
  });
764
436
 
765
- const entry = tracker.addTask('01', [attempt1, attempt2]);
766
-
767
- // Opus cache rates: $1.5/MTok read, $18.75/MTok create
768
- // Sonnet cache rates: $0.30/MTok read, $3.75/MTok create
769
- // Attempt 1 cache: 0.1M*$1.5 + 0.05M*$18.75 = $0.15 + $0.9375 = $1.0875
770
- // Attempt 2 cache: 0.1M*$0.30 + 0.05M*$3.75 = $0.03 + $0.1875 = $0.2175
771
- // Total cache: $1.0875 + $0.2175 = $1.305
772
- expect(entry.cost.cacheReadCost).toBeCloseTo(0.15 + 0.03);
773
- expect(entry.cost.cacheCreateCost).toBeCloseTo(0.9375 + 0.1875);
437
+ const result = accumulateUsage([attempt1, attempt2]);
438
+ expect(result.inputTokens).toBe(300);
439
+ expect(result.totalCostUsd).toBe(0.5);
774
440
  });
775
441
  });
776
442
 
777
443
  describe('sumCostBreakdowns', () => {
778
444
  it('should return zero breakdown for empty array', () => {
779
445
  const result = sumCostBreakdowns([]);
780
- expect(result.inputCost).toBe(0);
781
- expect(result.outputCost).toBe(0);
782
- expect(result.cacheReadCost).toBe(0);
783
- expect(result.cacheCreateCost).toBe(0);
784
446
  expect(result.totalCost).toBe(0);
785
447
  });
786
448
 
787
449
  it('should return same breakdown for single element', () => {
788
450
  const cost: CostBreakdown = {
789
- inputCost: 10,
790
- outputCost: 20,
791
- cacheReadCost: 1,
792
- cacheCreateCost: 2,
793
451
  totalCost: 33,
794
452
  };
795
453
  const result = sumCostBreakdowns([cost]);
@@ -798,191 +456,13 @@ describe('TokenTracker', () => {
798
456
 
799
457
  it('should sum all cost fields across breakdowns', () => {
800
458
  const cost1: CostBreakdown = {
801
- inputCost: 10,
802
- outputCost: 20,
803
- cacheReadCost: 1,
804
- cacheCreateCost: 2,
805
459
  totalCost: 33,
806
460
  };
807
461
  const cost2: CostBreakdown = {
808
- inputCost: 5,
809
- outputCost: 10,
810
- cacheReadCost: 0.5,
811
- cacheCreateCost: 1,
812
462
  totalCost: 16.5,
813
463
  };
814
464
  const result = sumCostBreakdowns([cost1, cost2]);
815
- expect(result.inputCost).toBe(15);
816
- expect(result.outputCost).toBe(30);
817
- expect(result.cacheReadCost).toBe(1.5);
818
- expect(result.cacheCreateCost).toBe(3);
819
465
  expect(result.totalCost).toBe(49.5);
820
466
  });
821
467
  });
822
-
823
- describe('custom pricing', () => {
824
- it('should use custom pricing config', () => {
825
- const customPricing: PricingConfig = {
826
- opus: { inputPerMTok: 10, outputPerMTok: 50, cacheReadPerMTok: 1, cacheCreatePerMTok: 12.5 },
827
- sonnet: { inputPerMTok: 2, outputPerMTok: 10, cacheReadPerMTok: 0.2, cacheCreatePerMTok: 2.5 },
828
- haiku: { inputPerMTok: 0.5, outputPerMTok: 2.5, cacheReadPerMTok: 0.05, cacheCreatePerMTok: 0.625 },
829
- };
830
-
831
- const tracker = new TokenTracker(customPricing);
832
- const usage = makeUsage({
833
- inputTokens: 1_000_000,
834
- outputTokens: 1_000_000,
835
- modelUsage: {
836
- 'claude-opus-4-6': {
837
- inputTokens: 1_000_000,
838
- outputTokens: 1_000_000,
839
- cacheReadInputTokens: 0,
840
- cacheCreationInputTokens: 0,
841
- },
842
- },
843
- });
844
-
845
- const cost = tracker.calculateCost(usage);
846
- expect(cost.inputCost).toBeCloseTo(10); // 1M * $10/MTok
847
- expect(cost.outputCost).toBeCloseTo(50); // 1M * $50/MTok
848
- expect(cost.totalCost).toBeCloseTo(60);
849
- });
850
- });
851
-
852
- describe('rate limit estimation', () => {
853
- it('should calculate rate limit percentage from cost', () => {
854
- const tracker = new TokenTracker(testPricing);
855
- // With default sonnet pricing ($3 input, $15 output), avg = $9/MTok
856
- // Sonnet-equivalent tokens = cost / (9/1M) = cost * 1M/9
857
- // Percentage = sonnetEquivTokens / cap * 100
858
-
859
- // Test with $0.18 cost (should be ~2222 Sonnet-equiv tokens)
860
- // With cap of 88000, that's ~2.5%
861
- const percentage = tracker.calculateRateLimitPercentage(0.18, 88000);
862
- // $0.18 / ($9/1M) = 20000 Sonnet-equiv tokens
863
- // 20000 / 88000 * 100 = ~22.7%
864
- expect(percentage).toBeCloseTo(22.73, 1);
865
- });
866
-
867
- it('should return 0 for zero cost', () => {
868
- const tracker = new TokenTracker(testPricing);
869
- expect(tracker.calculateRateLimitPercentage(0, 88000)).toBe(0);
870
- });
871
-
872
- it('should respect custom sonnetTokenCap', () => {
873
- const tracker = new TokenTracker(testPricing);
874
- const percentageDefault = tracker.calculateRateLimitPercentage(0.09, 88000);
875
- const percentageHigherCap = tracker.calculateRateLimitPercentage(0.09, 176000);
876
- // Higher cap should halve the percentage
877
- expect(percentageHigherCap).toBeCloseTo(percentageDefault / 2, 1);
878
- });
879
-
880
- it('should calculate cumulative rate limit across tasks', () => {
881
- const tracker = new TokenTracker(testPricing);
882
-
883
- // Add a task with sonnet usage: 1M in / 1M out = $3 + $15 = $18
884
- tracker.addTask('01', [makeUsage({
885
- inputTokens: 1_000_000,
886
- outputTokens: 1_000_000,
887
- modelUsage: {
888
- 'claude-sonnet-4-5': {
889
- inputTokens: 1_000_000,
890
- outputTokens: 1_000_000,
891
- cacheReadInputTokens: 0,
892
- cacheCreationInputTokens: 0,
893
- },
894
- },
895
- })]);
896
-
897
- const percentage = tracker.getCumulativeRateLimitPercentage(88000);
898
- // $18 / ($9/1M) = 2,000,000 Sonnet-equiv tokens
899
- // 2,000,000 / 88,000 * 100 = ~2272.7%
900
- expect(percentage).toBeCloseTo(2272.73, 0);
901
- });
902
-
903
- it('should correctly weight Opus usage higher than Sonnet', () => {
904
- const tracker = new TokenTracker(testPricing);
905
-
906
- // Opus task: 1M in / 1M out = $15 + $75 = $90
907
- tracker.addTask('01', [makeUsage({
908
- inputTokens: 1_000_000,
909
- outputTokens: 1_000_000,
910
- modelUsage: {
911
- 'claude-opus-4-6': {
912
- inputTokens: 1_000_000,
913
- outputTokens: 1_000_000,
914
- cacheReadInputTokens: 0,
915
- cacheCreationInputTokens: 0,
916
- },
917
- },
918
- })]);
919
-
920
- const opusPercentage = tracker.getCumulativeRateLimitPercentage(88000);
921
-
922
- // Sonnet equivalent of $90 = $90 / ($9/1M) = 10,000,000 tokens
923
- // 10,000,000 / 88,000 * 100 = ~11363.6%
924
- expect(opusPercentage).toBeCloseTo(11363.6, 0);
925
- });
926
-
927
- it('should correctly weight Haiku usage lower than Sonnet', () => {
928
- const tracker = new TokenTracker(testPricing);
929
-
930
- // Haiku task: 1M in / 1M out = $1 + $5 = $6
931
- tracker.addTask('01', [makeUsage({
932
- inputTokens: 1_000_000,
933
- outputTokens: 1_000_000,
934
- modelUsage: {
935
- 'claude-haiku-4-5': {
936
- inputTokens: 1_000_000,
937
- outputTokens: 1_000_000,
938
- cacheReadInputTokens: 0,
939
- cacheCreationInputTokens: 0,
940
- },
941
- },
942
- })]);
943
-
944
- const haikuPercentage = tracker.getCumulativeRateLimitPercentage(88000);
945
-
946
- // Sonnet equivalent of $6 = $6 / ($9/1M) = ~666,667 tokens
947
- // 666,667 / 88,000 * 100 = ~757.6%
948
- expect(haikuPercentage).toBeCloseTo(757.6, 0);
949
- });
950
-
951
- it('should handle multi-model tasks correctly for rate limit', () => {
952
- const tracker = new TokenTracker(testPricing);
953
-
954
- // Mixed task: Opus attempt ($52.5) + Sonnet attempt ($18) = $70.5
955
- const attempt1 = makeUsage({
956
- inputTokens: 1_000_000,
957
- outputTokens: 500_000,
958
- modelUsage: {
959
- 'claude-opus-4-6': {
960
- inputTokens: 1_000_000,
961
- outputTokens: 500_000,
962
- cacheReadInputTokens: 0,
963
- cacheCreationInputTokens: 0,
964
- },
965
- },
966
- });
967
- const attempt2 = makeUsage({
968
- inputTokens: 1_000_000,
969
- outputTokens: 1_000_000,
970
- modelUsage: {
971
- 'claude-sonnet-4-5': {
972
- inputTokens: 1_000_000,
973
- outputTokens: 1_000_000,
974
- cacheReadInputTokens: 0,
975
- cacheCreationInputTokens: 0,
976
- },
977
- },
978
- });
979
-
980
- tracker.addTask('01', [attempt1, attempt2]);
981
- const percentage = tracker.getCumulativeRateLimitPercentage(88000);
982
-
983
- // $70.5 / ($9/1M) = 7,833,333 Sonnet-equiv tokens
984
- // 7,833,333 / 88,000 * 100 = ~8901.5%
985
- expect(percentage).toBeCloseTo(8901.5, 0);
986
- });
987
- });
988
468
  });