weflayr 0.23.0 → 0.24.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 (41) hide show
  1. package/dist/api/generated/facade.gen.d.ts +77 -0
  2. package/dist/api/generated/facade.gen.js +7 -1
  3. package/dist/api/generated/index.d.ts +2 -2
  4. package/dist/api/generated/index.js +7 -1
  5. package/dist/api/generated/sdk.gen.d.ts +25 -1
  6. package/dist/api/generated/sdk.gen.js +55 -1
  7. package/dist/api/generated/types.gen.d.ts +256 -0
  8. package/index.d.ts +5 -2
  9. package/package.json +1 -1
  10. package/src/api/generated/facade.gen.ts +36 -0
  11. package/src/api/generated/index.ts +2 -2
  12. package/src/api/generated/sdk.gen.ts +55 -1
  13. package/src/api/generated/types.gen.ts +293 -0
  14. package/src/auto-instrument.js +1 -0
  15. package/src/instrumentation/anthropic/get_cache_and_reasoning_telemetry.js +11 -0
  16. package/src/instrumentation/anthropic/get_cache_config_telemetry.js +90 -0
  17. package/src/instrumentation/anthropic/get_tool_definitions_telemetry.js +6 -1
  18. package/src/instrumentation/anthropic/set_telemetry_attributes.js +2 -0
  19. package/src/instrumentation/bedrock/get_cache_config_telemetry.js +106 -0
  20. package/src/instrumentation/bedrock/get_cache_telemetry.js +11 -6
  21. package/src/instrumentation/bedrock/set_telemetry_attributes.js +6 -2
  22. package/src/instrumentation/google/get_cache_operations_telemetry.js +171 -0
  23. package/src/instrumentation/google/get_cache_telemetry.js +16 -2
  24. package/src/instrumentation/google/set_telemetry_attributes.js +8 -2
  25. package/src/instrumentation/openai/get_cache_config_telemetry.js +63 -0
  26. package/src/instrumentation/openai/set_telemetry_attributes.js +2 -0
  27. package/src/instrumentation/vercel/full_instrumentation.js +10 -2
  28. package/src/instrumentation/vercel/get_cache_creation_telemetry.js +70 -0
  29. package/src/otel/propagation.js +22 -9
  30. package/src/otel/span-filter.js +12 -12
  31. package/src/otel/span-processor.js +8 -0
  32. package/src/otel/vercel-ai-sdk-span-filter.js +1 -1
  33. package/tests/index.test.js +67 -7
  34. package/tests/instrumentation/anthropic/get_cache_and_reasoning_telemetry.test.js +23 -0
  35. package/tests/instrumentation/anthropic/get_cache_config_telemetry.test.js +92 -0
  36. package/tests/instrumentation/anthropic/get_tool_definitions_telemetry.test.js +15 -0
  37. package/tests/instrumentation/bedrock/get_cache_config_telemetry.test.js +107 -0
  38. package/tests/instrumentation/bedrock/get_cache_telemetry.test.js +16 -0
  39. package/tests/instrumentation/google/get_cache_operations_telemetry.test.js +130 -0
  40. package/tests/instrumentation/openai/get_cache_config_telemetry.test.js +83 -0
  41. package/tests/instrumentation/vercel/get_cache_creation_telemetry.test.js +96 -0
@@ -234,22 +234,32 @@ test('buildExporter does not warn when warnOnExportError is false', async () =>
234
234
 
235
235
  // ── span-filter ───────────────────────────────────────────────────────────────
236
236
 
237
- test('isDefaultExportSpan accepts spans with gen_ai.* attributes', () => {
237
+ test('isDefaultExportSpan accepts spans from known instrumentation scopes', () => {
238
238
  const span = makeFakeSpan({
239
- attributes: { 'gen_ai.system': 'openai' },
240
- instrumentationScope: { name: 'unknown' },
239
+ attributes: {},
240
+ instrumentationScope: { name: 'opentelemetry.instrumentation.openai' },
241
241
  });
242
242
  assert.equal(isDefaultExportSpan(span), true);
243
243
  });
244
244
 
245
- test('isDefaultExportSpan accepts spans from known instrumentation scopes', () => {
245
+ test('isDefaultExportSpan accepts weflayr instrumentation spans', () => {
246
246
  const span = makeFakeSpan({
247
247
  attributes: {},
248
- instrumentationScope: { name: 'opentelemetry.instrumentation.openai' },
248
+ instrumentationScope: { name: 'weflayr.instrumentation.openai_audio' },
249
249
  });
250
250
  assert.equal(isDefaultExportSpan(span), true);
251
251
  });
252
252
 
253
+ test('isDefaultExportSpan rejects gen_ai spans from unknown scopes', () => {
254
+ // APM SDKs (Sentry, Datadog, ...) also instrument LLM calls with gen_ai.*
255
+ // attributes — exporting them would duplicate the instrumentor's span.
256
+ const span = makeFakeSpan({
257
+ attributes: { 'gen_ai.system': 'openai' },
258
+ instrumentationScope: { name: '@sentry/node' },
259
+ });
260
+ assert.equal(isDefaultExportSpan(span), false);
261
+ });
262
+
253
263
  test('isDefaultExportSpan rejects unrelated spans', () => {
254
264
  const span = makeFakeSpan({
255
265
  attributes: { 'http.method': 'GET' },
@@ -335,6 +345,26 @@ test('WeflayrSpanProcessor applies mask before export', async () => {
335
345
  assert.equal(span.attributes['gen_ai.prompt'], '[REDACTED]');
336
346
  });
337
347
 
348
+ test('WeflayrSpanProcessor stamps mask function source on exported spans', async () => {
349
+ const exporter = makeExporterStub();
350
+ const mask = (attrs) => attrs;
351
+ const proc = new WeflayrSpanProcessor({ exporter, exportMode: 'immediate', mask });
352
+ const span = makeFakeSpan({ attributes: { 'gen_ai.system': 'openai' } });
353
+ proc.onEnd(span);
354
+ await proc.forceFlush();
355
+ const maskFunctionSource = exporter.exported[0].attributes['weflayr.mask_function'];
356
+ assert.ok(maskFunctionSource.includes('(attrs) => attrs'));
357
+ });
358
+
359
+ test('WeflayrSpanProcessor omits mask function attribute without mask', async () => {
360
+ const exporter = makeExporterStub();
361
+ const proc = new WeflayrSpanProcessor({ exporter, exportMode: 'immediate' });
362
+ const span = makeFakeSpan({ attributes: { 'gen_ai.system': 'openai' } });
363
+ proc.onEnd(span);
364
+ await proc.forceFlush();
365
+ assert.equal(exporter.exported[0].attributes['weflayr.mask_function'], undefined);
366
+ });
367
+
338
368
  test('WeflayrSpanProcessor strips message content when capture disabled', async () => {
339
369
  // Instrumentors gate content on their own env vars (openllmetry captures by
340
370
  // default), so the processor itself must drop message content when
@@ -395,7 +425,7 @@ function _makeMetadataHarness() {
395
425
  const exporter = makeExporterStub();
396
426
  const proc = new WeflayrSpanProcessor({ exporter, exportMode: 'immediate' });
397
427
  const provider = new BasicTracerProvider({ spanProcessors: [proc] });
398
- const tracer = provider.getTracer('test');
428
+ const tracer = provider.getTracer('opentelemetry.instrumentation.openai');
399
429
  return { exporter, proc, provider, tracer };
400
430
  }
401
431
 
@@ -558,9 +588,34 @@ test('propagateMetadata callable form runs function', async () => {
558
588
  await provider.shutdown();
559
589
  });
560
590
 
561
- test('propagateMetadata providerName writes attribute', async () => {
591
+ test('propagateMetadata providerNameOverride writes attribute', async () => {
592
+ const { exporter, proc, provider, tracer } = _makeMetadataHarness();
593
+
594
+ await weflayr.propagateMetadata(
595
+ {
596
+ featureName: 'f',
597
+ customerName: 'c',
598
+ providerNameOverride: weflayr.AIProviderName.VERCEL_AI_GATEWAY,
599
+ },
600
+ async () => {
601
+ const span = tracer.startSpan('child');
602
+ span.setAttribute('gen_ai.system', 'openai');
603
+ span.end();
604
+ }
605
+ );
606
+
607
+ await proc.forceFlush();
608
+ assert.equal(exporter.exported[0].attributes['weflayr.provider_name'], 'vercel_ai_gateway');
609
+ await provider.shutdown();
610
+ });
611
+
612
+ test('propagateMetadata deprecated providerName still works', async () => {
562
613
  const { exporter, proc, provider, tracer } = _makeMetadataHarness();
563
614
 
615
+ const warnings = [];
616
+ const onWarning = (warning) => warnings.push(warning);
617
+ process.on('warning', onWarning);
618
+
564
619
  await weflayr.propagateMetadata(
565
620
  {
566
621
  featureName: 'f',
@@ -574,6 +629,11 @@ test('propagateMetadata providerName writes attribute', async () => {
574
629
  }
575
630
  );
576
631
 
632
+ // process.emitWarning delivers on a later tick; drain it before asserting.
633
+ await new Promise((resolve) => process.nextTick(resolve));
634
+ process.removeListener('warning', onWarning);
635
+ assert.ok(warnings.some((warning) => /providerName.*deprecated/.test(warning.message)));
636
+
577
637
  await proc.forceFlush();
578
638
  assert.equal(exporter.exported[0].attributes['weflayr.provider_name'], 'vercel_ai_gateway');
579
639
  await provider.shutdown();
@@ -63,6 +63,29 @@ test('stamps cache read on a warm call', () => {
63
63
  assert.equal(span.attrs['gen_ai.usage.cache_creation_input_tokens'], undefined);
64
64
  });
65
65
 
66
+ test('stamps usage.cache_creation verbatim', () => {
67
+ const instrumentation = { _endSpan() {} };
68
+ new AnthropicPromptCachingInstrumentor().instrument(instrumentation);
69
+
70
+ const span = fakeSpan();
71
+ instrumentation._endSpan({
72
+ span,
73
+ type: 'completion',
74
+ result: {
75
+ usage: {
76
+ input_tokens: 10,
77
+ cache_creation_input_tokens: 5300,
78
+ cache_creation: { ephemeral_5m_input_tokens: 4000, ephemeral_1h_input_tokens: 1300 },
79
+ },
80
+ },
81
+ });
82
+
83
+ assert.deepEqual(JSON.parse(span.attrs['weflayr.cache.creation']), {
84
+ ephemeral_5m_input_tokens: 4000,
85
+ ephemeral_1h_input_tokens: 1300,
86
+ });
87
+ });
88
+
66
89
  test('sets nothing for an uncached call', () => {
67
90
  const instrumentation = { _endSpan() {} };
68
91
  new AnthropicPromptCachingInstrumentor().instrument(instrumentation);
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+
3
+ // Tests for the Anthropic request-side cache_control breakpoint capture.
4
+ //
5
+ // The traceloop instrumentation instance is stubbed with a `startSpan` that
6
+ // returns a fake span, so the wrap can be exercised without the real SDK.
7
+
8
+ const test = require('node:test');
9
+ const assert = require('node:assert/strict');
10
+
11
+ const {
12
+ AnthropicCacheConfigInstrumentor,
13
+ extractCacheBreakpoints,
14
+ } = require('../../../src/instrumentation/anthropic/get_cache_config_telemetry');
15
+
16
+ function makeFakeSpan() {
17
+ const attributes = {};
18
+ return {
19
+ attributes,
20
+ setAttribute(key, value) {
21
+ attributes[key] = value;
22
+ },
23
+ };
24
+ }
25
+
26
+ function makeStubInstrumentation() {
27
+ return {
28
+ startSpan() {
29
+ return makeFakeSpan();
30
+ },
31
+ };
32
+ }
33
+
34
+ test('stamps breakpoints from system, message content and tools', () => {
35
+ const instrumentation = makeStubInstrumentation();
36
+ new AnthropicCacheConfigInstrumentor().instrument(instrumentation);
37
+
38
+ const span = instrumentation.startSpan({
39
+ type: 'chat',
40
+ params: {
41
+ system: [{ type: 'text', text: 'big prefix', cache_control: { type: 'ephemeral' } }],
42
+ messages: [
43
+ { role: 'user', content: 'plain string content' },
44
+ {
45
+ role: 'user',
46
+ content: [{ type: 'text', text: 'doc', cache_control: { type: 'ephemeral', ttl: '1h' } }],
47
+ },
48
+ ],
49
+ tools: [
50
+ { name: 'get_weather', cache_control: { type: 'ephemeral' } },
51
+ { name: 'uncached_tool' },
52
+ ],
53
+ },
54
+ });
55
+
56
+ assert.deepEqual(JSON.parse(span.attributes['weflayr.cache.breakpoints']), [
57
+ { location: 'system' },
58
+ { location: 'messages[1]', ttl: '1h' },
59
+ { location: 'tools[0]' },
60
+ ]);
61
+ });
62
+
63
+ test('no attribute without breakpoints', () => {
64
+ const instrumentation = makeStubInstrumentation();
65
+ new AnthropicCacheConfigInstrumentor().instrument(instrumentation);
66
+
67
+ const span = instrumentation.startSpan({
68
+ type: 'chat',
69
+ params: { messages: [{ role: 'user', content: 'hello' }] },
70
+ });
71
+
72
+ assert.ok(!('weflayr.cache.breakpoints' in span.attributes));
73
+ });
74
+
75
+ test('extractCacheBreakpoints omits the ttl when the request sets none', () => {
76
+ assert.deepEqual(
77
+ extractCacheBreakpoints({
78
+ system: [{ type: 'text', text: 'p', cache_control: { type: 'ephemeral' } }],
79
+ }),
80
+ [{ location: 'system' }]
81
+ );
82
+ });
83
+
84
+ test('uninstrument restores the original startSpan', () => {
85
+ const instrumentation = makeStubInstrumentation();
86
+ const original = instrumentation.startSpan;
87
+ const instrumentor = new AnthropicCacheConfigInstrumentor();
88
+ instrumentor.instrument(instrumentation);
89
+ assert.notEqual(instrumentation.startSpan, original);
90
+ instrumentor.uninstrument();
91
+ assert.equal(instrumentation.startSpan, original);
92
+ });
@@ -52,6 +52,21 @@ test('stamps tool definitions from the request params', () => {
52
52
  assert.deepEqual(JSON.parse(span.attributes['gen_ai.tool.definitions']), [WEATHER_TOOL]);
53
53
  });
54
54
 
55
+ test('drops cache_control from the stamped tool definitions', () => {
56
+ const instrumentation = makeStubInstrumentation();
57
+ new AnthropicToolDefinitionsInstrumentor().instrument(instrumentation);
58
+
59
+ const span = instrumentation.startSpan({
60
+ type: 'chat',
61
+ params: {
62
+ model: 'claude-haiku-4-5',
63
+ tools: [{ ...WEATHER_TOOL, cache_control: { type: 'ephemeral', ttl: '1h' } }],
64
+ },
65
+ });
66
+
67
+ assert.deepEqual(JSON.parse(span.attributes['gen_ai.tool.definitions']), [WEATHER_TOOL]);
68
+ });
69
+
55
70
  test('no attribute when the request has no tools', () => {
56
71
  const instrumentation = makeStubInstrumentation();
57
72
  new AnthropicToolDefinitionsInstrumentor().instrument(instrumentation);
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+
3
+ // Tests for the Bedrock request-side cache breakpoint capture.
4
+ //
5
+ // The extractor reads both body schemas an InvokeModel request can carry:
6
+ // Anthropic-on-Bedrock `cache_control` blocks and Amazon Nova `cachePoint`
7
+ // blocks (attached to a content block or standing alone). The traceloop
8
+ // instrumentation instance is stubbed with a `_startSpan` returning a fake
9
+ // span, so the wrap can be exercised without the real SDK.
10
+
11
+ const test = require('node:test');
12
+ const assert = require('node:assert/strict');
13
+
14
+ const {
15
+ BedrockCacheConfigInstrumentor,
16
+ extractBedrockCacheBreakpoints,
17
+ } = require('../../../src/instrumentation/bedrock/get_cache_config_telemetry');
18
+
19
+ function makeFakeSpan() {
20
+ const attributes = {};
21
+ return {
22
+ attributes,
23
+ setAttribute(key, value) {
24
+ attributes[key] = value;
25
+ },
26
+ };
27
+ }
28
+
29
+ function makeStubInstrumentation() {
30
+ return {
31
+ _startSpan() {
32
+ return makeFakeSpan();
33
+ },
34
+ };
35
+ }
36
+
37
+ test('extracts nova cache points', () => {
38
+ const body = {
39
+ system: [{ text: 'sys' }, { cachePoint: { type: 'default' } }],
40
+ messages: [
41
+ {
42
+ role: 'user',
43
+ content: [
44
+ { text: 'big prefix', cachePoint: { type: 'default' } },
45
+ { text: 'Reply with OK.' },
46
+ ],
47
+ },
48
+ ],
49
+ toolConfig: {
50
+ tools: [
51
+ { toolSpec: { name: 'get_weather' } },
52
+ { cachePoint: { type: 'default', ttl: '1h' } },
53
+ ],
54
+ },
55
+ };
56
+ assert.deepEqual(extractBedrockCacheBreakpoints(body), [
57
+ { location: 'system' },
58
+ { location: 'messages[0]' },
59
+ { location: 'tools[1]', ttl: '1h' },
60
+ ]);
61
+ });
62
+
63
+ test('extracts anthropic-on-bedrock cache_control', () => {
64
+ const body = {
65
+ anthropic_version: 'bedrock-2023-05-31',
66
+ system: [{ type: 'text', text: 'big prefix', cache_control: { type: 'ephemeral', ttl: '1h' } }],
67
+ messages: [{ role: 'user', content: 'Reply with OK.' }],
68
+ };
69
+ assert.deepEqual(extractBedrockCacheBreakpoints(body), [{ location: 'system', ttl: '1h' }]);
70
+ });
71
+
72
+ test('no breakpoints without cache blocks', () => {
73
+ assert.deepEqual(
74
+ extractBedrockCacheBreakpoints({ messages: [{ role: 'user', content: 'hello' }] }),
75
+ []
76
+ );
77
+ });
78
+
79
+ test('stamps breakpoints from the request body', () => {
80
+ const instrumentation = makeStubInstrumentation();
81
+ new BedrockCacheConfigInstrumentor().instrument(instrumentation);
82
+
83
+ const span = instrumentation._startSpan({
84
+ params: {
85
+ input: {
86
+ modelId: 'amazon.nova-micro-v1:0',
87
+ body: JSON.stringify({
88
+ messages: [{ role: 'user', content: [{ text: 'p', cachePoint: { type: 'default' } }] }],
89
+ }),
90
+ },
91
+ },
92
+ });
93
+
94
+ assert.deepEqual(JSON.parse(span.attributes['weflayr.cache.breakpoints']), [
95
+ { location: 'messages[0]' },
96
+ ]);
97
+ });
98
+
99
+ test('uninstrument restores the original _startSpan', () => {
100
+ const instrumentation = makeStubInstrumentation();
101
+ const original = instrumentation._startSpan;
102
+ const instrumentor = new BedrockCacheConfigInstrumentor();
103
+ instrumentor.instrument(instrumentation);
104
+ assert.notEqual(instrumentation._startSpan, original);
105
+ instrumentor.uninstrument();
106
+ assert.equal(instrumentation._startSpan, original);
107
+ });
@@ -32,6 +32,22 @@ function passthroughInstrumentation() {
32
32
  return { _wrapPromise: (_span, promise) => Promise.resolve(promise) };
33
33
  }
34
34
 
35
+ test('stamps cache write from an anthropic-on-bedrock usage shape', () => {
36
+ const instrumentation = { _wrapPromise: (span, promise) => promise };
37
+ new BedrockPromptCachingInstrumentor().instrument(instrumentation);
38
+
39
+ const span = fakeSpan();
40
+ const body = new TextEncoder().encode(
41
+ JSON.stringify({
42
+ usage: { input_tokens: 10, cache_creation_input_tokens: 5300, cache_read_input_tokens: 0 },
43
+ })
44
+ );
45
+ return instrumentation._wrapPromise(span, Promise.resolve({ body })).then(() => {
46
+ assert.equal(span.attrs['gen_ai.usage.cache_creation_input_tokens'], 5300);
47
+ assert.equal(span.attrs['gen_ai.usage.cache_read_input_tokens'], undefined);
48
+ });
49
+ });
50
+
35
51
  test('stamps cache read/write from a buffered InvokeModel body', async () => {
36
52
  const instrumentation = passthroughInstrumentation();
37
53
  new BedrockPromptCachingInstrumentor().instrument(instrumentation);
@@ -0,0 +1,130 @@
1
+ 'use strict';
2
+
3
+ // Tests for the Google GenAI explicit-cache lifecycle telemetry.
4
+ //
5
+ // The @google/genai `Caches` class is stubbed with prototype
6
+ // create/update/delete methods mirroring the SDK shape.
7
+ // `instrumentCacheOperations(Caches, patched)` wraps them and pushes the
8
+ // restore tuples onto `patched`.
9
+
10
+ const test = require('node:test');
11
+ const assert = require('node:assert/strict');
12
+
13
+ const { trace, SpanStatusCode } = require('@opentelemetry/api');
14
+ const {
15
+ BasicTracerProvider,
16
+ InMemorySpanExporter,
17
+ SimpleSpanProcessor,
18
+ } = require('@opentelemetry/sdk-trace-base');
19
+
20
+ const {
21
+ instrumentCacheOperations,
22
+ } = require('../../../src/instrumentation/google/get_cache_operations_telemetry');
23
+
24
+ const exporter = new InMemorySpanExporter();
25
+ trace.setGlobalTracerProvider(
26
+ new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(exporter)] })
27
+ );
28
+
29
+ const CACHED_CONTENT = {
30
+ name: 'cachedContents/abc123',
31
+ expireTime: '2026-07-16T14:00:00Z',
32
+ usageMetadata: { totalTokenCount: 5300 },
33
+ };
34
+
35
+ class StubCaches {
36
+ create(params = {}) {
37
+ if (params.model === 'boom') return Promise.reject(new Error('api error'));
38
+ return Promise.resolve(CACHED_CONTENT);
39
+ }
40
+
41
+ update() {
42
+ return Promise.resolve(CACHED_CONTENT);
43
+ }
44
+
45
+ delete() {
46
+ return Promise.resolve({});
47
+ }
48
+ }
49
+
50
+ function instrumented() {
51
+ const patched = [];
52
+ instrumentCacheOperations(StubCaches, patched);
53
+ return () => {
54
+ for (const [owner, methodName, original] of patched) owner[methodName] = original;
55
+ exporter.reset();
56
+ };
57
+ }
58
+
59
+ test('create span carries cache facts and content', async () => {
60
+ const teardown = instrumented();
61
+ try {
62
+ await new StubCaches().create({
63
+ model: 'models/gemini-2.5-flash',
64
+ config: {
65
+ contents: ['a very large document to cache'],
66
+ systemInstruction: 'Answer from the document.',
67
+ ttl: '3600s',
68
+ },
69
+ });
70
+
71
+ const [span] = exporter.getFinishedSpans();
72
+ assert.equal(span.name, 'cache_create');
73
+ assert.equal(span.attributes['gen_ai.system'], 'gemini');
74
+ assert.equal(span.attributes['gen_ai.request.model'], 'gemini-2.5-flash');
75
+ assert.equal(span.attributes['weflayr.cache.operation'], 'create');
76
+ assert.equal(span.attributes['weflayr.cache.cached_content_id'], 'cachedContents/abc123');
77
+ assert.equal(span.attributes['weflayr.cache.expire_time'], '2026-07-16T14:00:00Z');
78
+ assert.equal(span.attributes['gen_ai.usage.input_tokens'], 5300);
79
+ assert.deepEqual(JSON.parse(span.attributes['gen_ai.input.messages']), [
80
+ { role: 'user', parts: [{ type: 'text', content: 'a very large document to cache' }] },
81
+ ]);
82
+ assert.deepEqual(JSON.parse(span.attributes['gen_ai.system_instructions']), [
83
+ { type: 'text', content: 'Answer from the document.' },
84
+ ]);
85
+ } finally {
86
+ teardown();
87
+ }
88
+ });
89
+
90
+ test('update span carries the new expiry but no token count', async () => {
91
+ const teardown = instrumented();
92
+ try {
93
+ await new StubCaches().update({ name: 'cachedContents/abc123', config: { ttl: '180s' } });
94
+
95
+ const [span] = exporter.getFinishedSpans();
96
+ assert.equal(span.name, 'cache_update');
97
+ assert.equal(span.attributes['weflayr.cache.operation'], 'update');
98
+ assert.equal(span.attributes['weflayr.cache.expire_time'], '2026-07-16T14:00:00Z');
99
+ assert.ok(!('gen_ai.usage.input_tokens' in span.attributes));
100
+ } finally {
101
+ teardown();
102
+ }
103
+ });
104
+
105
+ test('delete span carries operation and cache id', async () => {
106
+ const teardown = instrumented();
107
+ try {
108
+ await new StubCaches().delete({ name: 'cachedContents/abc123' });
109
+
110
+ const [span] = exporter.getFinishedSpans();
111
+ assert.equal(span.name, 'cache_delete');
112
+ assert.equal(span.attributes['weflayr.cache.operation'], 'delete');
113
+ assert.equal(span.attributes['weflayr.cache.cached_content_id'], 'cachedContents/abc123');
114
+ assert.ok(!('gen_ai.usage.input_tokens' in span.attributes));
115
+ } finally {
116
+ teardown();
117
+ }
118
+ });
119
+
120
+ test('error span records status', async () => {
121
+ const teardown = instrumented();
122
+ try {
123
+ await assert.rejects(new StubCaches().create({ model: 'boom' }));
124
+
125
+ const [span] = exporter.getFinishedSpans();
126
+ assert.equal(span.status.code, SpanStatusCode.ERROR);
127
+ } finally {
128
+ teardown();
129
+ }
130
+ });
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ // Tests for the OpenAI request-side prompt-cache param capture.
4
+ //
5
+ // The traceloop instrumentation instance is stubbed with a `startSpan` that
6
+ // returns a fake span, so the wrap can be exercised without the real SDK.
7
+
8
+ const test = require('node:test');
9
+ const assert = require('node:assert/strict');
10
+
11
+ const {
12
+ OpenAICacheConfigInstrumentor,
13
+ } = require('../../../src/instrumentation/openai/get_cache_config_telemetry');
14
+
15
+ function makeFakeSpan() {
16
+ const attributes = {};
17
+ return {
18
+ attributes,
19
+ setAttribute(key, value) {
20
+ attributes[key] = value;
21
+ },
22
+ };
23
+ }
24
+
25
+ function makeStubInstrumentation() {
26
+ return {
27
+ startSpan() {
28
+ return makeFakeSpan();
29
+ },
30
+ };
31
+ }
32
+
33
+ test('stamps prompt cache key and retention from the request params', () => {
34
+ const instrumentation = makeStubInstrumentation();
35
+ new OpenAICacheConfigInstrumentor().instrument(instrumentation);
36
+
37
+ const span = instrumentation.startSpan({
38
+ type: 'chat',
39
+ params: {
40
+ model: 'gpt-4o-mini',
41
+ prompt_cache_key: 'session-42',
42
+ prompt_cache_retention: '24h',
43
+ },
44
+ });
45
+
46
+ assert.equal(span.attributes['weflayr.cache.key'], 'session-42');
47
+ assert.equal(span.attributes['weflayr.cache.retention'], '24h');
48
+ });
49
+
50
+ test('stamps prompt cache options ttl as the retention', () => {
51
+ const instrumentation = makeStubInstrumentation();
52
+ new OpenAICacheConfigInstrumentor().instrument(instrumentation);
53
+
54
+ const span = instrumentation.startSpan({
55
+ type: 'chat',
56
+ params: {
57
+ model: 'gpt-5.6',
58
+ prompt_cache_options: { ttl: '30m' },
59
+ },
60
+ });
61
+
62
+ assert.equal(span.attributes['weflayr.cache.retention'], '30m');
63
+ });
64
+
65
+ test('no attributes without cache params', () => {
66
+ const instrumentation = makeStubInstrumentation();
67
+ new OpenAICacheConfigInstrumentor().instrument(instrumentation);
68
+
69
+ const span = instrumentation.startSpan({ type: 'chat', params: { model: 'gpt-4o-mini' } });
70
+
71
+ assert.ok(!('weflayr.cache.key' in span.attributes));
72
+ assert.ok(!('weflayr.cache.retention' in span.attributes));
73
+ });
74
+
75
+ test('uninstrument restores the original startSpan', () => {
76
+ const instrumentation = makeStubInstrumentation();
77
+ const original = instrumentation.startSpan;
78
+ const instrumentor = new OpenAICacheConfigInstrumentor();
79
+ instrumentor.instrument(instrumentation);
80
+ assert.notEqual(instrumentation.startSpan, original);
81
+ instrumentor.uninstrument();
82
+ assert.equal(instrumentation.startSpan, original);
83
+ });