weflayr 0.20.6 → 0.22.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 (49) hide show
  1. package/index.d.ts +1 -1
  2. package/package.json +2 -2
  3. package/src/auto-instrument.js +64 -31
  4. package/src/instrumentation/anthropic/get_cache_and_reasoning_telemetry.js +107 -0
  5. package/src/instrumentation/anthropic/get_streaming_stop_reason_telemetry.js +121 -0
  6. package/src/instrumentation/anthropic/get_tool_definitions_telemetry.js +50 -0
  7. package/src/instrumentation/anthropic/set_telemetry_attributes.js +37 -0
  8. package/src/instrumentation/bedrock/get_cache_telemetry.js +104 -0
  9. package/src/instrumentation/bedrock/get_input_message_content_telemetry.js +94 -0
  10. package/src/instrumentation/bedrock/get_streaming_output_message_telemetry.js +87 -0
  11. package/src/instrumentation/bedrock/set_telemetry_attributes.js +36 -0
  12. package/src/instrumentation/cohere/get_chat_telemetry.js +88 -0
  13. package/src/instrumentation/cohere/get_embeddings_telemetry.js +107 -0
  14. package/src/instrumentation/cohere/set_telemetry_attributes.js +32 -0
  15. package/src/instrumentation/google/get_audio_telemetry.js +30 -0
  16. package/src/instrumentation/google/get_cache_telemetry.js +25 -0
  17. package/src/instrumentation/google/get_embeddings_telemetry.js +99 -0
  18. package/src/instrumentation/google/get_provider_name.js +25 -0
  19. package/src/instrumentation/google/get_reasoning_telemetry.js +21 -0
  20. package/src/instrumentation/google/set_telemetry_attributes.js +98 -0
  21. package/src/instrumentation/openai/get_audio_telemetry.js +149 -0
  22. package/src/instrumentation/openai/get_cache_and_reasoning_telemetry.js +71 -0
  23. package/src/instrumentation/openai/get_embeddings_telemetry.js +107 -0
  24. package/src/instrumentation/openai/get_streaming_usage.js +81 -0
  25. package/src/instrumentation/openai/set_telemetry_attributes.js +39 -0
  26. package/src/otel/exporter.js +1 -6
  27. package/src/otel/propagation.js +5 -0
  28. package/src/otel/span-filter.js +0 -5
  29. package/src/otel/span-processor.js +30 -1
  30. package/src/setup.js +1 -2
  31. package/tests/index.test.js +144 -44
  32. package/tests/instrumentation/anthropic/get_cache_and_reasoning_telemetry.test.js +158 -0
  33. package/tests/instrumentation/anthropic/get_streaming_stop_reason_telemetry.test.js +117 -0
  34. package/tests/instrumentation/anthropic/get_tool_definitions_telemetry.test.js +82 -0
  35. package/tests/instrumentation/bedrock/get_cache_telemetry.test.js +102 -0
  36. package/tests/instrumentation/bedrock/get_input_message_content_telemetry.test.js +89 -0
  37. package/tests/instrumentation/bedrock/get_streaming_output_message_telemetry.test.js +87 -0
  38. package/tests/instrumentation/cohere/get_chat_telemetry.test.js +148 -0
  39. package/tests/instrumentation/cohere/get_embeddings_telemetry.test.js +132 -0
  40. package/tests/instrumentation/{elevenlabs.test.js → elevenlabs/full_instrumentation.test.js} +21 -5
  41. package/tests/instrumentation/google/get_embeddings_telemetry.test.js +128 -0
  42. package/tests/instrumentation/google/get_reasoning_telemetry.test.js +41 -0
  43. package/tests/instrumentation/google/set_telemetry_attributes.test.js +219 -0
  44. package/tests/instrumentation/openai/get_audio_telemetry.test.js +170 -0
  45. package/tests/instrumentation/openai/get_cache_and_reasoning_telemetry.test.js +83 -0
  46. package/tests/instrumentation/openai/get_embeddings_telemetry.test.js +145 -0
  47. package/tests/instrumentation/openai/get_streaming_usage.test.js +105 -0
  48. package/src/otel/openai-streaming-usage.js +0 -60
  49. /package/src/instrumentation/{elevenlabs.js → elevenlabs/full_instrumentation.js} +0 -0
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ // Tests for the Bedrock cached-prompt token complement. It wraps the primary
4
+ // instrumentor's `_wrapPromise(span, promise, modelId)` to read Nova's cache-token
5
+ // counts off the decoded InvokeModel response body and stamp them as
6
+ // gen_ai.usage.cache_* attributes.
7
+
8
+ const test = require('node:test');
9
+ const assert = require('node:assert/strict');
10
+
11
+ const {
12
+ BedrockPromptCachingInstrumentor,
13
+ } = require('../../../src/instrumentation/bedrock/get_cache_telemetry');
14
+
15
+ function fakeSpan() {
16
+ const attrs = {};
17
+ return {
18
+ attrs,
19
+ isRecording: () => true,
20
+ setAttribute(key, value) {
21
+ attrs[key] = value;
22
+ },
23
+ };
24
+ }
25
+
26
+ function encode(obj) {
27
+ return new TextEncoder().encode(JSON.stringify(obj));
28
+ }
29
+
30
+ // Pass-through original: resolve to whatever promise it was handed.
31
+ function passthroughInstrumentation() {
32
+ return { _wrapPromise: (_span, promise) => Promise.resolve(promise) };
33
+ }
34
+
35
+ test('stamps cache read/write from a buffered InvokeModel body', async () => {
36
+ const instrumentation = passthroughInstrumentation();
37
+ new BedrockPromptCachingInstrumentor().instrument(instrumentation);
38
+
39
+ const span = fakeSpan();
40
+ const body = encode({
41
+ usage: { cacheReadInputTokenCount: 100, cacheWriteInputTokenCount: 1300 },
42
+ });
43
+ await instrumentation._wrapPromise(span, Promise.resolve({ body }), 'amazon.nova-micro-v1:0');
44
+
45
+ assert.equal(span.attrs['gen_ai.usage.cache_read_input_tokens'], 100);
46
+ assert.equal(span.attrs['gen_ai.usage.cache_creation_input_tokens'], 1300);
47
+ });
48
+
49
+ test('sets nothing when cache counts are zero', async () => {
50
+ const instrumentation = passthroughInstrumentation();
51
+ new BedrockPromptCachingInstrumentor().instrument(instrumentation);
52
+
53
+ const span = fakeSpan();
54
+ const body = encode({ usage: { cacheReadInputTokenCount: 0, cacheWriteInputTokenCount: 0 } });
55
+ await instrumentation._wrapPromise(span, Promise.resolve({ body }), 'm');
56
+
57
+ assert.equal(span.attrs['gen_ai.usage.cache_read_input_tokens'], undefined);
58
+ assert.equal(span.attrs['gen_ai.usage.cache_creation_input_tokens'], undefined);
59
+ });
60
+
61
+ test('skips a streamed (async-iterable) body without consuming it', async () => {
62
+ const instrumentation = passthroughInstrumentation();
63
+ new BedrockPromptCachingInstrumentor().instrument(instrumentation);
64
+
65
+ const span = fakeSpan();
66
+ let consumed = false;
67
+ const body = (async function* () {
68
+ consumed = true;
69
+ yield {};
70
+ })();
71
+ await instrumentation._wrapPromise(span, Promise.resolve({ body }), 'm');
72
+
73
+ assert.equal(span.attrs['gen_ai.usage.cache_read_input_tokens'], undefined);
74
+ assert.equal(consumed, false);
75
+ });
76
+
77
+ test('stamps cache from the streamed metadata chunk and calls the original', () => {
78
+ const calls = [];
79
+ const instrumentation = {
80
+ _handleNovaStreamingMetadata(span, parsed) {
81
+ calls.push(parsed);
82
+ },
83
+ };
84
+ new BedrockPromptCachingInstrumentor().instrument(instrumentation);
85
+
86
+ const span = fakeSpan();
87
+ instrumentation._handleNovaStreamingMetadata(span, {
88
+ metadata: {
89
+ usage: { inputTokens: 3, cacheReadInputTokenCount: 1458, cacheWriteInputTokenCount: 0 },
90
+ },
91
+ });
92
+
93
+ assert.equal(span.attrs['gen_ai.usage.cache_read_input_tokens'], 1458);
94
+ assert.equal(span.attrs['gen_ai.usage.cache_creation_input_tokens'], undefined);
95
+ assert.equal(calls.length, 1);
96
+ });
97
+
98
+ test('is a no-op when the instrumentor has no _wrapPromise', () => {
99
+ const instrumentation = {};
100
+ new BedrockPromptCachingInstrumentor().instrument(instrumentation);
101
+ assert.equal(instrumentation._wrapPromise, undefined);
102
+ });
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ // Tests for the Bedrock input-message-content complement.
4
+ //
5
+ // The traceloop instrumentation instance is stubbed with a `_startSpan` that
6
+ // returns a fake span — with or without instrumentor-set input messages — so
7
+ // the wrap's "only when missing" behaviour can be exercised.
8
+
9
+ const test = require('node:test');
10
+ const assert = require('node:assert/strict');
11
+
12
+ const {
13
+ BedrockInputMessageContentInstrumentor,
14
+ } = require('../../../src/instrumentation/bedrock/get_input_message_content_telemetry');
15
+
16
+ function makeFakeSpan(attributes = {}) {
17
+ return {
18
+ attributes,
19
+ setAttribute(key, value) {
20
+ attributes[key] = value;
21
+ },
22
+ };
23
+ }
24
+
25
+ function makeStubInstrumentation(spanAttributes) {
26
+ return {
27
+ _startSpan() {
28
+ return makeFakeSpan(spanAttributes);
29
+ },
30
+ };
31
+ }
32
+
33
+ function request(body) {
34
+ return { params: { input: { modelId: 'model-that-does-not-exist', body } } };
35
+ }
36
+
37
+ test('stamps input messages when the instrumentor set none', () => {
38
+ const instrumentation = makeStubInstrumentation({});
39
+ new BedrockInputMessageContentInstrumentor().instrument(instrumentation);
40
+
41
+ const span = instrumentation._startSpan(
42
+ request(JSON.stringify({ messages: [{ role: 'user', content: [{ text: 'Hello' }] }] }))
43
+ );
44
+
45
+ assert.deepEqual(JSON.parse(span.attributes['gen_ai.input.messages']), [
46
+ { role: 'user', parts: [{ type: 'text', content: 'Hello' }] },
47
+ ]);
48
+ });
49
+
50
+ test('supports the flat inputText schema', () => {
51
+ const instrumentation = makeStubInstrumentation({});
52
+ new BedrockInputMessageContentInstrumentor().instrument(instrumentation);
53
+
54
+ const span = instrumentation._startSpan(request(JSON.stringify({ inputText: 'Hello' })));
55
+
56
+ assert.deepEqual(JSON.parse(span.attributes['gen_ai.input.messages']), [
57
+ { role: 'user', parts: [{ type: 'text', content: 'Hello' }] },
58
+ ]);
59
+ });
60
+
61
+ test('leaves instrumentor-set input messages untouched', () => {
62
+ const existing = '[{"role":"user","parts":[{"type":"text","content":"from-traceloop"}]}]';
63
+ const instrumentation = makeStubInstrumentation({ 'gen_ai.input.messages': existing });
64
+ new BedrockInputMessageContentInstrumentor().instrument(instrumentation);
65
+
66
+ const span = instrumentation._startSpan(
67
+ request(JSON.stringify({ messages: [{ role: 'user', content: [{ text: 'other' }] }] }))
68
+ );
69
+
70
+ assert.equal(span.attributes['gen_ai.input.messages'], existing);
71
+ });
72
+
73
+ test('no attribute for an unparseable or empty body', () => {
74
+ const instrumentation = makeStubInstrumentation({});
75
+ new BedrockInputMessageContentInstrumentor().instrument(instrumentation);
76
+
77
+ assert.ok(
78
+ !('gen_ai.input.messages' in instrumentation._startSpan(request('not-json')).attributes)
79
+ );
80
+ });
81
+
82
+ test('uninstrument restores the original startSpan', () => {
83
+ const instrumentation = makeStubInstrumentation({});
84
+ const original = instrumentation._startSpan;
85
+ const instrumentor = new BedrockInputMessageContentInstrumentor();
86
+ instrumentor.instrument(instrumentation);
87
+ instrumentor.uninstrument();
88
+ assert.equal(instrumentation._startSpan, original);
89
+ });
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ // Tests for the Bedrock streamed-output message complement.
4
+ //
5
+ // The traceloop instrumentation instance is stubbed with a
6
+ // `_handleNovaStreamingMetadata` no-op; chunks are replayed through the wrap the
7
+ // way the instrumentation's stream loop calls it.
8
+
9
+ const test = require('node:test');
10
+ const assert = require('node:assert/strict');
11
+
12
+ const {
13
+ BedrockStreamingOutputMessageInstrumentor,
14
+ } = require('../../../src/instrumentation/bedrock/get_streaming_output_message_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 instrumented() {
27
+ const instrumentation = { _handleNovaStreamingMetadata() {} };
28
+ const instrumentor = new BedrockStreamingOutputMessageInstrumentor();
29
+ instrumentor.instrument(instrumentation);
30
+ return { instrumentation, instrumentor };
31
+ }
32
+
33
+ test('re-stamps the output message from accumulated stream chunks', () => {
34
+ const { instrumentation } = instrumented();
35
+ const span = makeFakeSpan();
36
+
37
+ instrumentation._handleNovaStreamingMetadata(span, {
38
+ contentBlockDelta: { delta: { text: 'Hel' } },
39
+ });
40
+ instrumentation._handleNovaStreamingMetadata(span, {
41
+ contentBlockDelta: { delta: { text: 'lo!' } },
42
+ });
43
+ instrumentation._handleNovaStreamingMetadata(span, {
44
+ messageStop: { stopReason: 'end_turn' },
45
+ });
46
+ assert.ok(!('gen_ai.output.messages' in span.attributes), 'set only on the metadata chunk');
47
+
48
+ instrumentation._handleNovaStreamingMetadata(span, { metadata: { usage: {} } });
49
+ assert.deepEqual(JSON.parse(span.attributes['gen_ai.output.messages']), [
50
+ {
51
+ role: 'assistant',
52
+ parts: [{ type: 'text', content: 'Hello!' }],
53
+ finish_reason: 'stop',
54
+ },
55
+ ]);
56
+ });
57
+
58
+ test('maps max_tokens to length', () => {
59
+ const { instrumentation } = instrumented();
60
+ const span = makeFakeSpan();
61
+ instrumentation._handleNovaStreamingMetadata(span, {
62
+ contentBlockDelta: { delta: { text: 'OK' } },
63
+ });
64
+ instrumentation._handleNovaStreamingMetadata(span, {
65
+ messageStop: { stopReason: 'max_tokens' },
66
+ });
67
+ instrumentation._handleNovaStreamingMetadata(span, { metadata: {} });
68
+ assert.equal(JSON.parse(span.attributes['gen_ai.output.messages'])[0].finish_reason, 'length');
69
+ });
70
+
71
+ test('no attribute when no text was streamed', () => {
72
+ const { instrumentation } = instrumented();
73
+ const span = makeFakeSpan();
74
+ instrumentation._handleNovaStreamingMetadata(span, {
75
+ messageStop: { stopReason: 'end_turn' },
76
+ });
77
+ instrumentation._handleNovaStreamingMetadata(span, { metadata: {} });
78
+ assert.ok(!('gen_ai.output.messages' in span.attributes));
79
+ });
80
+
81
+ test('uninstrument restores the original handler', () => {
82
+ const { instrumentation, instrumentor } = instrumented();
83
+ instrumentor.uninstrument();
84
+ const span = makeFakeSpan();
85
+ instrumentation._handleNovaStreamingMetadata(span, { metadata: {} });
86
+ assert.deepEqual(span.attributes, {});
87
+ });
@@ -0,0 +1,148 @@
1
+ 'use strict';
2
+
3
+ // Tests for the Cohere chat token-usage enrichment complement.
4
+ //
5
+ // The cohere-ai v1 `CohereClient` is stubbed with an internal `__chat` method
6
+ // that mirrors the SDK shape — resolving to `{ data: { meta: { billedUnits } } }`
7
+ // — so the complement can be exercised without network access or the dependency
8
+ // installed. The complement enriches whichever span is active when `__chat` runs
9
+ // (in production, the span traceloop opens around the public `chat`), so the
10
+ // tests start a span and make it active before invoking `__chat`.
11
+
12
+ const test = require('node:test');
13
+ const assert = require('node:assert/strict');
14
+ const Module = require('node:module');
15
+
16
+ const { trace, context } = require('@opentelemetry/api');
17
+ const {
18
+ BasicTracerProvider,
19
+ InMemorySpanExporter,
20
+ SimpleSpanProcessor,
21
+ } = require('@opentelemetry/sdk-trace-base');
22
+ const { AsyncLocalStorageContextManager } = require('@opentelemetry/context-async-hooks');
23
+
24
+ const {
25
+ CohereChatTokenUsageInstrumentor,
26
+ } = require('../../../src/instrumentation/cohere/get_chat_telemetry');
27
+
28
+ const CLIENT_MODULE = 'cohere-ai/Client';
29
+
30
+ const exporter = new InMemorySpanExporter();
31
+ trace.setGlobalTracerProvider(
32
+ new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(exporter)] })
33
+ );
34
+ context.setGlobalContextManager(new AsyncLocalStorageContextManager().enable());
35
+ const tracer = trace.getTracer('test');
36
+
37
+ class StubCohereClient {
38
+ __chat(request = {}) {
39
+ if (request.model === 'boom') return Promise.reject(new Error('api error'));
40
+ return Promise.resolve({
41
+ data: { text: 'hi', meta: { billedUnits: { inputTokens: 7, outputTokens: 3 } } },
42
+ });
43
+ }
44
+ }
45
+
46
+ function instrumented() {
47
+ const stubs = { [CLIENT_MODULE]: { CohereClient: StubCohereClient } };
48
+ const origResolve = Module._resolveFilename;
49
+ Module._resolveFilename = function patched(request, ...rest) {
50
+ if (request in stubs) return request;
51
+ return origResolve.call(this, request, ...rest);
52
+ };
53
+ for (const [moduleId, exports] of Object.entries(stubs)) {
54
+ require.cache[moduleId] = { id: moduleId, filename: moduleId, loaded: true, exports };
55
+ }
56
+
57
+ const instrumentor = new CohereChatTokenUsageInstrumentor();
58
+ instrumentor.instrument();
59
+
60
+ return () => {
61
+ instrumentor.uninstrument();
62
+ Module._resolveFilename = origResolve;
63
+ for (const moduleId of Object.keys(stubs)) delete require.cache[moduleId];
64
+ exporter.reset();
65
+ };
66
+ }
67
+
68
+ // Runs `__chat` inside an active span, mirroring the context traceloop sets up
69
+ // around the public `chat`, and returns the finished span.
70
+ async function callChatWithinSpan(client, request) {
71
+ const span = tracer.startSpan('cohere.chat');
72
+ await context.with(trace.setSpan(context.active(), span), () => client.__chat(request));
73
+ span.end();
74
+ return exporter.getFinishedSpans().find((finished) => finished.name === 'cohere.chat');
75
+ }
76
+
77
+ test('enriches the active chat span with input and output tokens', async () => {
78
+ const teardown = instrumented();
79
+ try {
80
+ const client = new StubCohereClient();
81
+ const span = await callChatWithinSpan(client, { model: 'command-r-08-2024', message: 'hi' });
82
+
83
+ assert.equal(span.attributes['gen_ai.usage.input_tokens'], 7);
84
+ assert.equal(span.attributes['gen_ai.usage.output_tokens'], 3);
85
+ } finally {
86
+ teardown();
87
+ }
88
+ });
89
+
90
+ test('leaves the response unchanged', async () => {
91
+ const teardown = instrumented();
92
+ try {
93
+ const client = new StubCohereClient();
94
+ let result;
95
+ const span = tracer.startSpan('cohere.chat');
96
+ await context.with(trace.setSpan(context.active(), span), async () => {
97
+ result = await client.__chat({ model: 'command-r-08-2024', message: 'hi' });
98
+ });
99
+ span.end();
100
+
101
+ assert.equal(result.data.text, 'hi');
102
+ assert.equal(result.data.meta.billedUnits.inputTokens, 7);
103
+ } finally {
104
+ teardown();
105
+ }
106
+ });
107
+
108
+ test('does nothing when no span is active', async () => {
109
+ const teardown = instrumented();
110
+ try {
111
+ const client = new StubCohereClient();
112
+ // No active span — must not throw, just pass the response through.
113
+ const result = await client.__chat({ model: 'command-r-08-2024', message: 'hi' });
114
+ assert.equal(result.data.meta.billedUnits.outputTokens, 3);
115
+ assert.equal(exporter.getFinishedSpans().length, 0);
116
+ } finally {
117
+ teardown();
118
+ }
119
+ });
120
+
121
+ test('propagates errors without enriching the span', async () => {
122
+ const teardown = instrumented();
123
+ try {
124
+ const client = new StubCohereClient();
125
+ const span = tracer.startSpan('cohere.chat');
126
+ await assert.rejects(
127
+ () =>
128
+ context.with(trace.setSpan(context.active(), span), () =>
129
+ client.__chat({ model: 'boom', message: 'x' })
130
+ ),
131
+ /api error/
132
+ );
133
+ span.end();
134
+
135
+ const finished = exporter.getFinishedSpans().find((each) => each.name === 'cohere.chat');
136
+ assert.equal(finished.attributes['gen_ai.usage.input_tokens'], undefined);
137
+ } finally {
138
+ teardown();
139
+ }
140
+ });
141
+
142
+ test('uninstrument restores the original method', async () => {
143
+ const teardown = instrumented();
144
+ const { CohereClient } = require(CLIENT_MODULE);
145
+ const wrapped = CohereClient.prototype.__chat;
146
+ teardown();
147
+ assert.notEqual(CohereClient.prototype.__chat, wrapped);
148
+ });
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+
3
+ // Tests for the bundled Cohere embeddings instrumentor.
4
+ //
5
+ // The cohere-ai v1 `CohereClient` and v2 `V2Client` classes are stubbed with an
6
+ // `embed` method that mirrors the SDK shape — resolving to a response carrying
7
+ // `meta.billedUnits.inputTokens` — so the instrumentor can be exercised without
8
+ // network access or the dependency installed.
9
+
10
+ const test = require('node:test');
11
+ const assert = require('node:assert/strict');
12
+ const Module = require('node:module');
13
+
14
+ const { trace, SpanStatusCode } = require('@opentelemetry/api');
15
+ const {
16
+ BasicTracerProvider,
17
+ InMemorySpanExporter,
18
+ SimpleSpanProcessor,
19
+ } = require('@opentelemetry/sdk-trace-base');
20
+
21
+ const {
22
+ CohereEmbeddingsInstrumentor,
23
+ } = require('../../../src/instrumentation/cohere/get_embeddings_telemetry');
24
+
25
+ const V1_MODULE = 'cohere-ai/Client';
26
+ const V2_MODULE = 'cohere-ai/api/resources/v2/client/Client';
27
+
28
+ const exporter = new InMemorySpanExporter();
29
+ trace.setGlobalTracerProvider(
30
+ new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(exporter)] })
31
+ );
32
+
33
+ class StubCohereClient {
34
+ embed(request = {}) {
35
+ if (request.model === 'boom') return Promise.reject(new Error('api error'));
36
+ return Promise.resolve({ embeddings: [[0.1]], meta: { billedUnits: { inputTokens: 8 } } });
37
+ }
38
+ }
39
+
40
+ class StubV2Client {
41
+ embed(_request = {}) {
42
+ return Promise.resolve({
43
+ embeddings: { float: [[0.2]] },
44
+ meta: { billedUnits: { inputTokens: 5 } },
45
+ });
46
+ }
47
+ }
48
+
49
+ function instrumented() {
50
+ const stubs = {
51
+ [V1_MODULE]: { CohereClient: StubCohereClient },
52
+ [V2_MODULE]: { V2Client: StubV2Client },
53
+ };
54
+ const origResolve = Module._resolveFilename;
55
+ Module._resolveFilename = function patched(request, ...rest) {
56
+ if (request in stubs) return request;
57
+ return origResolve.call(this, request, ...rest);
58
+ };
59
+ for (const [moduleId, exports] of Object.entries(stubs)) {
60
+ require.cache[moduleId] = { id: moduleId, filename: moduleId, loaded: true, exports };
61
+ }
62
+
63
+ const instrumentor = new CohereEmbeddingsInstrumentor();
64
+ instrumentor.instrument();
65
+
66
+ return () => {
67
+ instrumentor.uninstrument();
68
+ Module._resolveFilename = origResolve;
69
+ for (const moduleId of Object.keys(stubs)) delete require.cache[moduleId];
70
+ exporter.reset();
71
+ };
72
+ }
73
+
74
+ test('v1 embed emits genai span with input tokens', async () => {
75
+ const teardown = instrumented();
76
+ try {
77
+ const client = new StubCohereClient();
78
+ const result = await client.embed({ model: 'embed-english-v3.0', texts: ['hi'] });
79
+ assert.equal(result.meta.billedUnits.inputTokens, 8);
80
+
81
+ const [span] = exporter.getFinishedSpans();
82
+ assert.equal(span.name, 'embeddings');
83
+ assert.equal(span.attributes['gen_ai.system'], 'Cohere');
84
+ assert.equal(span.attributes['gen_ai.operation.name'], 'embeddings');
85
+ assert.equal(span.attributes['gen_ai.request.model'], 'embed-english-v3.0');
86
+ assert.equal(span.attributes['gen_ai.usage.input_tokens'], 8);
87
+ assert.deepEqual(JSON.parse(span.attributes['gen_ai.input.messages']), [
88
+ { role: 'user', parts: [{ type: 'text', content: 'hi' }] },
89
+ ]);
90
+ assert.equal(span.status.code, SpanStatusCode.UNSET);
91
+ } finally {
92
+ teardown();
93
+ }
94
+ });
95
+
96
+ test('v2 embed emits genai span with input tokens', async () => {
97
+ const teardown = instrumented();
98
+ try {
99
+ const client = new StubV2Client();
100
+ await client.embed({ model: 'embed-v4.0', texts: ['hi'] });
101
+
102
+ const [span] = exporter.getFinishedSpans();
103
+ assert.equal(span.attributes['gen_ai.request.model'], 'embed-v4.0');
104
+ assert.equal(span.attributes['gen_ai.usage.input_tokens'], 5);
105
+ } finally {
106
+ teardown();
107
+ }
108
+ });
109
+
110
+ test('embed records error status on failure', async () => {
111
+ const teardown = instrumented();
112
+ try {
113
+ const client = new StubCohereClient();
114
+ await assert.rejects(() => client.embed({ model: 'boom', texts: ['x'] }), /api error/);
115
+
116
+ const [span] = exporter.getFinishedSpans();
117
+ assert.equal(span.status.code, SpanStatusCode.ERROR);
118
+ } finally {
119
+ teardown();
120
+ }
121
+ });
122
+
123
+ test('uninstrument restores original methods', async () => {
124
+ const teardown = instrumented();
125
+ try {
126
+ const client = new StubCohereClient();
127
+ await client.embed({ model: 'embed-english-v3.0', texts: ['x'] });
128
+ assert.equal(exporter.getFinishedSpans().length, 1);
129
+ } finally {
130
+ teardown();
131
+ }
132
+ });
@@ -20,7 +20,9 @@ const {
20
20
  SimpleSpanProcessor,
21
21
  } = require('@opentelemetry/sdk-trace-base');
22
22
 
23
- const { ElevenLabsInstrumentor } = require('../../src/instrumentation/elevenlabs');
23
+ const {
24
+ ElevenLabsInstrumentor,
25
+ } = require('../../../src/instrumentation/elevenlabs/full_instrumentation');
24
26
 
25
27
  const TTS_CLIENT_MODULE = '@elevenlabs/elevenlabs-js/api/resources/textToSpeech/client/Client';
26
28
  const STT_CLIENT_MODULE = '@elevenlabs/elevenlabs-js/api/resources/speechToText/client/Client';
@@ -33,6 +35,12 @@ trace.setGlobalTracerProvider(
33
35
  new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(exporter)] })
34
36
  );
35
37
 
38
+ // The exact chunk objects the streaming stubs yield. Shared so the passthrough
39
+ // assertions can compare object identity — proof our proxies re-emit the user's
40
+ // chunks without copying or mutating them.
41
+ const TTS_BYTE_CHUNKS = [Buffer.from('a'), Buffer.from('b')];
42
+ const TTS_OBJECT_CHUNKS = [{ audioBase64: 'eA==' }, { audioBase64: 'YmI=' }];
43
+
36
44
  function readableOf(chunks) {
37
45
  let index = 0;
38
46
  return new ReadableStream({
@@ -62,7 +70,7 @@ function asyncIterableOf(chunks) {
62
70
  class StubTextToSpeechClient {
63
71
  convert(_voiceId, request = {}) {
64
72
  if (request.text === 'boom') return Promise.resolve(failingReadable(new Error('api error')));
65
- return Promise.resolve(readableOf([Buffer.from('a'), Buffer.from('b')]));
73
+ return Promise.resolve(readableOf(TTS_BYTE_CHUNKS));
66
74
  }
67
75
 
68
76
  stream(_voiceId, _request = {}) {
@@ -70,7 +78,7 @@ class StubTextToSpeechClient {
70
78
  }
71
79
 
72
80
  streamWithTimestamps(_voiceId, _request = {}) {
73
- return Promise.resolve(asyncIterableOf([{ audioBase64: 'eA==' }]));
81
+ return Promise.resolve(asyncIterableOf(TTS_OBJECT_CHUNKS));
74
82
  }
75
83
 
76
84
  convertWithTimestamps(_voiceId, _request = {}) {
@@ -134,7 +142,11 @@ test('convert emits genai span with char count', async () => {
134
142
  try {
135
143
  const client = new StubTextToSpeechClient();
136
144
  const stream = await client.convert('voice', { text: 'hello world', modelId: 'eleven_v2' });
137
- assert.deepEqual(await drain(stream), [Buffer.from('a'), Buffer.from('b')]);
145
+ // Same count, order, and object identity: the byte-stream proxy re-emits each
146
+ // chunk by reference — no telemetry collision with the caller's audio.
147
+ const collected = await drain(stream);
148
+ assert.equal(collected.length, TTS_BYTE_CHUNKS.length);
149
+ collected.forEach((chunk, index) => assert.equal(chunk, TTS_BYTE_CHUNKS[index]));
138
150
 
139
151
  const [span] = exporter.getFinishedSpans();
140
152
  assert.equal(span.name, 'text_to_speech');
@@ -184,7 +196,11 @@ test('stream with timestamps emits span with char count', async () => {
184
196
  try {
185
197
  const client = new StubTextToSpeechClient();
186
198
  const stream = await client.streamWithTimestamps('voice', { text: 'hello', modelId: 'm' });
187
- assert.deepEqual(await drainIterable(stream), [{ audioBase64: 'eA==' }]);
199
+ // Same count, order, and object identity: the object-stream proxy re-yields
200
+ // each chunk by reference — no telemetry collision with the caller's payload.
201
+ const collected = await drainIterable(stream);
202
+ assert.equal(collected.length, TTS_OBJECT_CHUNKS.length);
203
+ collected.forEach((chunk, index) => assert.equal(chunk, TTS_OBJECT_CHUNKS[index]));
188
204
 
189
205
  const [span] = exporter.getFinishedSpans();
190
206
  assert.equal(span.name, 'text_to_speech');