@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.
Files changed (64) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/__tests__/LDAIClientImpl.test.ts +3 -0
  3. package/__tests__/LDAIConfigMapper.test.ts +159 -0
  4. package/__tests__/LDAIConfigTrackerImpl.test.ts +445 -0
  5. package/__tests__/TokenUsage.test.ts +43 -2
  6. package/dist/LDAIClientImpl.d.ts.map +1 -1
  7. package/dist/LDAIClientImpl.js +3 -1
  8. package/dist/LDAIClientImpl.js.map +1 -1
  9. package/dist/LDAIConfigMapper.d.ts +10 -0
  10. package/dist/LDAIConfigMapper.d.ts.map +1 -0
  11. package/dist/LDAIConfigMapper.js +54 -0
  12. package/dist/LDAIConfigMapper.js.map +1 -0
  13. package/dist/LDAIConfigTrackerImpl.d.ts +15 -0
  14. package/dist/LDAIConfigTrackerImpl.d.ts.map +1 -1
  15. package/dist/LDAIConfigTrackerImpl.js +51 -2
  16. package/dist/LDAIConfigTrackerImpl.js.map +1 -1
  17. package/dist/api/config/LDAIConfig.d.ts +16 -3
  18. package/dist/api/config/LDAIConfig.d.ts.map +1 -1
  19. package/dist/api/config/LDAIConfigTracker.d.ts +39 -0
  20. package/dist/api/config/LDAIConfigTracker.d.ts.map +1 -1
  21. package/dist/api/config/VercelAISDK.d.ts +18 -0
  22. package/dist/api/config/VercelAISDK.d.ts.map +1 -0
  23. package/dist/api/config/VercelAISDK.js +3 -0
  24. package/dist/api/config/VercelAISDK.js.map +1 -0
  25. package/dist/api/config/index.d.ts +1 -0
  26. package/dist/api/config/index.d.ts.map +1 -1
  27. package/dist/api/config/index.js +1 -0
  28. package/dist/api/config/index.js.map +1 -1
  29. package/dist/api/metrics/VercelAISDKTokenUsage.d.ts +7 -0
  30. package/dist/api/metrics/VercelAISDKTokenUsage.d.ts.map +1 -0
  31. package/dist/api/metrics/VercelAISDKTokenUsage.js +13 -0
  32. package/dist/api/metrics/VercelAISDKTokenUsage.js.map +1 -0
  33. package/dist/api/metrics/index.d.ts +2 -0
  34. package/dist/api/metrics/index.d.ts.map +1 -1
  35. package/dist/api/metrics/index.js +2 -0
  36. package/dist/api/metrics/index.js.map +1 -1
  37. package/docs/assets/search.js +1 -1
  38. package/docs/enums/LDFeedbackKind.html +11 -6
  39. package/docs/functions/createBedrockTokenUsage.html +9 -4
  40. package/docs/functions/createOpenAiUsage.html +73 -0
  41. package/docs/functions/createVercelAISDKTokenUsage.html +73 -0
  42. package/docs/functions/initAi.html +9 -4
  43. package/docs/index.html +14 -4
  44. package/docs/interfaces/LDAIClient.html +10 -5
  45. package/docs/interfaces/LDAIConfig.html +53 -9
  46. package/docs/interfaces/LDAIConfigTracker.html +92 -15
  47. package/docs/interfaces/LDMessage.html +11 -6
  48. package/docs/interfaces/LDModelConfig.html +12 -7
  49. package/docs/interfaces/LDProviderConfig.html +11 -6
  50. package/docs/interfaces/LDTokenUsage.html +12 -7
  51. package/docs/interfaces/VercelAISDKConfig.html +149 -0
  52. package/docs/interfaces/VercelAISDKMapOptions.html +81 -0
  53. package/docs/types/LDAIDefaults.html +11 -6
  54. package/docs/types/VercelAISDKProvider.html +75 -0
  55. package/package.json +2 -2
  56. package/src/LDAIClientImpl.ts +21 -3
  57. package/src/LDAIConfigMapper.ts +64 -0
  58. package/src/LDAIConfigTrackerImpl.ts +73 -2
  59. package/src/api/config/LDAIConfig.ts +20 -3
  60. package/src/api/config/LDAIConfigTracker.ts +49 -0
  61. package/src/api/config/VercelAISDK.ts +20 -0
  62. package/src/api/config/index.ts +1 -0
  63. package/src/api/metrics/VercelAISDKTokenUsage.ts +13 -0
  64. 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', () => {