@moxxy/sdk 0.14.2 → 0.14.3
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.
- package/dist/mode/collect-stream.d.ts +68 -0
- package/dist/mode/collect-stream.d.ts.map +1 -0
- package/dist/mode/collect-stream.js +181 -0
- package/dist/mode/collect-stream.js.map +1 -0
- package/dist/mode/project-messages.d.ts +84 -0
- package/dist/mode/project-messages.d.ts.map +1 -0
- package/dist/mode/project-messages.js +392 -0
- package/dist/mode/project-messages.js.map +1 -0
- package/dist/mode/single-shot.d.ts +17 -0
- package/dist/mode/single-shot.d.ts.map +1 -0
- package/dist/mode/single-shot.js +53 -0
- package/dist/mode/single-shot.js.map +1 -0
- package/dist/mode/stable-hash.d.ts +7 -0
- package/dist/mode/stable-hash.d.ts.map +1 -0
- package/dist/mode/stable-hash.js +20 -0
- package/dist/mode/stable-hash.js.map +1 -0
- package/dist/mode/stuck-loop.d.ts +39 -0
- package/dist/mode/stuck-loop.d.ts.map +1 -0
- package/dist/mode/stuck-loop.js +51 -0
- package/dist/mode/stuck-loop.js.map +1 -0
- package/dist/mode-helpers.d.ts +15 -175
- package/dist/mode-helpers.d.ts.map +1 -1
- package/dist/mode-helpers.js +14 -666
- package/dist/mode-helpers.js.map +1 -1
- package/package.json +1 -1
- package/src/mode/collect-stream.ts +247 -0
- package/src/mode/project-messages.test.ts +121 -0
- package/src/mode/project-messages.ts +461 -0
- package/src/mode/single-shot.ts +63 -0
- package/src/mode/stable-hash.ts +20 -0
- package/src/mode/stuck-loop.ts +89 -0
- package/src/mode-helpers.ts +32 -850
- package/src/mode.test.ts +20 -0
- package/src/token-accounting.test.ts +90 -0
package/src/mode.test.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { migrateModeName } from './mode.js';
|
|
3
|
+
|
|
4
|
+
describe('migrateModeName', () => {
|
|
5
|
+
it('maps every legacy mode name to its current target', () => {
|
|
6
|
+
expect(migrateModeName('tool-use')).toBe('default');
|
|
7
|
+
expect(migrateModeName('deep-research')).toBe('research');
|
|
8
|
+
expect(migrateModeName('plan-execute')).toBe('default');
|
|
9
|
+
expect(migrateModeName('bmad')).toBe('default');
|
|
10
|
+
expect(migrateModeName('developer')).toBe('default');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('passes a current/unknown name through unchanged (identity)', () => {
|
|
14
|
+
expect(migrateModeName('default')).toBe('default');
|
|
15
|
+
expect(migrateModeName('goal')).toBe('goal');
|
|
16
|
+
expect(migrateModeName('research')).toBe('research');
|
|
17
|
+
expect(migrateModeName('some-future-mode')).toBe('some-future-mode');
|
|
18
|
+
expect(migrateModeName('')).toBe('');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
asEventId,
|
|
5
5
|
asSessionId,
|
|
6
6
|
asTurnId,
|
|
7
|
+
summarizeSessionTokensFromEvents,
|
|
7
8
|
summarizeTokensByModel,
|
|
8
9
|
type ModelUsageTotals,
|
|
9
10
|
type MoxxyEvent,
|
|
@@ -78,6 +79,95 @@ describe('summarizeTokensByModel', () => {
|
|
|
78
79
|
});
|
|
79
80
|
});
|
|
80
81
|
|
|
82
|
+
describe('summarizeSessionTokensFromEvents — cost fold', () => {
|
|
83
|
+
it('counts only responses that reported usage', () => {
|
|
84
|
+
const s = summarizeSessionTokensFromEvents([
|
|
85
|
+
resp(0, {}), // no token fields → not counted
|
|
86
|
+
resp(1, { inputTokens: 100, outputTokens: 10 }),
|
|
87
|
+
resp(2, { cacheReadTokens: 50 }),
|
|
88
|
+
]);
|
|
89
|
+
expect(s.calls).toBe(2);
|
|
90
|
+
expect(s.totalInput).toBe(100);
|
|
91
|
+
expect(s.totalCacheRead).toBe(50);
|
|
92
|
+
expect(s.totalOutput).toBe(10);
|
|
93
|
+
expect(s.totalPrompt).toBe(150);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('applies the read 0.1x / write 1.25x billing multipliers', () => {
|
|
97
|
+
const s = summarizeSessionTokensFromEvents([
|
|
98
|
+
resp(0, { inputTokens: 100, cacheReadTokens: 1000, cacheCreationTokens: 200 }),
|
|
99
|
+
]);
|
|
100
|
+
// totalPrompt = 100 + 1000 + 200 = 1300 (== uncachedInputEq)
|
|
101
|
+
expect(s.totalPrompt).toBe(1300);
|
|
102
|
+
expect(s.uncachedInputEq).toBe(1300);
|
|
103
|
+
// billedInputEq = 100 + 1000*0.1 + 200*1.25 = 100 + 100 + 250 = 450
|
|
104
|
+
expect(s.billedInputEq).toBeCloseTo(450, 6);
|
|
105
|
+
expect(s.cacheHitRate).toBeCloseTo(1000 / 1300, 6);
|
|
106
|
+
expect(s.savedRatio).toBeCloseTo(1 - 450 / 1300, 6);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('guards divide-by-zero when no prompt tokens were reported', () => {
|
|
110
|
+
const s = summarizeSessionTokensFromEvents([resp(0, { outputTokens: 5 })]);
|
|
111
|
+
expect(s.totalPrompt).toBe(0);
|
|
112
|
+
expect(s.cacheHitRate).toBe(0);
|
|
113
|
+
expect(s.savedRatio).toBe(0);
|
|
114
|
+
// No writes → cache considered effective (deliberately-off cache never alarms).
|
|
115
|
+
expect(s.cacheEffective).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('reports cacheEffective=true for a healthy cache (high read ratio)', () => {
|
|
119
|
+
const events = Array.from({ length: 6 }, (_, i) =>
|
|
120
|
+
resp(i, { inputTokens: 10, cacheReadTokens: 1000, cacheCreationTokens: 100 }),
|
|
121
|
+
);
|
|
122
|
+
const s = summarizeSessionTokensFromEvents(events);
|
|
123
|
+
expect(s.calls).toBe(6);
|
|
124
|
+
expect(s.cacheHitRate).toBeGreaterThan(0.05);
|
|
125
|
+
expect(s.savedRatio).toBeGreaterThan(0);
|
|
126
|
+
expect(s.cacheEffective).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('trips cacheEffective=false on broken cache: >=5 calls, writes>0, near-zero reads', () => {
|
|
130
|
+
// 5 calls each writing cache but never reading it → cacheHitRate 0.
|
|
131
|
+
const events = Array.from({ length: 5 }, (_, i) =>
|
|
132
|
+
resp(i, { inputTokens: 100, cacheCreationTokens: 500, cacheReadTokens: 0 }),
|
|
133
|
+
);
|
|
134
|
+
const s = summarizeSessionTokensFromEvents(events);
|
|
135
|
+
expect(s.calls).toBe(5);
|
|
136
|
+
expect(s.totalCacheCreation).toBeGreaterThan(0);
|
|
137
|
+
expect(s.cacheHitRate).toBe(0);
|
|
138
|
+
expect(s.cacheEffective).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('exempts the broken-cache trip below 5 calls (boundary at calls===4 vs 5)', () => {
|
|
142
|
+
const make = (n: number) =>
|
|
143
|
+
summarizeSessionTokensFromEvents(
|
|
144
|
+
Array.from({ length: n }, (_, i) =>
|
|
145
|
+
resp(i, { inputTokens: 100, cacheCreationTokens: 500, cacheReadTokens: 0 }),
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
expect(make(4).cacheEffective).toBe(true); // < 5 → exempt
|
|
149
|
+
expect(make(5).cacheEffective).toBe(false); // >= 5 → trips
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('exempts when there are no cache writes even with zero reads over many calls', () => {
|
|
153
|
+
const events = Array.from({ length: 8 }, (_, i) =>
|
|
154
|
+
resp(i, { inputTokens: 100, cacheCreationTokens: 0, cacheReadTokens: 0 }),
|
|
155
|
+
);
|
|
156
|
+
expect(summarizeSessionTokensFromEvents(events).cacheEffective).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('does not trip when cacheHitRate sits exactly at the 0.05 threshold', () => {
|
|
160
|
+
// hitRate === 0.05 is NOT < 0.05, so the cache stays effective.
|
|
161
|
+
// read/(input+read+write) = 125 / 2500 = 0.05.
|
|
162
|
+
const events = Array.from({ length: 5 }, (_, i) =>
|
|
163
|
+
resp(i, { inputTokens: 1875, cacheReadTokens: 125, cacheCreationTokens: 500 }),
|
|
164
|
+
);
|
|
165
|
+
const s = summarizeSessionTokensFromEvents(events);
|
|
166
|
+
expect(s.cacheHitRate).toBeCloseTo(0.05, 6);
|
|
167
|
+
expect(s.cacheEffective).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
81
171
|
describe('addModelTotals', () => {
|
|
82
172
|
it('adds field-wise', () => {
|
|
83
173
|
const a: ModelUsageTotals = { calls: 1, inputTokens: 10, outputTokens: 2, cacheReadTokens: 3, cacheCreationTokens: 4 };
|