@hazeljs/ai 0.2.0-beta.55 → 0.2.0-beta.56
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/ai-enhanced.service.test.d.ts +2 -0
- package/dist/ai-enhanced.service.test.d.ts.map +1 -0
- package/dist/ai-enhanced.service.test.js +501 -0
- package/dist/context/context.manager.test.d.ts +2 -0
- package/dist/context/context.manager.test.d.ts.map +1 -0
- package/dist/context/context.manager.test.js +180 -0
- package/dist/providers/anthropic.provider.test.d.ts +2 -0
- package/dist/providers/anthropic.provider.test.d.ts.map +1 -0
- package/dist/providers/anthropic.provider.test.js +222 -0
- package/dist/providers/cohere.provider.test.d.ts +2 -0
- package/dist/providers/cohere.provider.test.d.ts.map +1 -0
- package/dist/providers/cohere.provider.test.js +267 -0
- package/dist/providers/gemini.provider.test.d.ts +2 -0
- package/dist/providers/gemini.provider.test.d.ts.map +1 -0
- package/dist/providers/gemini.provider.test.js +219 -0
- package/dist/providers/ollama.provider.test.d.ts +2 -0
- package/dist/providers/ollama.provider.test.d.ts.map +1 -0
- package/dist/providers/ollama.provider.test.js +267 -0
- package/dist/providers/openai.provider.test.d.ts +2 -0
- package/dist/providers/openai.provider.test.d.ts.map +1 -0
- package/dist/providers/openai.provider.test.js +364 -0
- package/dist/tracking/token.tracker.test.d.ts +2 -0
- package/dist/tracking/token.tracker.test.d.ts.map +1 -0
- package/dist/tracking/token.tracker.test.js +272 -0
- package/package.json +2 -2
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
jest.mock('@hazeljs/core', () => ({
|
|
4
|
+
__esModule: true,
|
|
5
|
+
Service: () => () => undefined,
|
|
6
|
+
default: { info: jest.fn(), debug: jest.fn(), warn: jest.fn(), error: jest.fn() },
|
|
7
|
+
}));
|
|
8
|
+
const token_tracker_1 = require("./token.tracker");
|
|
9
|
+
const NOW = Date.now();
|
|
10
|
+
function makeUsage(overrides = {}) {
|
|
11
|
+
return {
|
|
12
|
+
promptTokens: 100,
|
|
13
|
+
completionTokens: 50,
|
|
14
|
+
totalTokens: 150,
|
|
15
|
+
timestamp: NOW,
|
|
16
|
+
...overrides,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
describe('TokenTracker', () => {
|
|
20
|
+
let tracker;
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
tracker = new token_tracker_1.TokenTracker();
|
|
23
|
+
});
|
|
24
|
+
describe('constructor', () => {
|
|
25
|
+
it('creates with defaults', () => {
|
|
26
|
+
expect(tracker).toBeDefined();
|
|
27
|
+
});
|
|
28
|
+
it('accepts custom config', () => {
|
|
29
|
+
const t = new token_tracker_1.TokenTracker({
|
|
30
|
+
maxTokensPerRequest: 1000,
|
|
31
|
+
maxTokensPerDay: 5000,
|
|
32
|
+
maxTokensPerMonth: 50000,
|
|
33
|
+
});
|
|
34
|
+
expect(t).toBeDefined();
|
|
35
|
+
});
|
|
36
|
+
it('uses partial config with defaults for missing fields', () => {
|
|
37
|
+
const t = new token_tracker_1.TokenTracker({ maxTokensPerRequest: 500 });
|
|
38
|
+
expect(t).toBeDefined();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe('track()', () => {
|
|
42
|
+
it('tracks global usage', () => {
|
|
43
|
+
tracker.track(makeUsage());
|
|
44
|
+
const exported = tracker.exportData();
|
|
45
|
+
expect(exported).toHaveLength(1);
|
|
46
|
+
expect(exported[0].totalTokens).toBe(150);
|
|
47
|
+
});
|
|
48
|
+
it('tracks per-user usage when userId provided', () => {
|
|
49
|
+
tracker.track(makeUsage({ userId: 'user1' }));
|
|
50
|
+
const userData = tracker.exportData('user1');
|
|
51
|
+
expect(userData).toHaveLength(1);
|
|
52
|
+
});
|
|
53
|
+
it('appends to existing user history', () => {
|
|
54
|
+
tracker.track(makeUsage({ userId: 'user1' }));
|
|
55
|
+
tracker.track(makeUsage({ totalTokens: 200, userId: 'user1' }));
|
|
56
|
+
expect(tracker.exportData('user1')).toHaveLength(2);
|
|
57
|
+
});
|
|
58
|
+
it('calculates cost when model is provided and cost is missing', () => {
|
|
59
|
+
tracker.track({ promptTokens: 1000, completionTokens: 500, totalTokens: 1500, timestamp: NOW }, 'gpt-4-turbo-preview');
|
|
60
|
+
const exported = tracker.exportData();
|
|
61
|
+
expect(exported[0].cost).toBeGreaterThan(0);
|
|
62
|
+
});
|
|
63
|
+
it('does not recalculate if cost is already set', () => {
|
|
64
|
+
tracker.track(makeUsage({ cost: 0.999 }), 'gpt-4');
|
|
65
|
+
expect(tracker.exportData()[0].cost).toBe(0.999);
|
|
66
|
+
});
|
|
67
|
+
it('does not set cost when no model provided and cost is missing', () => {
|
|
68
|
+
tracker.track(makeUsage());
|
|
69
|
+
expect(tracker.exportData()[0].cost).toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
describe('checkLimits()', () => {
|
|
73
|
+
it('allows request within all limits', async () => {
|
|
74
|
+
const result = await tracker.checkLimits('user1', 100);
|
|
75
|
+
expect(result.allowed).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
it('blocks request exceeding per-request limit', async () => {
|
|
78
|
+
const t = new token_tracker_1.TokenTracker({ maxTokensPerRequest: 10 });
|
|
79
|
+
const result = await t.checkLimits(undefined, 100);
|
|
80
|
+
expect(result.allowed).toBe(false);
|
|
81
|
+
expect(result.reason).toContain('Request exceeds token limit');
|
|
82
|
+
});
|
|
83
|
+
it('returns allowed when no userId and requestTokens within limit', async () => {
|
|
84
|
+
const result = await tracker.checkLimits(undefined, 50);
|
|
85
|
+
expect(result.allowed).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
it('returns allowed with no arguments', async () => {
|
|
88
|
+
const result = await tracker.checkLimits();
|
|
89
|
+
expect(result.allowed).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
it('blocks when daily limit exceeded', async () => {
|
|
92
|
+
const t = new token_tracker_1.TokenTracker({ maxTokensPerDay: 100, maxTokensPerMonth: 1000000 });
|
|
93
|
+
t.track({
|
|
94
|
+
promptTokens: 60,
|
|
95
|
+
completionTokens: 50,
|
|
96
|
+
totalTokens: 110,
|
|
97
|
+
timestamp: NOW,
|
|
98
|
+
userId: 'user1',
|
|
99
|
+
});
|
|
100
|
+
const result = await t.checkLimits('user1');
|
|
101
|
+
expect(result.allowed).toBe(false);
|
|
102
|
+
expect(result.reason).toBe('Daily token limit exceeded');
|
|
103
|
+
});
|
|
104
|
+
it('blocks when monthly limit exceeded', async () => {
|
|
105
|
+
const t = new token_tracker_1.TokenTracker({ maxTokensPerDay: 1000000, maxTokensPerMonth: 100 });
|
|
106
|
+
t.track({
|
|
107
|
+
promptTokens: 60,
|
|
108
|
+
completionTokens: 50,
|
|
109
|
+
totalTokens: 110,
|
|
110
|
+
timestamp: NOW,
|
|
111
|
+
userId: 'user1',
|
|
112
|
+
});
|
|
113
|
+
const result = await t.checkLimits('user1');
|
|
114
|
+
expect(result.allowed).toBe(false);
|
|
115
|
+
expect(result.reason).toBe('Monthly token limit exceeded');
|
|
116
|
+
});
|
|
117
|
+
it('returns usage info in response for user with history', async () => {
|
|
118
|
+
tracker.track(makeUsage({ userId: 'user1', totalTokens: 15 }));
|
|
119
|
+
const result = await tracker.checkLimits('user1');
|
|
120
|
+
expect(result.usage).toBeDefined();
|
|
121
|
+
expect(result.usage?.today).toBe(15);
|
|
122
|
+
expect(result.usage?.limit.daily).toBeDefined();
|
|
123
|
+
});
|
|
124
|
+
it('returns allowed true for user with no history', async () => {
|
|
125
|
+
const result = await tracker.checkLimits('newUser');
|
|
126
|
+
expect(result.allowed).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
describe('calculateCost()', () => {
|
|
130
|
+
it('calculates cost for gpt-4', () => {
|
|
131
|
+
const cost = tracker.calculateCost(makeUsage({ promptTokens: 1000, completionTokens: 500 }), 'gpt-4');
|
|
132
|
+
expect(cost).toBeGreaterThan(0);
|
|
133
|
+
});
|
|
134
|
+
it('calculates cost for gpt-4-turbo-preview', () => {
|
|
135
|
+
const cost = tracker.calculateCost(makeUsage({ promptTokens: 1000, completionTokens: 500 }), 'gpt-4-turbo-preview');
|
|
136
|
+
expect(cost).toBeGreaterThan(0);
|
|
137
|
+
});
|
|
138
|
+
it('calculates cost for gpt-3.5-turbo', () => {
|
|
139
|
+
const cost = tracker.calculateCost(makeUsage({ promptTokens: 1000, completionTokens: 500 }), 'gpt-3.5-turbo');
|
|
140
|
+
expect(cost).toBeGreaterThan(0);
|
|
141
|
+
});
|
|
142
|
+
it('calculates cost for claude models', () => {
|
|
143
|
+
const models = ['claude-3-opus', 'claude-3-sonnet', 'claude-3-haiku'];
|
|
144
|
+
models.forEach((model) => {
|
|
145
|
+
const cost = tracker.calculateCost(makeUsage({ promptTokens: 1000, completionTokens: 500 }), model);
|
|
146
|
+
expect(cost).toBeGreaterThan(0);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
it('returns 0 for unknown model', () => {
|
|
150
|
+
const cost = tracker.calculateCost(makeUsage(), 'unknown-model-xyz');
|
|
151
|
+
expect(cost).toBe(0);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
describe('getUserStats()', () => {
|
|
155
|
+
it('returns zero stats for unknown user', () => {
|
|
156
|
+
const stats = tracker.getUserStats('nobody');
|
|
157
|
+
expect(stats.totalTokens).toBe(0);
|
|
158
|
+
expect(stats.requestCount).toBe(0);
|
|
159
|
+
expect(stats.averageTokensPerRequest).toBe(0);
|
|
160
|
+
});
|
|
161
|
+
it('returns correct stats for user with history', () => {
|
|
162
|
+
tracker.track(makeUsage({ totalTokens: 150, cost: 0.01, userId: 'user1' }));
|
|
163
|
+
tracker.track(makeUsage({ totalTokens: 300, cost: 0.02, userId: 'user1' }));
|
|
164
|
+
const stats = tracker.getUserStats('user1');
|
|
165
|
+
expect(stats.totalTokens).toBe(450);
|
|
166
|
+
expect(stats.requestCount).toBe(2);
|
|
167
|
+
expect(stats.totalCost).toBeCloseTo(0.03);
|
|
168
|
+
expect(stats.averageTokensPerRequest).toBe(225);
|
|
169
|
+
});
|
|
170
|
+
it('calculates dailyAverage', () => {
|
|
171
|
+
tracker.track(makeUsage({ totalTokens: 300, userId: 'user1' }));
|
|
172
|
+
const stats = tracker.getUserStats('user1', 30);
|
|
173
|
+
expect(stats.dailyAverage).toBe(10); // 300/30
|
|
174
|
+
});
|
|
175
|
+
it('excludes usage outside the time window', () => {
|
|
176
|
+
const old = NOW - 40 * 24 * 60 * 60 * 1000;
|
|
177
|
+
tracker.track(makeUsage({ totalTokens: 999, timestamp: old, userId: 'user1' }));
|
|
178
|
+
tracker.track(makeUsage({ totalTokens: 10, userId: 'user1' }));
|
|
179
|
+
const stats = tracker.getUserStats('user1', 30);
|
|
180
|
+
expect(stats.totalTokens).toBe(10);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
describe('getGlobalStats()', () => {
|
|
184
|
+
it('returns zero stats when empty', () => {
|
|
185
|
+
const stats = tracker.getGlobalStats();
|
|
186
|
+
expect(stats.totalTokens).toBe(0);
|
|
187
|
+
expect(stats.requestCount).toBe(0);
|
|
188
|
+
expect(stats.uniqueUsers).toBe(0);
|
|
189
|
+
expect(stats.topUsers).toHaveLength(0);
|
|
190
|
+
});
|
|
191
|
+
it('returns correct stats with multiple users', () => {
|
|
192
|
+
tracker.track(makeUsage({ totalTokens: 150, userId: 'u1' }));
|
|
193
|
+
tracker.track(makeUsage({ totalTokens: 300, userId: 'u2' }));
|
|
194
|
+
tracker.track(makeUsage({ totalTokens: 75 })); // no userId
|
|
195
|
+
const stats = tracker.getGlobalStats();
|
|
196
|
+
expect(stats.totalTokens).toBe(525);
|
|
197
|
+
expect(stats.requestCount).toBe(3);
|
|
198
|
+
expect(stats.uniqueUsers).toBe(2);
|
|
199
|
+
});
|
|
200
|
+
it('returns top users sorted by token usage', () => {
|
|
201
|
+
tracker.track(makeUsage({ totalTokens: 100, userId: 'small' }));
|
|
202
|
+
tracker.track(makeUsage({ totalTokens: 500, userId: 'big' }));
|
|
203
|
+
const stats = tracker.getGlobalStats();
|
|
204
|
+
expect(stats.topUsers[0].userId).toBe('big');
|
|
205
|
+
});
|
|
206
|
+
it('limits top users to 10', () => {
|
|
207
|
+
for (let i = 0; i < 15; i++) {
|
|
208
|
+
tracker.track(makeUsage({ totalTokens: i * 10, userId: `user${i}` }));
|
|
209
|
+
}
|
|
210
|
+
const stats = tracker.getGlobalStats();
|
|
211
|
+
expect(stats.topUsers.length).toBeLessThanOrEqual(10);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
describe('cleanup()', () => {
|
|
215
|
+
it('removes old global usage data', () => {
|
|
216
|
+
const old = NOW - 91 * 24 * 60 * 60 * 1000;
|
|
217
|
+
tracker.track(makeUsage({ totalTokens: 999, timestamp: old }));
|
|
218
|
+
tracker.track(makeUsage({ totalTokens: 15 }));
|
|
219
|
+
tracker.cleanup(90);
|
|
220
|
+
const exported = tracker.exportData();
|
|
221
|
+
expect(exported).toHaveLength(1);
|
|
222
|
+
expect(exported[0].totalTokens).toBe(15);
|
|
223
|
+
});
|
|
224
|
+
it('removes empty user entries after cleanup', () => {
|
|
225
|
+
const old = NOW - 100 * 24 * 60 * 60 * 1000;
|
|
226
|
+
tracker.track(makeUsage({ timestamp: old, userId: 'oldUser' }));
|
|
227
|
+
tracker.cleanup(90);
|
|
228
|
+
expect(tracker.exportData('oldUser')).toHaveLength(0);
|
|
229
|
+
});
|
|
230
|
+
it('keeps recent user entries after cleanup', () => {
|
|
231
|
+
const old = NOW - 100 * 24 * 60 * 60 * 1000;
|
|
232
|
+
tracker.track(makeUsage({ timestamp: old, userId: 'user1' }));
|
|
233
|
+
tracker.track(makeUsage({ userId: 'user1' }));
|
|
234
|
+
tracker.cleanup(90);
|
|
235
|
+
expect(tracker.exportData('user1')).toHaveLength(1);
|
|
236
|
+
});
|
|
237
|
+
it('uses default 90 days when no argument', () => {
|
|
238
|
+
const old = NOW - 95 * 24 * 60 * 60 * 1000;
|
|
239
|
+
tracker.track(makeUsage({ timestamp: old }));
|
|
240
|
+
tracker.cleanup();
|
|
241
|
+
expect(tracker.exportData()).toHaveLength(0);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
describe('exportData()', () => {
|
|
245
|
+
it('exports all global data when no userId', () => {
|
|
246
|
+
tracker.track(makeUsage({ userId: 'u1' }));
|
|
247
|
+
tracker.track(makeUsage());
|
|
248
|
+
const data = tracker.exportData();
|
|
249
|
+
expect(data).toHaveLength(2);
|
|
250
|
+
});
|
|
251
|
+
it('returns empty array for unknown user', () => {
|
|
252
|
+
expect(tracker.exportData('nobody')).toEqual([]);
|
|
253
|
+
});
|
|
254
|
+
it('returns copy of global history', () => {
|
|
255
|
+
tracker.track(makeUsage());
|
|
256
|
+
const data = tracker.exportData();
|
|
257
|
+
expect(data).toHaveLength(1);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
describe('updateConfig()', () => {
|
|
261
|
+
it('updates maxTokensPerRequest', async () => {
|
|
262
|
+
tracker.updateConfig({ maxTokensPerRequest: 2000 });
|
|
263
|
+
const result = await tracker.checkLimits(undefined, 1500);
|
|
264
|
+
expect(result.allowed).toBe(true);
|
|
265
|
+
});
|
|
266
|
+
it('merges config without overwriting unspecified fields', async () => {
|
|
267
|
+
tracker.updateConfig({ maxTokensPerRequest: 50 });
|
|
268
|
+
const result = await tracker.checkLimits(undefined, 100);
|
|
269
|
+
expect(result.allowed).toBe(false);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hazeljs/ai",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.56",
|
|
4
4
|
"description": "AI integration module for HazelJS framework",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -55,5 +55,5 @@
|
|
|
55
55
|
"@hazeljs/cache": ">=0.2.0-beta.0",
|
|
56
56
|
"@hazeljs/core": ">=0.2.0-beta.0"
|
|
57
57
|
},
|
|
58
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "c2737e90974458a8438eee623726f0a453b66b8b"
|
|
59
59
|
}
|