@launchdarkly/server-sdk-ai 0.9.8 → 0.10.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.
- package/CHANGELOG.md +18 -0
- package/__tests__/LDAIClientImpl.test.ts +3 -0
- package/__tests__/LDAIConfigMapper.test.ts +159 -0
- package/__tests__/LDAIConfigTrackerImpl.test.ts +445 -0
- package/__tests__/TokenUsage.test.ts +43 -2
- package/dist/LDAIClientImpl.d.ts.map +1 -1
- package/dist/LDAIClientImpl.js +3 -1
- package/dist/LDAIClientImpl.js.map +1 -1
- package/dist/LDAIConfigMapper.d.ts +10 -0
- package/dist/LDAIConfigMapper.d.ts.map +1 -0
- package/dist/LDAIConfigMapper.js +54 -0
- package/dist/LDAIConfigMapper.js.map +1 -0
- package/dist/LDAIConfigTrackerImpl.d.ts +15 -0
- package/dist/LDAIConfigTrackerImpl.d.ts.map +1 -1
- package/dist/LDAIConfigTrackerImpl.js +51 -2
- package/dist/LDAIConfigTrackerImpl.js.map +1 -1
- package/dist/api/config/LDAIConfig.d.ts +16 -3
- package/dist/api/config/LDAIConfig.d.ts.map +1 -1
- package/dist/api/config/LDAIConfigTracker.d.ts +39 -0
- package/dist/api/config/LDAIConfigTracker.d.ts.map +1 -1
- package/dist/api/config/VercelAISDK.d.ts +18 -0
- package/dist/api/config/VercelAISDK.d.ts.map +1 -0
- package/dist/api/config/VercelAISDK.js +3 -0
- package/dist/api/config/VercelAISDK.js.map +1 -0
- package/dist/api/config/index.d.ts +1 -0
- package/dist/api/config/index.d.ts.map +1 -1
- package/dist/api/config/index.js +1 -0
- package/dist/api/config/index.js.map +1 -1
- package/dist/api/metrics/VercelAISDKTokenUsage.d.ts +7 -0
- package/dist/api/metrics/VercelAISDKTokenUsage.d.ts.map +1 -0
- package/dist/api/metrics/VercelAISDKTokenUsage.js +13 -0
- package/dist/api/metrics/VercelAISDKTokenUsage.js.map +1 -0
- package/dist/api/metrics/index.d.ts +2 -0
- package/dist/api/metrics/index.d.ts.map +1 -1
- package/dist/api/metrics/index.js +2 -0
- package/dist/api/metrics/index.js.map +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/enums/LDFeedbackKind.html +11 -6
- package/docs/functions/createBedrockTokenUsage.html +9 -4
- package/docs/functions/createOpenAiUsage.html +73 -0
- package/docs/functions/createVercelAISDKTokenUsage.html +73 -0
- package/docs/functions/initAi.html +9 -4
- package/docs/index.html +14 -4
- package/docs/interfaces/LDAIClient.html +10 -5
- package/docs/interfaces/LDAIConfig.html +53 -9
- package/docs/interfaces/LDAIConfigTracker.html +92 -15
- package/docs/interfaces/LDMessage.html +11 -6
- package/docs/interfaces/LDModelConfig.html +12 -7
- package/docs/interfaces/LDProviderConfig.html +11 -6
- package/docs/interfaces/LDTokenUsage.html +12 -7
- package/docs/interfaces/VercelAISDKConfig.html +149 -0
- package/docs/interfaces/VercelAISDKMapOptions.html +81 -0
- package/docs/types/LDAIDefaults.html +11 -6
- package/docs/types/VercelAISDKProvider.html +75 -0
- package/package.json +2 -2
- package/src/LDAIClientImpl.ts +21 -3
- package/src/LDAIConfigMapper.ts +64 -0
- package/src/LDAIConfigTrackerImpl.ts +73 -2
- package/src/api/config/LDAIConfig.ts +20 -3
- package/src/api/config/LDAIConfigTracker.ts +49 -0
- package/src/api/config/VercelAISDK.ts +20 -0
- package/src/api/config/index.ts +1 -0
- package/src/api/metrics/VercelAISDKTokenUsage.ts +13 -0
- package/src/api/metrics/index.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.0](https://github.com/launchdarkly/js-core/compare/server-sdk-ai-v0.9.9...server-sdk-ai-v0.10.0) (2025-07-16)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* Adding Vercel AI SDK mapper ([#895](https://github.com/launchdarkly/js-core/issues/895)) ([0befee0](https://github.com/launchdarkly/js-core/commit/0befee0888d0af03b01c0cf6f46eacc80a3ce8e8))
|
|
9
|
+
|
|
10
|
+
## [0.9.9](https://github.com/launchdarkly/js-core/compare/server-sdk-ai-v0.9.8...server-sdk-ai-v0.9.9) (2025-06-17)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Dependencies
|
|
14
|
+
|
|
15
|
+
* The following workspace dependencies were updated
|
|
16
|
+
* devDependencies
|
|
17
|
+
* @launchdarkly/js-server-sdk-common bumped from 2.15.2 to 2.16.0
|
|
18
|
+
* peerDependencies
|
|
19
|
+
* @launchdarkly/js-server-sdk-common bumped from 2.x to 2.16.0
|
|
20
|
+
|
|
3
21
|
## [0.9.8](https://github.com/launchdarkly/js-core/compare/server-sdk-ai-v0.9.7...server-sdk-ai-v0.9.8) (2025-05-21)
|
|
4
22
|
|
|
5
23
|
|
|
@@ -57,6 +57,7 @@ it('returns config with interpolated messagess', async () => {
|
|
|
57
57
|
],
|
|
58
58
|
tracker: expect.any(Object),
|
|
59
59
|
enabled: true,
|
|
60
|
+
toVercelAISDK: expect.any(Function),
|
|
60
61
|
});
|
|
61
62
|
});
|
|
62
63
|
|
|
@@ -102,6 +103,7 @@ it('handles missing metadata in variation', async () => {
|
|
|
102
103
|
messages: [{ role: 'system', content: 'Hello' }],
|
|
103
104
|
tracker: expect.any(Object),
|
|
104
105
|
enabled: false,
|
|
106
|
+
toVercelAISDK: expect.any(Function),
|
|
105
107
|
});
|
|
106
108
|
});
|
|
107
109
|
|
|
@@ -125,6 +127,7 @@ it('passes the default value to the underlying client', async () => {
|
|
|
125
127
|
provider: defaultValue.provider,
|
|
126
128
|
tracker: expect.any(Object),
|
|
127
129
|
enabled: false,
|
|
130
|
+
toVercelAISDK: expect.any(Function),
|
|
128
131
|
});
|
|
129
132
|
|
|
130
133
|
expect(mockLdClient.variation).toHaveBeenCalledWith(key, testContext, defaultValue);
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { LDMessage, VercelAISDKMapOptions } from '../src/api/config';
|
|
2
|
+
import { LDAIConfigMapper } from '../src/LDAIConfigMapper';
|
|
3
|
+
|
|
4
|
+
describe('_findParameter', () => {
|
|
5
|
+
it('handles undefined model and messages', () => {
|
|
6
|
+
const mapper = new LDAIConfigMapper();
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
8
|
+
expect(mapper['_findParameter']<number>('test-param')).toBeUndefined();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('handles parameter not found', () => {
|
|
12
|
+
const mapper = new LDAIConfigMapper({
|
|
13
|
+
name: 'test-ai-model',
|
|
14
|
+
parameters: {
|
|
15
|
+
'test-param': 123,
|
|
16
|
+
},
|
|
17
|
+
custom: {
|
|
18
|
+
'test-param': 456,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
22
|
+
expect(mapper['_findParameter']<number>('other-param')).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('finds parameter from single model parameter', () => {
|
|
26
|
+
const mapper = new LDAIConfigMapper({
|
|
27
|
+
name: 'test-ai-model',
|
|
28
|
+
parameters: {
|
|
29
|
+
'test-param': 123,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
33
|
+
expect(mapper['_findParameter']<number>('test-param')).toEqual(123);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('finds parameter from multiple model parameters', () => {
|
|
37
|
+
const mapper = new LDAIConfigMapper({
|
|
38
|
+
name: 'test-ai-model',
|
|
39
|
+
parameters: {
|
|
40
|
+
testParam: 123,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
44
|
+
expect(mapper['_findParameter']<number>('test-param', 'testParam')).toEqual(123);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('finds parameter from single model custom parameter', () => {
|
|
48
|
+
const mapper = new LDAIConfigMapper({
|
|
49
|
+
name: 'test-ai-model',
|
|
50
|
+
custom: {
|
|
51
|
+
'test-param': 123,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
55
|
+
expect(mapper['_findParameter']<number>('test-param')).toEqual(123);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('finds parameter from multiple model custom parameters', () => {
|
|
59
|
+
const mapper = new LDAIConfigMapper({
|
|
60
|
+
name: 'test-ai-model',
|
|
61
|
+
custom: {
|
|
62
|
+
testParam: 123,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
66
|
+
expect(mapper['_findParameter']<number>('test-param', 'testParam')).toEqual(123);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('gives precedence to model parameters over model custom parameters', () => {
|
|
70
|
+
const mapper = new LDAIConfigMapper({
|
|
71
|
+
name: 'test-ai-model',
|
|
72
|
+
parameters: {
|
|
73
|
+
'test-param': 123,
|
|
74
|
+
},
|
|
75
|
+
custom: {
|
|
76
|
+
'test-param': 456,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
80
|
+
expect(mapper['_findParameter']<number>('test-param', 'testParam')).toEqual(123);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('toVercelAIAISDK', () => {
|
|
85
|
+
const mockModel = { name: 'mockModel' };
|
|
86
|
+
const mockMessages: LDMessage[] = [
|
|
87
|
+
{ role: 'user', content: 'test prompt' },
|
|
88
|
+
{ role: 'system', content: 'test instruction' },
|
|
89
|
+
];
|
|
90
|
+
const mockOptions: VercelAISDKMapOptions = {
|
|
91
|
+
nonInterpolatedMessages: [{ role: 'assistant', content: 'test assistant instruction' }],
|
|
92
|
+
};
|
|
93
|
+
const mockProvider = jest.fn().mockReturnValue(mockModel);
|
|
94
|
+
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
jest.clearAllMocks();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('handles undefined model and messages', () => {
|
|
100
|
+
const mapper = new LDAIConfigMapper();
|
|
101
|
+
const result = mapper.toVercelAISDK(mockProvider);
|
|
102
|
+
|
|
103
|
+
expect(mockProvider).toHaveBeenCalledWith('');
|
|
104
|
+
expect(result).toEqual(
|
|
105
|
+
expect.objectContaining({
|
|
106
|
+
model: mockModel,
|
|
107
|
+
messages: undefined,
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('uses additional messages', () => {
|
|
113
|
+
const mapper = new LDAIConfigMapper({ name: 'test-ai-model' });
|
|
114
|
+
const result = mapper.toVercelAISDK(mockProvider, mockOptions);
|
|
115
|
+
|
|
116
|
+
expect(mockProvider).toHaveBeenCalledWith('test-ai-model');
|
|
117
|
+
expect(result).toEqual(
|
|
118
|
+
expect.objectContaining({
|
|
119
|
+
model: mockModel,
|
|
120
|
+
messages: mockOptions.nonInterpolatedMessages,
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('combines config messages and additional messages', () => {
|
|
126
|
+
const mapper = new LDAIConfigMapper({ name: 'test-ai-model' }, undefined, mockMessages);
|
|
127
|
+
const result = mapper.toVercelAISDK(mockProvider, mockOptions);
|
|
128
|
+
|
|
129
|
+
expect(mockProvider).toHaveBeenCalledWith('test-ai-model');
|
|
130
|
+
expect(result).toEqual(
|
|
131
|
+
expect.objectContaining({
|
|
132
|
+
model: mockModel,
|
|
133
|
+
messages: [...mockMessages, ...(mockOptions.nonInterpolatedMessages ?? [])],
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('requests parameters correctly', () => {
|
|
139
|
+
const mapper = new LDAIConfigMapper({ name: 'test-ai-model' }, undefined, mockMessages);
|
|
140
|
+
const findParameterMock = jest.spyOn(mapper as any, '_findParameter');
|
|
141
|
+
const result = mapper.toVercelAISDK(mockProvider);
|
|
142
|
+
|
|
143
|
+
expect(mockProvider).toHaveBeenCalledWith('test-ai-model');
|
|
144
|
+
expect(result).toEqual(
|
|
145
|
+
expect.objectContaining({
|
|
146
|
+
model: mockModel,
|
|
147
|
+
messages: mockMessages,
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
150
|
+
expect(findParameterMock).toHaveBeenCalledWith('max_tokens', 'maxTokens');
|
|
151
|
+
expect(findParameterMock).toHaveBeenCalledWith('temperature');
|
|
152
|
+
expect(findParameterMock).toHaveBeenCalledWith('top_p', 'topP');
|
|
153
|
+
expect(findParameterMock).toHaveBeenCalledWith('top_k', 'topK');
|
|
154
|
+
expect(findParameterMock).toHaveBeenCalledWith('presence_penalty', 'presencePenalty');
|
|
155
|
+
expect(findParameterMock).toHaveBeenCalledWith('frequency_penalty', 'frequencyPenalty');
|
|
156
|
+
expect(findParameterMock).toHaveBeenCalledWith('stop', 'stop_sequences', 'stopSequences');
|
|
157
|
+
expect(findParameterMock).toHaveBeenCalledWith('seed');
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -129,6 +129,13 @@ it('tracks success', () => {
|
|
|
129
129
|
{ configKey, variationKey, version },
|
|
130
130
|
1,
|
|
131
131
|
);
|
|
132
|
+
|
|
133
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
134
|
+
'$ld:ai:generation:success',
|
|
135
|
+
testContext,
|
|
136
|
+
{ configKey, variationKey, version },
|
|
137
|
+
1,
|
|
138
|
+
);
|
|
132
139
|
});
|
|
133
140
|
|
|
134
141
|
it('tracks OpenAI usage', async () => {
|
|
@@ -167,6 +174,20 @@ it('tracks OpenAI usage', async () => {
|
|
|
167
174
|
1,
|
|
168
175
|
);
|
|
169
176
|
|
|
177
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
178
|
+
'$ld:ai:generation:success',
|
|
179
|
+
testContext,
|
|
180
|
+
{ configKey, variationKey, version },
|
|
181
|
+
1,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
expect(mockTrack).not.toHaveBeenCalledWith(
|
|
185
|
+
'$ld:ai:generation:error',
|
|
186
|
+
expect.anything(),
|
|
187
|
+
expect.anything(),
|
|
188
|
+
expect.anything(),
|
|
189
|
+
);
|
|
190
|
+
|
|
170
191
|
expect(mockTrack).toHaveBeenCalledWith(
|
|
171
192
|
'$ld:ai:tokens:total',
|
|
172
193
|
testContext,
|
|
@@ -226,6 +247,13 @@ it('tracks error when OpenAI metrics function throws', async () => {
|
|
|
226
247
|
{ configKey, variationKey, version },
|
|
227
248
|
1,
|
|
228
249
|
);
|
|
250
|
+
|
|
251
|
+
expect(mockTrack).not.toHaveBeenCalledWith(
|
|
252
|
+
expect.stringMatching(/^\$ld:ai:tokens:/),
|
|
253
|
+
expect.anything(),
|
|
254
|
+
expect.anything(),
|
|
255
|
+
expect.anything(),
|
|
256
|
+
);
|
|
229
257
|
});
|
|
230
258
|
|
|
231
259
|
it('tracks Bedrock conversation with successful response', () => {
|
|
@@ -260,6 +288,20 @@ it('tracks Bedrock conversation with successful response', () => {
|
|
|
260
288
|
1,
|
|
261
289
|
);
|
|
262
290
|
|
|
291
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
292
|
+
'$ld:ai:generation:success',
|
|
293
|
+
testContext,
|
|
294
|
+
{ configKey, variationKey, version },
|
|
295
|
+
1,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
expect(mockTrack).not.toHaveBeenCalledWith(
|
|
299
|
+
'$ld:ai:generation:error',
|
|
300
|
+
expect.anything(),
|
|
301
|
+
expect.anything(),
|
|
302
|
+
expect.anything(),
|
|
303
|
+
);
|
|
304
|
+
|
|
263
305
|
expect(mockTrack).toHaveBeenCalledWith(
|
|
264
306
|
'$ld:ai:duration:total',
|
|
265
307
|
testContext,
|
|
@@ -318,6 +360,409 @@ it('tracks Bedrock conversation with error response', () => {
|
|
|
318
360
|
{ configKey, variationKey, version },
|
|
319
361
|
1,
|
|
320
362
|
);
|
|
363
|
+
|
|
364
|
+
expect(mockTrack).not.toHaveBeenCalledWith(
|
|
365
|
+
expect.stringMatching(/^\$ld:ai:tokens:/),
|
|
366
|
+
expect.anything(),
|
|
367
|
+
expect.anything(),
|
|
368
|
+
expect.anything(),
|
|
369
|
+
);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
describe('Vercel AI SDK generateText', () => {
|
|
373
|
+
it('tracks Vercel AI SDK usage', async () => {
|
|
374
|
+
const tracker = new LDAIConfigTrackerImpl(
|
|
375
|
+
mockLdClient,
|
|
376
|
+
configKey,
|
|
377
|
+
variationKey,
|
|
378
|
+
version,
|
|
379
|
+
testContext,
|
|
380
|
+
);
|
|
381
|
+
jest.spyOn(global.Date, 'now').mockReturnValueOnce(1000).mockReturnValueOnce(2000);
|
|
382
|
+
|
|
383
|
+
const TOTAL_TOKENS = 100;
|
|
384
|
+
const PROMPT_TOKENS = 49;
|
|
385
|
+
const COMPLETION_TOKENS = 51;
|
|
386
|
+
|
|
387
|
+
await tracker.trackVercelAISDKGenerateTextMetrics(async () => ({
|
|
388
|
+
usage: {
|
|
389
|
+
totalTokens: TOTAL_TOKENS,
|
|
390
|
+
promptTokens: PROMPT_TOKENS,
|
|
391
|
+
completionTokens: COMPLETION_TOKENS,
|
|
392
|
+
},
|
|
393
|
+
}));
|
|
394
|
+
|
|
395
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
396
|
+
'$ld:ai:duration:total',
|
|
397
|
+
testContext,
|
|
398
|
+
{ configKey, variationKey, version },
|
|
399
|
+
1000,
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
403
|
+
'$ld:ai:generation',
|
|
404
|
+
testContext,
|
|
405
|
+
{ configKey, variationKey, version },
|
|
406
|
+
1,
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
410
|
+
'$ld:ai:generation:success',
|
|
411
|
+
testContext,
|
|
412
|
+
{ configKey, variationKey, version },
|
|
413
|
+
1,
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
expect(mockTrack).not.toHaveBeenCalledWith(
|
|
417
|
+
'$ld:ai:generation:error',
|
|
418
|
+
expect.anything(),
|
|
419
|
+
expect.anything(),
|
|
420
|
+
expect.anything(),
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
424
|
+
'$ld:ai:tokens:total',
|
|
425
|
+
testContext,
|
|
426
|
+
{ configKey, variationKey, version },
|
|
427
|
+
TOTAL_TOKENS,
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
431
|
+
'$ld:ai:tokens:input',
|
|
432
|
+
testContext,
|
|
433
|
+
{ configKey, variationKey, version },
|
|
434
|
+
PROMPT_TOKENS,
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
438
|
+
'$ld:ai:tokens:output',
|
|
439
|
+
testContext,
|
|
440
|
+
{ configKey, variationKey, version },
|
|
441
|
+
COMPLETION_TOKENS,
|
|
442
|
+
);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('tracks error when Vercel AI SDK metrics function throws', async () => {
|
|
446
|
+
const tracker = new LDAIConfigTrackerImpl(
|
|
447
|
+
mockLdClient,
|
|
448
|
+
configKey,
|
|
449
|
+
variationKey,
|
|
450
|
+
version,
|
|
451
|
+
testContext,
|
|
452
|
+
);
|
|
453
|
+
jest.spyOn(global.Date, 'now').mockReturnValueOnce(1000).mockReturnValueOnce(2000);
|
|
454
|
+
|
|
455
|
+
const error = new Error('Vercel AI SDK API error');
|
|
456
|
+
await expect(
|
|
457
|
+
tracker.trackVercelAISDKGenerateTextMetrics(async () => {
|
|
458
|
+
throw error;
|
|
459
|
+
}),
|
|
460
|
+
).rejects.toThrow(error);
|
|
461
|
+
|
|
462
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
463
|
+
'$ld:ai:duration:total',
|
|
464
|
+
testContext,
|
|
465
|
+
{ configKey, variationKey, version },
|
|
466
|
+
1000,
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
470
|
+
'$ld:ai:generation',
|
|
471
|
+
testContext,
|
|
472
|
+
{ configKey, variationKey, version },
|
|
473
|
+
1,
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
477
|
+
'$ld:ai:generation:error',
|
|
478
|
+
testContext,
|
|
479
|
+
{ configKey, variationKey, version },
|
|
480
|
+
1,
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
expect(mockTrack).not.toHaveBeenCalledWith(
|
|
484
|
+
expect.stringMatching(/^\$ld:ai:tokens:/),
|
|
485
|
+
expect.anything(),
|
|
486
|
+
expect.anything(),
|
|
487
|
+
expect.anything(),
|
|
488
|
+
);
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
describe('Vercel AI SDK streamText', () => {
|
|
493
|
+
it('tracks Vercel AI SDK usage', async () => {
|
|
494
|
+
const tracker = new LDAIConfigTrackerImpl(
|
|
495
|
+
mockLdClient,
|
|
496
|
+
configKey,
|
|
497
|
+
variationKey,
|
|
498
|
+
version,
|
|
499
|
+
testContext,
|
|
500
|
+
);
|
|
501
|
+
jest.spyOn(global.Date, 'now').mockReturnValueOnce(1000).mockReturnValueOnce(2000);
|
|
502
|
+
|
|
503
|
+
const TOTAL_TOKENS = 100;
|
|
504
|
+
const PROMPT_TOKENS = 49;
|
|
505
|
+
const COMPLETION_TOKENS = 51;
|
|
506
|
+
|
|
507
|
+
let resolveDone: ((value: boolean) => void) | undefined;
|
|
508
|
+
const donePromise = new Promise<boolean>((resolve) => {
|
|
509
|
+
resolveDone = resolve;
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const finishReason = Promise.resolve('stop');
|
|
513
|
+
jest
|
|
514
|
+
.spyOn(finishReason, 'then')
|
|
515
|
+
.mockImplementationOnce((fn) => finishReason.then(fn).finally(() => resolveDone?.(true)));
|
|
516
|
+
|
|
517
|
+
tracker.trackVercelAISDKStreamTextMetrics(() => ({
|
|
518
|
+
finishReason,
|
|
519
|
+
usage: Promise.resolve({
|
|
520
|
+
totalTokens: TOTAL_TOKENS,
|
|
521
|
+
promptTokens: PROMPT_TOKENS,
|
|
522
|
+
completionTokens: COMPLETION_TOKENS,
|
|
523
|
+
}),
|
|
524
|
+
}));
|
|
525
|
+
|
|
526
|
+
await donePromise;
|
|
527
|
+
|
|
528
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
529
|
+
'$ld:ai:duration:total',
|
|
530
|
+
testContext,
|
|
531
|
+
{ configKey, variationKey, version },
|
|
532
|
+
1000,
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
536
|
+
'$ld:ai:generation',
|
|
537
|
+
testContext,
|
|
538
|
+
{ configKey, variationKey, version },
|
|
539
|
+
1,
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
543
|
+
'$ld:ai:generation:success',
|
|
544
|
+
testContext,
|
|
545
|
+
{ configKey, variationKey, version },
|
|
546
|
+
1,
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
expect(mockTrack).not.toHaveBeenCalledWith(
|
|
550
|
+
'$ld:ai:generation:error',
|
|
551
|
+
expect.anything(),
|
|
552
|
+
expect.anything(),
|
|
553
|
+
expect.anything(),
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
557
|
+
'$ld:ai:tokens:total',
|
|
558
|
+
testContext,
|
|
559
|
+
{ configKey, variationKey, version },
|
|
560
|
+
TOTAL_TOKENS,
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
564
|
+
'$ld:ai:tokens:input',
|
|
565
|
+
testContext,
|
|
566
|
+
{ configKey, variationKey, version },
|
|
567
|
+
PROMPT_TOKENS,
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
571
|
+
'$ld:ai:tokens:output',
|
|
572
|
+
testContext,
|
|
573
|
+
{ configKey, variationKey, version },
|
|
574
|
+
COMPLETION_TOKENS,
|
|
575
|
+
);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it('tracks error when Vercel AI SDK metrics function throws', async () => {
|
|
579
|
+
const tracker = new LDAIConfigTrackerImpl(
|
|
580
|
+
mockLdClient,
|
|
581
|
+
configKey,
|
|
582
|
+
variationKey,
|
|
583
|
+
version,
|
|
584
|
+
testContext,
|
|
585
|
+
);
|
|
586
|
+
jest.spyOn(global.Date, 'now').mockReturnValueOnce(1000).mockReturnValueOnce(2000);
|
|
587
|
+
|
|
588
|
+
const error = new Error('Vercel AI SDK API error');
|
|
589
|
+
expect(() =>
|
|
590
|
+
tracker.trackVercelAISDKStreamTextMetrics(() => {
|
|
591
|
+
throw error;
|
|
592
|
+
}),
|
|
593
|
+
).toThrow(error);
|
|
594
|
+
|
|
595
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
596
|
+
'$ld:ai:duration:total',
|
|
597
|
+
testContext,
|
|
598
|
+
{ configKey, variationKey, version },
|
|
599
|
+
1000,
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
603
|
+
'$ld:ai:generation',
|
|
604
|
+
testContext,
|
|
605
|
+
{ configKey, variationKey, version },
|
|
606
|
+
1,
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
610
|
+
'$ld:ai:generation:error',
|
|
611
|
+
testContext,
|
|
612
|
+
{ configKey, variationKey, version },
|
|
613
|
+
1,
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
expect(mockTrack).not.toHaveBeenCalledWith(
|
|
617
|
+
expect.stringMatching(/^\$ld:ai:tokens:/),
|
|
618
|
+
expect.anything(),
|
|
619
|
+
expect.anything(),
|
|
620
|
+
expect.anything(),
|
|
621
|
+
);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it('tracks error when Vercel AI SDK finishes because of an error', async () => {
|
|
625
|
+
const tracker = new LDAIConfigTrackerImpl(
|
|
626
|
+
mockLdClient,
|
|
627
|
+
configKey,
|
|
628
|
+
variationKey,
|
|
629
|
+
version,
|
|
630
|
+
testContext,
|
|
631
|
+
);
|
|
632
|
+
jest.spyOn(global.Date, 'now').mockReturnValueOnce(1000).mockReturnValueOnce(2000);
|
|
633
|
+
|
|
634
|
+
tracker.trackVercelAISDKStreamTextMetrics(() => ({
|
|
635
|
+
finishReason: Promise.resolve('error'),
|
|
636
|
+
}));
|
|
637
|
+
|
|
638
|
+
await new Promise(process.nextTick);
|
|
639
|
+
|
|
640
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
641
|
+
'$ld:ai:duration:total',
|
|
642
|
+
testContext,
|
|
643
|
+
{ configKey, variationKey, version },
|
|
644
|
+
1000,
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
648
|
+
'$ld:ai:generation',
|
|
649
|
+
testContext,
|
|
650
|
+
{ configKey, variationKey, version },
|
|
651
|
+
1,
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
655
|
+
'$ld:ai:generation:error',
|
|
656
|
+
testContext,
|
|
657
|
+
{ configKey, variationKey, version },
|
|
658
|
+
1,
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
expect(mockTrack).not.toHaveBeenCalledWith(
|
|
662
|
+
expect.stringMatching(/^\$ld:ai:tokens:/),
|
|
663
|
+
expect.anything(),
|
|
664
|
+
expect.anything(),
|
|
665
|
+
expect.anything(),
|
|
666
|
+
);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it('tracks error when Vercel AI SDK finishReason promise rejects', async () => {
|
|
670
|
+
const tracker = new LDAIConfigTrackerImpl(
|
|
671
|
+
mockLdClient,
|
|
672
|
+
configKey,
|
|
673
|
+
variationKey,
|
|
674
|
+
version,
|
|
675
|
+
testContext,
|
|
676
|
+
);
|
|
677
|
+
jest.spyOn(global.Date, 'now').mockReturnValueOnce(1000).mockReturnValueOnce(2000);
|
|
678
|
+
|
|
679
|
+
tracker.trackVercelAISDKStreamTextMetrics(() => ({
|
|
680
|
+
finishReason: Promise.reject(new Error('Vercel AI SDK API error')),
|
|
681
|
+
}));
|
|
682
|
+
|
|
683
|
+
await new Promise(process.nextTick);
|
|
684
|
+
|
|
685
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
686
|
+
'$ld:ai:duration:total',
|
|
687
|
+
testContext,
|
|
688
|
+
{ configKey, variationKey, version },
|
|
689
|
+
1000,
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
693
|
+
'$ld:ai:generation',
|
|
694
|
+
testContext,
|
|
695
|
+
{ configKey, variationKey, version },
|
|
696
|
+
1,
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
700
|
+
'$ld:ai:generation:error',
|
|
701
|
+
testContext,
|
|
702
|
+
{ configKey, variationKey, version },
|
|
703
|
+
1,
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
expect(mockTrack).not.toHaveBeenCalledWith(
|
|
707
|
+
expect.stringMatching(/^\$ld:ai:tokens:/),
|
|
708
|
+
expect.anything(),
|
|
709
|
+
expect.anything(),
|
|
710
|
+
expect.anything(),
|
|
711
|
+
);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it('squashes error when Vercel AI SDK usage promise rejects', async () => {
|
|
715
|
+
const tracker = new LDAIConfigTrackerImpl(
|
|
716
|
+
mockLdClient,
|
|
717
|
+
configKey,
|
|
718
|
+
variationKey,
|
|
719
|
+
version,
|
|
720
|
+
testContext,
|
|
721
|
+
);
|
|
722
|
+
jest.spyOn(global.Date, 'now').mockReturnValueOnce(1000).mockReturnValueOnce(2000);
|
|
723
|
+
|
|
724
|
+
tracker.trackVercelAISDKStreamTextMetrics(() => ({
|
|
725
|
+
finishReason: Promise.resolve('stop'),
|
|
726
|
+
usage: Promise.reject(new Error('Vercel AI SDK API error')),
|
|
727
|
+
}));
|
|
728
|
+
|
|
729
|
+
await new Promise(process.nextTick);
|
|
730
|
+
|
|
731
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
732
|
+
'$ld:ai:duration:total',
|
|
733
|
+
testContext,
|
|
734
|
+
{ configKey, variationKey, version },
|
|
735
|
+
1000,
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
739
|
+
'$ld:ai:generation',
|
|
740
|
+
testContext,
|
|
741
|
+
{ configKey, variationKey, version },
|
|
742
|
+
1,
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
expect(mockTrack).toHaveBeenCalledWith(
|
|
746
|
+
'$ld:ai:generation:success',
|
|
747
|
+
testContext,
|
|
748
|
+
{ configKey, variationKey, version },
|
|
749
|
+
1,
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
expect(mockTrack).not.toHaveBeenCalledWith(
|
|
753
|
+
'$ld:ai:generation:error',
|
|
754
|
+
expect.anything(),
|
|
755
|
+
expect.anything(),
|
|
756
|
+
expect.anything(),
|
|
757
|
+
);
|
|
758
|
+
|
|
759
|
+
expect(mockTrack).not.toHaveBeenCalledWith(
|
|
760
|
+
expect.stringMatching(/^\$ld:ai:tokens:/),
|
|
761
|
+
expect.anything(),
|
|
762
|
+
expect.anything(),
|
|
763
|
+
expect.anything(),
|
|
764
|
+
);
|
|
765
|
+
});
|
|
321
766
|
});
|
|
322
767
|
|
|
323
768
|
it('tracks tokens', () => {
|