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.
- package/dist/api/generated/facade.gen.d.ts +77 -0
- package/dist/api/generated/facade.gen.js +7 -1
- package/dist/api/generated/index.d.ts +2 -2
- package/dist/api/generated/index.js +7 -1
- package/dist/api/generated/sdk.gen.d.ts +25 -1
- package/dist/api/generated/sdk.gen.js +55 -1
- package/dist/api/generated/types.gen.d.ts +256 -0
- package/index.d.ts +5 -2
- package/package.json +1 -1
- package/src/api/generated/facade.gen.ts +36 -0
- package/src/api/generated/index.ts +2 -2
- package/src/api/generated/sdk.gen.ts +55 -1
- package/src/api/generated/types.gen.ts +293 -0
- package/src/auto-instrument.js +1 -0
- package/src/instrumentation/anthropic/get_cache_and_reasoning_telemetry.js +11 -0
- package/src/instrumentation/anthropic/get_cache_config_telemetry.js +90 -0
- package/src/instrumentation/anthropic/get_tool_definitions_telemetry.js +6 -1
- package/src/instrumentation/anthropic/set_telemetry_attributes.js +2 -0
- package/src/instrumentation/bedrock/get_cache_config_telemetry.js +106 -0
- package/src/instrumentation/bedrock/get_cache_telemetry.js +11 -6
- package/src/instrumentation/bedrock/set_telemetry_attributes.js +6 -2
- package/src/instrumentation/google/get_cache_operations_telemetry.js +171 -0
- package/src/instrumentation/google/get_cache_telemetry.js +16 -2
- package/src/instrumentation/google/set_telemetry_attributes.js +8 -2
- package/src/instrumentation/openai/get_cache_config_telemetry.js +63 -0
- package/src/instrumentation/openai/set_telemetry_attributes.js +2 -0
- package/src/instrumentation/vercel/full_instrumentation.js +10 -2
- package/src/instrumentation/vercel/get_cache_creation_telemetry.js +70 -0
- package/src/otel/propagation.js +22 -9
- package/src/otel/span-filter.js +12 -12
- package/src/otel/span-processor.js +8 -0
- package/src/otel/vercel-ai-sdk-span-filter.js +1 -1
- package/tests/index.test.js +67 -7
- package/tests/instrumentation/anthropic/get_cache_and_reasoning_telemetry.test.js +23 -0
- package/tests/instrumentation/anthropic/get_cache_config_telemetry.test.js +92 -0
- package/tests/instrumentation/anthropic/get_tool_definitions_telemetry.test.js +15 -0
- package/tests/instrumentation/bedrock/get_cache_config_telemetry.test.js +107 -0
- package/tests/instrumentation/bedrock/get_cache_telemetry.test.js +16 -0
- package/tests/instrumentation/google/get_cache_operations_telemetry.test.js +130 -0
- package/tests/instrumentation/openai/get_cache_config_telemetry.test.js +83 -0
- package/tests/instrumentation/vercel/get_cache_creation_telemetry.test.js +96 -0
package/tests/index.test.js
CHANGED
|
@@ -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
|
|
237
|
+
test('isDefaultExportSpan accepts spans from known instrumentation scopes', () => {
|
|
238
238
|
const span = makeFakeSpan({
|
|
239
|
-
attributes: {
|
|
240
|
-
instrumentationScope: { name: '
|
|
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
|
|
245
|
+
test('isDefaultExportSpan accepts weflayr instrumentation spans', () => {
|
|
246
246
|
const span = makeFakeSpan({
|
|
247
247
|
attributes: {},
|
|
248
|
-
instrumentationScope: { name: '
|
|
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('
|
|
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
|
|
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
|
+
});
|