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.
- package/index.d.ts +1 -1
- package/package.json +2 -2
- package/src/auto-instrument.js +64 -31
- package/src/instrumentation/anthropic/get_cache_and_reasoning_telemetry.js +107 -0
- package/src/instrumentation/anthropic/get_streaming_stop_reason_telemetry.js +121 -0
- package/src/instrumentation/anthropic/get_tool_definitions_telemetry.js +50 -0
- package/src/instrumentation/anthropic/set_telemetry_attributes.js +37 -0
- package/src/instrumentation/bedrock/get_cache_telemetry.js +104 -0
- package/src/instrumentation/bedrock/get_input_message_content_telemetry.js +94 -0
- package/src/instrumentation/bedrock/get_streaming_output_message_telemetry.js +87 -0
- package/src/instrumentation/bedrock/set_telemetry_attributes.js +36 -0
- package/src/instrumentation/cohere/get_chat_telemetry.js +88 -0
- package/src/instrumentation/cohere/get_embeddings_telemetry.js +107 -0
- package/src/instrumentation/cohere/set_telemetry_attributes.js +32 -0
- package/src/instrumentation/google/get_audio_telemetry.js +30 -0
- package/src/instrumentation/google/get_cache_telemetry.js +25 -0
- package/src/instrumentation/google/get_embeddings_telemetry.js +99 -0
- package/src/instrumentation/google/get_provider_name.js +25 -0
- package/src/instrumentation/google/get_reasoning_telemetry.js +21 -0
- package/src/instrumentation/google/set_telemetry_attributes.js +98 -0
- package/src/instrumentation/openai/get_audio_telemetry.js +149 -0
- package/src/instrumentation/openai/get_cache_and_reasoning_telemetry.js +71 -0
- package/src/instrumentation/openai/get_embeddings_telemetry.js +107 -0
- package/src/instrumentation/openai/get_streaming_usage.js +81 -0
- package/src/instrumentation/openai/set_telemetry_attributes.js +39 -0
- package/src/otel/exporter.js +1 -6
- package/src/otel/propagation.js +5 -0
- package/src/otel/span-filter.js +0 -5
- package/src/otel/span-processor.js +30 -1
- package/src/setup.js +1 -2
- package/tests/index.test.js +144 -44
- package/tests/instrumentation/anthropic/get_cache_and_reasoning_telemetry.test.js +158 -0
- package/tests/instrumentation/anthropic/get_streaming_stop_reason_telemetry.test.js +117 -0
- package/tests/instrumentation/anthropic/get_tool_definitions_telemetry.test.js +82 -0
- package/tests/instrumentation/bedrock/get_cache_telemetry.test.js +102 -0
- package/tests/instrumentation/bedrock/get_input_message_content_telemetry.test.js +89 -0
- package/tests/instrumentation/bedrock/get_streaming_output_message_telemetry.test.js +87 -0
- package/tests/instrumentation/cohere/get_chat_telemetry.test.js +148 -0
- package/tests/instrumentation/cohere/get_embeddings_telemetry.test.js +132 -0
- package/tests/instrumentation/{elevenlabs.test.js → elevenlabs/full_instrumentation.test.js} +21 -5
- package/tests/instrumentation/google/get_embeddings_telemetry.test.js +128 -0
- package/tests/instrumentation/google/get_reasoning_telemetry.test.js +41 -0
- package/tests/instrumentation/google/set_telemetry_attributes.test.js +219 -0
- package/tests/instrumentation/openai/get_audio_telemetry.test.js +170 -0
- package/tests/instrumentation/openai/get_cache_and_reasoning_telemetry.test.js +83 -0
- package/tests/instrumentation/openai/get_embeddings_telemetry.test.js +145 -0
- package/tests/instrumentation/openai/get_streaming_usage.test.js +105 -0
- package/src/otel/openai-streaming-usage.js +0 -60
- /package/src/instrumentation/{elevenlabs.js → elevenlabs/full_instrumentation.js} +0 -0
|
@@ -0,0 +1,145 @@
|
|
|
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.deepEqual(JSON.parse(span.attributes['gen_ai.input.messages']), [
|
|
83
|
+
{ role: 'user', parts: [{ type: 'text', content: 'hello' }] },
|
|
84
|
+
]);
|
|
85
|
+
assert.equal(span.status.code, SpanStatusCode.UNSET);
|
|
86
|
+
} finally {
|
|
87
|
+
teardown();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('embeddings emits one input message per element of a list input', async () => {
|
|
92
|
+
const teardown = instrumented();
|
|
93
|
+
try {
|
|
94
|
+
const client = new StubEmbeddings();
|
|
95
|
+
await client.create({ input: ['a', 'b'], model: 'text-embedding-3-small' });
|
|
96
|
+
|
|
97
|
+
const [span] = exporter.getFinishedSpans();
|
|
98
|
+
assert.deepEqual(JSON.parse(span.attributes['gen_ai.input.messages']), [
|
|
99
|
+
{ role: 'user', parts: [{ type: 'text', content: 'a' }] },
|
|
100
|
+
{ role: 'user', parts: [{ type: 'text', content: 'b' }] },
|
|
101
|
+
]);
|
|
102
|
+
} finally {
|
|
103
|
+
teardown();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('embeddings records error status on failure', async () => {
|
|
108
|
+
const teardown = instrumented();
|
|
109
|
+
try {
|
|
110
|
+
const client = new StubEmbeddings();
|
|
111
|
+
await assert.rejects(() => client.create({ input: 'x', model: 'boom' }), /api error/);
|
|
112
|
+
|
|
113
|
+
const [span] = exporter.getFinishedSpans();
|
|
114
|
+
assert.equal(span.status.code, SpanStatusCode.ERROR);
|
|
115
|
+
} finally {
|
|
116
|
+
teardown();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('uninstrument restores original methods', async () => {
|
|
121
|
+
const teardown = instrumented();
|
|
122
|
+
try {
|
|
123
|
+
const client = new StubEmbeddings();
|
|
124
|
+
await client.create({ input: 'x', model: 'text-embedding-3-small' });
|
|
125
|
+
assert.equal(exporter.getFinishedSpans().length, 1);
|
|
126
|
+
} finally {
|
|
127
|
+
teardown();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('system is azure when client targets azure', async () => {
|
|
132
|
+
const teardown = instrumented();
|
|
133
|
+
try {
|
|
134
|
+
const client = new StubEmbeddings();
|
|
135
|
+
// Azure is reached via the same openai SDK; the vendor is read off the
|
|
136
|
+
// client base URL so the span is attributed to azure.ai.openai, not openai.
|
|
137
|
+
client._client = { baseURL: 'https://acme.openai.azure.com/' };
|
|
138
|
+
await client.create({ input: 'hi', model: 'text-embedding-3-small' });
|
|
139
|
+
|
|
140
|
+
const [span] = exporter.getFinishedSpans();
|
|
141
|
+
assert.equal(span.attributes['gen_ai.system'], 'azure.ai.openai');
|
|
142
|
+
} finally {
|
|
143
|
+
teardown();
|
|
144
|
+
}
|
|
145
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
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('re-yields every stream chunk unchanged — no telemetry collision with the payload', async () => {
|
|
73
|
+
const emitted = [
|
|
74
|
+
{ choices: [{ delta: { content: 'hel' } }] },
|
|
75
|
+
{ choices: [{ delta: { content: 'lo' } }] },
|
|
76
|
+
{ usage: { prompt_tokens: 12, completion_tokens: 3, total_tokens: 15 } },
|
|
77
|
+
];
|
|
78
|
+
const instrumentation = {
|
|
79
|
+
_streamingWrapPromise() {
|
|
80
|
+
return (async function* () {
|
|
81
|
+
for (const chunk of emitted) yield chunk;
|
|
82
|
+
})();
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
new OpenAIStreamingUsageInstrumentor().instrument(instrumentation);
|
|
86
|
+
|
|
87
|
+
const span = fakeSpan();
|
|
88
|
+
const collected = [];
|
|
89
|
+
for await (const chunk of instrumentation._streamingWrapPromise({ span })) {
|
|
90
|
+
collected.push(chunk);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Same count, order, and object identity: backfillUsage re-yields each chunk by
|
|
94
|
+
// reference without copying or mutating it.
|
|
95
|
+
assert.equal(collected.length, emitted.length);
|
|
96
|
+
collected.forEach((chunk, index) => assert.equal(chunk, emitted[index]));
|
|
97
|
+
// …while still backfilling usage off the trailing chunk.
|
|
98
|
+
assert.equal(span.attrs['gen_ai.usage.output_tokens'], 3);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('is a no-op when the instrumentor has no _streamingWrapPromise', () => {
|
|
102
|
+
const instrumentation = {};
|
|
103
|
+
new OpenAIStreamingUsageInstrumentor().instrument(instrumentation);
|
|
104
|
+
assert.equal(instrumentation._streamingWrapPromise, undefined);
|
|
105
|
+
});
|
|
@@ -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 };
|
|
File without changes
|