weflayr 0.20.6 → 0.21.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 (37) hide show
  1. package/package.json +2 -2
  2. package/src/auto-instrument.js +64 -31
  3. package/src/instrumentation/anthropic/get_cache_and_reasoning_telemetry.js +107 -0
  4. package/src/instrumentation/anthropic/set_telemetry_attributes.js +27 -0
  5. package/src/instrumentation/bedrock/get_cache_telemetry.js +104 -0
  6. package/src/instrumentation/bedrock/set_telemetry_attributes.js +27 -0
  7. package/src/instrumentation/cohere/get_chat_telemetry.js +88 -0
  8. package/src/instrumentation/cohere/get_embeddings_telemetry.js +97 -0
  9. package/src/instrumentation/cohere/set_telemetry_attributes.js +32 -0
  10. package/src/instrumentation/google/get_audio_telemetry.js +31 -0
  11. package/src/instrumentation/google/get_cache_telemetry.js +27 -0
  12. package/src/instrumentation/google/get_embeddings_telemetry.js +78 -0
  13. package/src/instrumentation/google/get_provider_name.js +25 -0
  14. package/src/instrumentation/google/get_reasoning_telemetry.js +21 -0
  15. package/src/instrumentation/google/set_telemetry_attributes.js +98 -0
  16. package/src/instrumentation/openai/get_audio_telemetry.js +149 -0
  17. package/src/instrumentation/openai/get_cache_and_reasoning_telemetry.js +71 -0
  18. package/src/instrumentation/openai/get_embeddings_telemetry.js +95 -0
  19. package/src/instrumentation/openai/get_streaming_usage.js +81 -0
  20. package/src/instrumentation/openai/set_telemetry_attributes.js +39 -0
  21. package/src/otel/propagation.js +5 -0
  22. package/src/otel/span-filter.js +0 -5
  23. package/tests/index.test.js +42 -44
  24. package/tests/instrumentation/anthropic/get_cache_and_reasoning_telemetry.test.js +125 -0
  25. package/tests/instrumentation/bedrock/get_cache_telemetry.test.js +102 -0
  26. package/tests/instrumentation/cohere/get_chat_telemetry.test.js +148 -0
  27. package/tests/instrumentation/cohere/get_embeddings_telemetry.test.js +129 -0
  28. package/tests/instrumentation/{elevenlabs.test.js → elevenlabs/full_instrumentation.test.js} +3 -1
  29. package/tests/instrumentation/google/get_embeddings_telemetry.test.js +125 -0
  30. package/tests/instrumentation/google/get_reasoning_telemetry.test.js +41 -0
  31. package/tests/instrumentation/google/set_telemetry_attributes.test.js +187 -0
  32. package/tests/instrumentation/openai/get_audio_telemetry.test.js +170 -0
  33. package/tests/instrumentation/openai/get_cache_and_reasoning_telemetry.test.js +83 -0
  34. package/tests/instrumentation/openai/get_embeddings_telemetry.test.js +126 -0
  35. package/tests/instrumentation/openai/get_streaming_usage.test.js +76 -0
  36. package/src/otel/openai-streaming-usage.js +0 -60
  37. /package/src/instrumentation/{elevenlabs.js → elevenlabs/full_instrumentation.js} +0 -0
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ // Tests for the OpenAI non-streaming cached-prompt token complement. It wraps the
4
+ // primary instrumentor's `_endSpan({ span, type, result })` to stamp
5
+ // `gen_ai.usage.cache_read_input_tokens` from `usage.prompt_tokens_details.cached_tokens`.
6
+
7
+ const test = require('node:test');
8
+ const assert = require('node:assert/strict');
9
+
10
+ const {
11
+ OpenAIPromptCachingInstrumentor,
12
+ } = require('../../../src/instrumentation/openai/get_cache_and_reasoning_telemetry');
13
+
14
+ function fakeSpan() {
15
+ const attrs = {};
16
+ return {
17
+ attrs,
18
+ isRecording: () => true,
19
+ setAttribute(key, value) {
20
+ attrs[key] = value;
21
+ },
22
+ };
23
+ }
24
+
25
+ test('stamps cache_read_input_tokens and still calls the original _endSpan', () => {
26
+ const calls = [];
27
+ const instrumentation = {
28
+ _endSpan(args) {
29
+ calls.push(args);
30
+ },
31
+ };
32
+ new OpenAIPromptCachingInstrumentor().instrument(instrumentation);
33
+
34
+ const span = fakeSpan();
35
+ instrumentation._endSpan({
36
+ span,
37
+ type: 'chat',
38
+ result: { usage: { prompt_tokens: 1600, prompt_tokens_details: { cached_tokens: 1536 } } },
39
+ });
40
+
41
+ assert.equal(span.attrs['gen_ai.usage.cache_read_input_tokens'], 1536);
42
+ assert.equal(calls.length, 1);
43
+ });
44
+
45
+ test('sets nothing when there are no cached tokens', () => {
46
+ const instrumentation = { _endSpan() {} };
47
+ new OpenAIPromptCachingInstrumentor().instrument(instrumentation);
48
+
49
+ const span = fakeSpan();
50
+ instrumentation._endSpan({ span, type: 'chat', result: { usage: { prompt_tokens: 10 } } });
51
+
52
+ assert.equal(span.attrs['gen_ai.usage.cache_read_input_tokens'], undefined);
53
+ });
54
+
55
+ test('stamps reasoning_tokens from completion_tokens_details', () => {
56
+ const instrumentation = { _endSpan() {} };
57
+ new OpenAIPromptCachingInstrumentor().instrument(instrumentation);
58
+
59
+ const span = fakeSpan();
60
+ instrumentation._endSpan({
61
+ span,
62
+ type: 'chat',
63
+ result: { usage: { output_tokens: 115, completion_tokens_details: { reasoning_tokens: 64 } } },
64
+ });
65
+
66
+ assert.equal(span.attrs['gen_ai.usage.reasoning_tokens'], 64);
67
+ });
68
+
69
+ test('is a no-op when the instrumentor has no _endSpan', () => {
70
+ const instrumentation = {};
71
+ new OpenAIPromptCachingInstrumentor().instrument(instrumentation);
72
+ assert.equal(instrumentation._endSpan, undefined);
73
+ });
74
+
75
+ test('uninstrument restores the original _endSpan', () => {
76
+ const original = () => {};
77
+ const instrumentation = { _endSpan: original };
78
+ const instrumentor = new OpenAIPromptCachingInstrumentor();
79
+ instrumentor.instrument(instrumentation);
80
+ assert.notEqual(instrumentation._endSpan, original);
81
+ instrumentor.uninstrument();
82
+ assert.equal(instrumentation._endSpan, original);
83
+ });
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+
3
+ // Tests for the bundled OpenAI embeddings instrumentor.
4
+ //
5
+ // The real `openai` Embeddings resource class is stubbed with a `create` method
6
+ // that mirrors the SDK's shape — resolving to a response carrying `model` and a
7
+ // `usage.prompt_tokens` count — 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
+ OpenAIEmbeddingsInstrumentor,
23
+ } = require('../../../src/instrumentation/openai/get_embeddings_telemetry');
24
+
25
+ const EMBEDDINGS_MODULE = 'openai/resources/embeddings';
26
+
27
+ const exporter = new InMemorySpanExporter();
28
+ trace.setGlobalTracerProvider(
29
+ new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(exporter)] })
30
+ );
31
+
32
+ class StubEmbeddings {
33
+ create(body = {}) {
34
+ if (body.model === 'boom') return Promise.reject(new Error('api error'));
35
+ return Promise.resolve({
36
+ object: 'list',
37
+ data: [{ embedding: [0.1, 0.2] }],
38
+ model: 'text-embedding-3-small',
39
+ usage: { prompt_tokens: 8, total_tokens: 8 },
40
+ });
41
+ }
42
+ }
43
+
44
+ // Stub the openai embeddings resource module, install the instrumentor + the
45
+ // in-memory exporter, and return a teardown that restores everything.
46
+ function instrumented() {
47
+ const stubs = { [EMBEDDINGS_MODULE]: { Embeddings: StubEmbeddings } };
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 OpenAIEmbeddingsInstrumentor();
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
+ test('embeddings emits genai span with prompt tokens', async () => {
69
+ const teardown = instrumented();
70
+ try {
71
+ const client = new StubEmbeddings();
72
+ const result = await client.create({ input: 'hello', model: 'text-embedding-3-small' });
73
+ assert.equal(result.usage.prompt_tokens, 8);
74
+
75
+ const [span] = exporter.getFinishedSpans();
76
+ assert.equal(span.name, 'embeddings');
77
+ assert.equal(span.attributes['gen_ai.system'], 'openai');
78
+ assert.equal(span.attributes['gen_ai.operation.name'], 'embeddings');
79
+ assert.equal(span.attributes['gen_ai.request.model'], 'text-embedding-3-small');
80
+ assert.equal(span.attributes['gen_ai.response.model'], 'text-embedding-3-small');
81
+ assert.equal(span.attributes['gen_ai.usage.input_tokens'], 8);
82
+ assert.equal(span.status.code, SpanStatusCode.UNSET);
83
+ } finally {
84
+ teardown();
85
+ }
86
+ });
87
+
88
+ test('embeddings records error status on failure', async () => {
89
+ const teardown = instrumented();
90
+ try {
91
+ const client = new StubEmbeddings();
92
+ await assert.rejects(() => client.create({ input: 'x', model: 'boom' }), /api error/);
93
+
94
+ const [span] = exporter.getFinishedSpans();
95
+ assert.equal(span.status.code, SpanStatusCode.ERROR);
96
+ } finally {
97
+ teardown();
98
+ }
99
+ });
100
+
101
+ test('uninstrument restores original methods', async () => {
102
+ const teardown = instrumented();
103
+ try {
104
+ const client = new StubEmbeddings();
105
+ await client.create({ input: 'x', model: 'text-embedding-3-small' });
106
+ assert.equal(exporter.getFinishedSpans().length, 1);
107
+ } finally {
108
+ teardown();
109
+ }
110
+ });
111
+
112
+ test('system is azure when client targets azure', async () => {
113
+ const teardown = instrumented();
114
+ try {
115
+ const client = new StubEmbeddings();
116
+ // Azure is reached via the same openai SDK; the vendor is read off the
117
+ // client base URL so the span is attributed to azure.ai.openai, not openai.
118
+ client._client = { baseURL: 'https://acme.openai.azure.com/' };
119
+ await client.create({ input: 'hi', model: 'text-embedding-3-small' });
120
+
121
+ const [span] = exporter.getFinishedSpans();
122
+ assert.equal(span.attributes['gen_ai.system'], 'azure.ai.openai');
123
+ } finally {
124
+ teardown();
125
+ }
126
+ });
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ // Tests for the OpenAI streamed token-usage complement. It wraps the primary
4
+ // instrumentor's `_streamingWrapPromise` so the streamed usage chunk's counts land
5
+ // on the span as gen_ai.usage.* attributes.
6
+
7
+ const test = require('node:test');
8
+ const assert = require('node:assert/strict');
9
+
10
+ const {
11
+ OpenAIStreamingUsageInstrumentor,
12
+ } = require('../../../src/instrumentation/openai/get_streaming_usage');
13
+
14
+ function fakeSpan() {
15
+ const attrs = {};
16
+ return {
17
+ attrs,
18
+ isRecording: () => true,
19
+ setAttribute(key, value) {
20
+ attrs[key] = value;
21
+ },
22
+ };
23
+ }
24
+
25
+ test('backfills token usage from the streamed usage chunk', async () => {
26
+ const instrumentation = {
27
+ _streamingWrapPromise() {
28
+ return (async function* () {
29
+ yield { choices: [{ delta: { content: 'hi' } }] };
30
+ yield { usage: { prompt_tokens: 12, completion_tokens: 3, total_tokens: 15 } };
31
+ })();
32
+ },
33
+ };
34
+ new OpenAIStreamingUsageInstrumentor().instrument(instrumentation);
35
+
36
+ const span = fakeSpan();
37
+ const stream = instrumentation._streamingWrapPromise({ span });
38
+ for await (const _chunk of stream) {
39
+ void _chunk;
40
+ }
41
+
42
+ assert.equal(span.attrs['gen_ai.usage.input_tokens'], 12);
43
+ assert.equal(span.attrs['gen_ai.usage.output_tokens'], 3);
44
+ assert.equal(span.attrs['gen_ai.usage.total_tokens'], 15);
45
+ });
46
+
47
+ test('backfills cached-prompt tokens from the streamed usage chunk', async () => {
48
+ const instrumentation = {
49
+ _streamingWrapPromise() {
50
+ return (async function* () {
51
+ yield {
52
+ usage: {
53
+ prompt_tokens: 1600,
54
+ completion_tokens: 2,
55
+ total_tokens: 1602,
56
+ prompt_tokens_details: { cached_tokens: 1536 },
57
+ },
58
+ };
59
+ })();
60
+ },
61
+ };
62
+ new OpenAIStreamingUsageInstrumentor().instrument(instrumentation);
63
+
64
+ const span = fakeSpan();
65
+ for await (const _chunk of instrumentation._streamingWrapPromise({ span })) {
66
+ void _chunk;
67
+ }
68
+
69
+ assert.equal(span.attrs['gen_ai.usage.cache_read_input_tokens'], 1536);
70
+ });
71
+
72
+ test('is a no-op when the instrumentor has no _streamingWrapPromise', () => {
73
+ const instrumentation = {};
74
+ new OpenAIStreamingUsageInstrumentor().instrument(instrumentation);
75
+ assert.equal(instrumentation._streamingWrapPromise, undefined);
76
+ });
@@ -1,60 +0,0 @@
1
- 'use strict';
2
-
3
- // Backfill token usage onto streamed OpenAI spans.
4
- //
5
- // @traceloop/instrumentation-openai (the OpenAI OTel instrumentor we load for
6
- // the `openai` SDK) does not record token usage for streamed calls: its stream
7
- // handler accumulates `choices[].delta` but never reads the final usage chunk
8
- // that OpenAI sends when the request sets `stream_options: { include_usage:
9
- // true }`. Non-streaming calls work because OpenAI returns `usage` in the
10
- // response body, which the instrumentor reads directly. The Python OpenLLMetry
11
- // instrumentor does read the streamed usage, so this gap is JS-only.
12
- //
13
- // Read `usage` from the streamed chunks. The instrumentor's streaming handler hands us the span
14
- // and ends it only after its chunk loop, so when the usage chunk arrives the
15
- // span is still open and we set the same `gen_ai.usage.*` attributes the
16
- // instrumentor sets for non-streaming calls.
17
-
18
- const USAGE_INPUT_TOKENS_ATTR = 'gen_ai.usage.input_tokens';
19
- const USAGE_OUTPUT_TOKENS_ATTR = 'gen_ai.usage.output_tokens';
20
- const USAGE_TOTAL_TOKENS_ATTR = 'gen_ai.usage.total_tokens';
21
-
22
- function _setUsageAttributes(span, usage) {
23
- if (typeof usage.prompt_tokens === 'number') {
24
- span.setAttribute(USAGE_INPUT_TOKENS_ATTR, usage.prompt_tokens);
25
- }
26
- if (typeof usage.completion_tokens === 'number') {
27
- span.setAttribute(USAGE_OUTPUT_TOKENS_ATTR, usage.completion_tokens);
28
- }
29
- if (typeof usage.total_tokens === 'number') {
30
- span.setAttribute(USAGE_TOTAL_TOKENS_ATTR, usage.total_tokens);
31
- }
32
- }
33
-
34
- /**
35
- * Wrap the OpenAI instrumentor's streaming handler so streamed spans carry
36
- * token usage. No-op if the instrumentor's internals have changed shape — the
37
- * worst case is streamed spans without token counts, never a crash.
38
- */
39
- function patchOpenAIStreamingUsage(instrumentation) {
40
- const originalStreamingWrap = instrumentation._streamingWrapPromise;
41
- if (typeof originalStreamingWrap !== 'function') return;
42
-
43
- instrumentation._streamingWrapPromise = function streamingWrapWithUsage(streamArgs) {
44
- const span = streamArgs && streamArgs.span;
45
- const wrappedStream = originalStreamingWrap.call(this, streamArgs);
46
- if (!span) return wrappedStream;
47
-
48
- return (async function* backfillUsage() {
49
- for await (const chunk of wrappedStream) {
50
- const usage = chunk && chunk.usage;
51
- if (usage && span.isRecording()) {
52
- _setUsageAttributes(span, usage);
53
- }
54
- yield chunk;
55
- }
56
- })();
57
- };
58
- }
59
-
60
- module.exports = { patchOpenAIStreamingUsage };