weflayr 0.29.1 → 0.31.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 (46) hide show
  1. package/dist/package.json +9 -9
  2. package/dist/src/api/generated/facade.gen.d.ts +1436 -10
  3. package/dist/src/api/generated/facade.gen.js +24 -1
  4. package/dist/src/api/generated/index.d.ts +2 -2
  5. package/dist/src/api/generated/index.js +24 -1
  6. package/dist/src/api/generated/sdk.gen.d.ts +97 -5
  7. package/dist/src/api/generated/sdk.gen.js +256 -5
  8. package/dist/src/api/generated/types.gen.d.ts +4527 -741
  9. package/dist/src/get-data-uri-mime-type.d.ts +8 -0
  10. package/dist/src/get-data-uri-mime-type.js +18 -0
  11. package/dist/src/instrumentation/anthropic/get_streaming_accumulation_telemetry.d.ts +6 -4
  12. package/dist/src/instrumentation/anthropic/get_streaming_accumulation_telemetry.js +22 -5
  13. package/dist/src/instrumentation/bedrock/{get_cache_telemetry.d.ts → get_cache_and_reasoning_telemetry.d.ts} +7 -0
  14. package/dist/src/instrumentation/bedrock/{get_cache_telemetry.js → get_cache_and_reasoning_telemetry.js} +25 -2
  15. package/dist/src/instrumentation/bedrock/get_input_message_content_telemetry.js +23 -16
  16. package/dist/src/instrumentation/bedrock/get_streaming_output_message_telemetry.d.ts +6 -0
  17. package/dist/src/instrumentation/bedrock/get_streaming_output_message_telemetry.js +18 -2
  18. package/dist/src/instrumentation/bedrock/set_telemetry_attributes.js +2 -2
  19. package/dist/src/instrumentation/cohere/get_input_message_content_telemetry.d.ts +2 -3
  20. package/dist/src/instrumentation/cohere/get_input_message_content_telemetry.js +6 -10
  21. package/dist/src/instrumentation/google/get_cache_operations_telemetry.js +7 -3
  22. package/dist/src/instrumentation/mistral/full_instrumentation.d.ts +10 -4
  23. package/dist/src/instrumentation/mistral/full_instrumentation.js +47 -14
  24. package/dist/src/instrumentation/mistral/get_input_message_content_telemetry.d.ts +1 -1
  25. package/dist/src/instrumentation/mistral/get_input_message_content_telemetry.js +5 -8
  26. package/dist/src/instrumentation/mistral/get_streamed_reasoning_telemetry.d.ts +38 -0
  27. package/dist/src/instrumentation/mistral/get_streamed_reasoning_telemetry.js +72 -0
  28. package/dist/src/instrumentation/openai/get_file_part_mime_type_telemetry.d.ts +5 -4
  29. package/dist/src/instrumentation/openai/get_file_part_mime_type_telemetry.js +14 -10
  30. package/dist/src/instrumentation/openai/get_reasoning_effort_telemetry.d.ts +16 -0
  31. package/dist/src/instrumentation/openai/get_reasoning_effort_telemetry.js +49 -0
  32. package/dist/src/instrumentation/openai/get_responses_telemetry.d.ts +7 -7
  33. package/dist/src/instrumentation/openai/get_responses_telemetry.js +11 -11
  34. package/dist/src/instrumentation/openai/get_streaming_usage.d.ts +5 -2
  35. package/dist/src/instrumentation/openai/get_streaming_usage.js +10 -2
  36. package/dist/src/instrumentation/openai/set_telemetry_attributes.d.ts +2 -1
  37. package/dist/src/instrumentation/openai/set_telemetry_attributes.js +4 -1
  38. package/dist/src/otel/exporter.d.ts +1 -1
  39. package/dist/src/otel/span-filter.d.ts +1 -1
  40. package/dist/src/otel/span-processor.d.ts +1 -1
  41. package/dist/src/otel/span-processor.js +3 -7
  42. package/dist/src/propagate-metadata-to-gateway.js +38 -11
  43. package/dist/src/setup.js +19 -7
  44. package/package.json +9 -9
  45. package/dist/src/otel/strip-media-payloads.d.ts +0 -26
  46. package/dist/src/otel/strip-media-payloads.js +0 -135
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The reasoning of a streamed Mistral answer.
3
+ *
4
+ * A non-streamed answer of a model that thinks holds its reasoning as a
5
+ * `thinking` content block before its text, which the `@mistralai/mistralai`
6
+ * built-in tracing writes as a `reasoning` output part. On a stream the same
7
+ * blocks arrive in the deltas' `content`, but the tracing's
8
+ * `accumulateChunksToResponseDict` keeps only their `text` blocks, so a streamed
9
+ * thinking call records no reasoning part. The package is ESM only, so that
10
+ * function cannot be wrapped: the thinking is read off the chunks as they pass
11
+ * (`addStreamedThinking`), then written as a `reasoning` part before the text of
12
+ * its choice's output message when the tracing sets `gen_ai.output.messages`
13
+ * (`addStreamedThinkingToOutputMessages`), the part a non-streamed answer gets.
14
+ */
15
+ /**
16
+ * Add the thinking text each choice of one streamed chunk (the parsed JSON of
17
+ * its `data:` line) carries to `thinkingByChoice`, by choice index. Every
18
+ * choice the chunk names gets an entry, an empty text when it streamed no
19
+ * thinking, so the entries list the choices the output messages are written
20
+ * for.
21
+ *
22
+ * Example: a chunk whose choice 0 delta content is `[{ type: "thinking",
23
+ * thinking: [{ type: "text", text: "the r." }] }]` turns `{ 0: "Count " }` into
24
+ * `{ 0: "Count the r." }`.
25
+ */
26
+ export declare function addStreamedThinking(chunk: any, thinkingByChoice: Map<number, string>): void;
27
+ /**
28
+ * `gen_ai.output.messages`, one message per choice in the order of their
29
+ * index, with each choice's streamed thinking written as a `reasoning` part
30
+ * before its other parts. A message that already has a reasoning part is left
31
+ * as it is.
32
+ *
33
+ * Example: `[{"role": "assistant", "parts": [{"type": "text", "content": "6"}]}]`
34
+ * with `{ 0: "Count the r." }` becomes `[{"role": "assistant", "parts":
35
+ * [{"type": "reasoning", "content": "Count the r."}, {"type": "text", "content":
36
+ * "6"}]}]`.
37
+ */
38
+ export declare function addStreamedThinkingToOutputMessages(outputMessagesJson: string, thinkingByChoice: Map<number, string>): string;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ /**
3
+ * The reasoning of a streamed Mistral answer.
4
+ *
5
+ * A non-streamed answer of a model that thinks holds its reasoning as a
6
+ * `thinking` content block before its text, which the `@mistralai/mistralai`
7
+ * built-in tracing writes as a `reasoning` output part. On a stream the same
8
+ * blocks arrive in the deltas' `content`, but the tracing's
9
+ * `accumulateChunksToResponseDict` keeps only their `text` blocks, so a streamed
10
+ * thinking call records no reasoning part. The package is ESM only, so that
11
+ * function cannot be wrapped: the thinking is read off the chunks as they pass
12
+ * (`addStreamedThinking`), then written as a `reasoning` part before the text of
13
+ * its choice's output message when the tracing sets `gen_ai.output.messages`
14
+ * (`addStreamedThinkingToOutputMessages`), the part a non-streamed answer gets.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.addStreamedThinking = addStreamedThinking;
18
+ exports.addStreamedThinkingToOutputMessages = addStreamedThinkingToOutputMessages;
19
+ const THINKING_BLOCK_TYPE = 'thinking';
20
+ const REASONING_PART_TYPE = 'reasoning';
21
+ /**
22
+ * Add the thinking text each choice of one streamed chunk (the parsed JSON of
23
+ * its `data:` line) carries to `thinkingByChoice`, by choice index. Every
24
+ * choice the chunk names gets an entry, an empty text when it streamed no
25
+ * thinking, so the entries list the choices the output messages are written
26
+ * for.
27
+ *
28
+ * Example: a chunk whose choice 0 delta content is `[{ type: "thinking",
29
+ * thinking: [{ type: "text", text: "the r." }] }]` turns `{ 0: "Count " }` into
30
+ * `{ 0: "Count the r." }`.
31
+ */
32
+ function addStreamedThinking(chunk, thinkingByChoice) {
33
+ for (const choice of chunk?.choices ?? []) {
34
+ const choiceIndex = choice?.index ?? 0;
35
+ let thinking = thinkingByChoice.get(choiceIndex) ?? '';
36
+ const content = choice?.delta?.content;
37
+ for (const block of Array.isArray(content) ? content : []) {
38
+ if (block?.type !== THINKING_BLOCK_TYPE || !Array.isArray(block.thinking))
39
+ continue;
40
+ for (const piece of block.thinking) {
41
+ if (typeof piece?.text === 'string')
42
+ thinking += piece.text;
43
+ }
44
+ }
45
+ thinkingByChoice.set(choiceIndex, thinking);
46
+ }
47
+ }
48
+ /**
49
+ * `gen_ai.output.messages`, one message per choice in the order of their
50
+ * index, with each choice's streamed thinking written as a `reasoning` part
51
+ * before its other parts. A message that already has a reasoning part is left
52
+ * as it is.
53
+ *
54
+ * Example: `[{"role": "assistant", "parts": [{"type": "text", "content": "6"}]}]`
55
+ * with `{ 0: "Count the r." }` becomes `[{"role": "assistant", "parts":
56
+ * [{"type": "reasoning", "content": "Count the r."}, {"type": "text", "content":
57
+ * "6"}]}]`.
58
+ */
59
+ function addStreamedThinkingToOutputMessages(outputMessagesJson, thinkingByChoice) {
60
+ if (![...thinkingByChoice.values()].some(Boolean))
61
+ return outputMessagesJson;
62
+ const messages = JSON.parse(outputMessagesJson);
63
+ const choiceIndexes = [...thinkingByChoice.keys()].sort((first, second) => first - second);
64
+ messages.forEach((message, position) => {
65
+ const thinking = thinkingByChoice.get(choiceIndexes[position]);
66
+ const parts = Array.isArray(message.parts) ? message.parts : [];
67
+ if (!thinking || parts.some((part) => part?.type === REASONING_PART_TYPE))
68
+ return;
69
+ message.parts = [{ type: REASONING_PART_TYPE, content: thinking }, ...parts];
70
+ });
71
+ return JSON.stringify(messages);
72
+ }
@@ -4,9 +4,10 @@
4
4
  * The traceloop instrumentor maps a `file` content block to a blob part but
5
5
  * reads its mime type from `block.file.mime_type`, a field OpenAI's API does
6
6
  * not have, so every inline document is recorded as
7
- * `application/octet-stream`. The real mime type is right there in the data URI
8
- * the caller sent (`data:application/pdf;base64,...`), and the Python SDK keeps
9
- * it, so this complement reads it back off the request and repairs the part.
7
+ * `application/octet-stream`. The real mime type and the bytes are right there
8
+ * in the data URI the caller sent (`data:application/pdf;base64,...`), and the
9
+ * Python SDK keeps them, so this complement reads them back off the request and
10
+ * repairs the part.
10
11
  *
11
12
  * Wraps `startSpan({ type, params })`, which is where the instrumentor builds
12
13
  * `gen_ai.input.messages`, so the attribute is already on the span when this
@@ -15,7 +16,7 @@
15
16
  */
16
17
  /**
17
18
  * `gen_ai.input.messages` with every blob part whose request block carried a
18
- * data URI given that URI's mime type, or null when there was nothing to
19
+ * data URI given that URI's mime type and bytes, or null when there was nothing to
19
20
  * repair (so the instrumentor's own JSON is left untouched).
20
21
  */
21
22
  export declare function repairFileMimeTypes(inputMessagesJson: unknown, messages: unknown): string | null;
@@ -5,9 +5,10 @@
5
5
  * The traceloop instrumentor maps a `file` content block to a blob part but
6
6
  * reads its mime type from `block.file.mime_type`, a field OpenAI's API does
7
7
  * not have, so every inline document is recorded as
8
- * `application/octet-stream`. The real mime type is right there in the data URI
9
- * the caller sent (`data:application/pdf;base64,...`), and the Python SDK keeps
10
- * it, so this complement reads it back off the request and repairs the part.
8
+ * `application/octet-stream`. The real mime type and the bytes are right there
9
+ * in the data URI the caller sent (`data:application/pdf;base64,...`), and the
10
+ * Python SDK keeps them, so this complement reads them back off the request and
11
+ * repairs the part.
11
12
  *
12
13
  * Wraps `startSpan({ type, params })`, which is where the instrumentor builds
13
14
  * `gen_ai.input.messages`, so the attribute is already on the span when this
@@ -24,16 +25,16 @@ function asObject(value) {
24
25
  ? value
25
26
  : null;
26
27
  }
27
- /** The mime type of the data URI a request content block carries, if any. */
28
- function getBlockMimeType(block) {
28
+ /** The blob part holding the data URI a request content block carries, if any. */
29
+ function getBlockBlobPart(block) {
29
30
  const fileBlock = asObject(asObject(block)?.file);
30
31
  if (fileBlock === null)
31
32
  return null;
32
- return (0, get_data_uri_mime_type_1.getDataUriMimeType)(String(fileBlock.file_data ?? ''));
33
+ return (0, get_data_uri_mime_type_1.getDataUriBlobPart)(String(fileBlock.file_data ?? ''));
33
34
  }
34
35
  /**
35
36
  * `gen_ai.input.messages` with every blob part whose request block carried a
36
- * data URI given that URI's mime type, or null when there was nothing to
37
+ * data URI given that URI's mime type and bytes, or null when there was nothing to
37
38
  * repair (so the instrumentor's own JSON is left untouched).
38
39
  */
39
40
  function repairFileMimeTypes(inputMessagesJson, messages) {
@@ -55,11 +56,14 @@ function repairFileMimeTypes(inputMessagesJson, messages) {
55
56
  if (!Array.isArray(content) || !Array.isArray(parts))
56
57
  return;
57
58
  content.forEach((block, blockIndex) => {
58
- const mimeType = getBlockMimeType(block);
59
+ const blobPart = getBlockBlobPart(block);
59
60
  const part = asObject(parts[blockIndex]);
60
- if (mimeType === null || part?.type !== 'blob' || part.mime_type === mimeType)
61
+ if (blobPart === null || part?.type !== 'blob')
61
62
  return;
62
- part.mime_type = mimeType;
63
+ if (part.mime_type === blobPart.mime_type && part.content === blobPart.content)
64
+ return;
65
+ part.mime_type = blobPart.mime_type;
66
+ part.content = blobPart.content;
63
67
  repaired = true;
64
68
  });
65
69
  });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Reasoning effort capture for the OpenAI chat endpoint.
3
+ *
4
+ * A chat request asks a model to think with `reasoning_effort` ("low",
5
+ * "medium", ...). OpenLLMetry's Python instrumentor stamps it as
6
+ * `gen_ai.request.reasoning_effort`; `@traceloop/instrumentation-openai` (Node)
7
+ * records nothing, so the stored call would not say how hard the model was
8
+ * asked to think. This stamps the same attribute when the span opens, in the
9
+ * `startSpan({ params })` hook the structured-output complement uses. The
10
+ * Responses endpoint's `reasoning.effort` is stamped by `get_responses_telemetry.ts`.
11
+ */
12
+ export declare class OpenAIReasoningEffortInstrumentor {
13
+ private _patched;
14
+ instrument(instrumentation: any): void;
15
+ uninstrument(): void;
16
+ }
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ /**
3
+ * Reasoning effort capture for the OpenAI chat endpoint.
4
+ *
5
+ * A chat request asks a model to think with `reasoning_effort` ("low",
6
+ * "medium", ...). OpenLLMetry's Python instrumentor stamps it as
7
+ * `gen_ai.request.reasoning_effort`; `@traceloop/instrumentation-openai` (Node)
8
+ * records nothing, so the stored call would not say how hard the model was
9
+ * asked to think. This stamps the same attribute when the span opens, in the
10
+ * `startSpan({ params })` hook the structured-output complement uses. The
11
+ * Responses endpoint's `reasoning.effort` is stamped by `get_responses_telemetry.ts`.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.OpenAIReasoningEffortInstrumentor = void 0;
15
+ const REASONING_EFFORT_ATTR = 'gen_ai.request.reasoning_effort';
16
+ class OpenAIReasoningEffortInstrumentor {
17
+ // [owner, methodName, originalFunction] for uninstrument().
18
+ _patched = [];
19
+ // No-op (and never throws) if the instrumentor's internals have changed shape:
20
+ // the worst case is a missing reasoning effort, never a crash.
21
+ instrument(instrumentation) {
22
+ if (!instrumentation)
23
+ return;
24
+ const startSpanOriginal = instrumentation.startSpan;
25
+ if (typeof startSpanOriginal !== 'function')
26
+ return;
27
+ instrumentation.startSpan = function startSpanWithReasoningEffort(request) {
28
+ const span = startSpanOriginal.call(this, request);
29
+ try {
30
+ const reasoningEffort = request?.params?.reasoning_effort;
31
+ if (typeof reasoningEffort === 'string' && reasoningEffort && span?.isRecording?.()) {
32
+ span.setAttribute(REASONING_EFFORT_ATTR, reasoningEffort);
33
+ }
34
+ }
35
+ catch {
36
+ // Telemetry must never break the app's LLM call.
37
+ }
38
+ return span;
39
+ };
40
+ this._patched.push([instrumentation, 'startSpan', startSpanOriginal]);
41
+ }
42
+ uninstrument() {
43
+ for (const [owner, methodName, original] of this._patched) {
44
+ owner[methodName] = original;
45
+ }
46
+ this._patched = [];
47
+ }
48
+ }
49
+ exports.OpenAIReasoningEffortInstrumentor = OpenAIReasoningEffortInstrumentor;
@@ -9,8 +9,8 @@
9
9
  * 1. Input messages. Upstream stringifies the whole `input` into a single
10
10
  * `{role: "user", parts: [{type: "text", content: "<JSON>"}]}` part. That
11
11
  * loses every role and part type, and it buries an inline image or
12
- * document's base64 inside a text string, where `strip-media-payloads`
13
- * cannot see it and the payload would reach the queue. This rebuilds the
12
+ * document's base64 inside a text string, where the ingestion API
13
+ * cannot find it. This rebuilds the
14
14
  * per-role, per-part shape the Python instrumentor emits.
15
15
  * 2. Response side. Upstream records only the output text, so a tool call or
16
16
  * a reasoning summary is lost, the finish reason is never `tool_call`, and
@@ -34,12 +34,12 @@
34
34
  * (`weflayr.document_search.requests`).
35
35
  *
36
36
  * Inline media is the one deliberate divergence from the Python instrumentor's
37
- * shape: an `input_file` carrying `file_data` becomes the byte-less
38
- * `{type: "blob", mime_type}` stub rather than upstream's
39
- * `{type: "file", file_id, filename}`, so an inline PDF pins the same stub here
37
+ * shape: an `input_file` carrying `file_data` becomes the
38
+ * `{type: "blob", mime_type, content}` part rather than upstream's
39
+ * `{type: "file", file_id, filename}`, so an inline PDF pins the same part here
40
40
  * as it does on the chat endpoint. The Python complement makes the same repair.
41
- * An `input_image` keeps its `data:` URI in a `uri` part, which the shared
42
- * `strip-media-payloads` step already rewrites to that stub.
41
+ * An `input_image` keeps its `data:` URI in a `uri` part, which the ingestion
42
+ * API moves to S3 like any other inline media.
43
43
  */
44
44
  import { type Span } from '@opentelemetry/api';
45
45
  type JsonObject = Record<string, any>;
@@ -10,8 +10,8 @@
10
10
  * 1. Input messages. Upstream stringifies the whole `input` into a single
11
11
  * `{role: "user", parts: [{type: "text", content: "<JSON>"}]}` part. That
12
12
  * loses every role and part type, and it buries an inline image or
13
- * document's base64 inside a text string, where `strip-media-payloads`
14
- * cannot see it and the payload would reach the queue. This rebuilds the
13
+ * document's base64 inside a text string, where the ingestion API
14
+ * cannot find it. This rebuilds the
15
15
  * per-role, per-part shape the Python instrumentor emits.
16
16
  * 2. Response side. Upstream records only the output text, so a tool call or
17
17
  * a reasoning summary is lost, the finish reason is never `tool_call`, and
@@ -35,12 +35,12 @@
35
35
  * (`weflayr.document_search.requests`).
36
36
  *
37
37
  * Inline media is the one deliberate divergence from the Python instrumentor's
38
- * shape: an `input_file` carrying `file_data` becomes the byte-less
39
- * `{type: "blob", mime_type}` stub rather than upstream's
40
- * `{type: "file", file_id, filename}`, so an inline PDF pins the same stub here
38
+ * shape: an `input_file` carrying `file_data` becomes the
39
+ * `{type: "blob", mime_type, content}` part rather than upstream's
40
+ * `{type: "file", file_id, filename}`, so an inline PDF pins the same part here
41
41
  * as it does on the chat endpoint. The Python complement makes the same repair.
42
- * An `input_image` keeps its `data:` URI in a `uri` part, which the shared
43
- * `strip-media-payloads` step already rewrites to that stub.
42
+ * An `input_image` keeps its `data:` URI in a `uri` part, which the ingestion
43
+ * API moves to S3 like any other inline media.
44
44
  */
45
45
  Object.defineProperty(exports, "__esModule", { value: true });
46
46
  exports.OpenAIResponsesInstrumentor = void 0;
@@ -106,7 +106,7 @@ function parseArguments(rawArguments) {
106
106
  *
107
107
  * Example: `{type: "input_text", text: "hi"}` -> `{type: "text", content: "hi"}`;
108
108
  * `{type: "input_file", filename: "a.pdf", file_data: "data:application/pdf;base64,..."}`
109
- * -> `{type: "blob", mime_type: "application/pdf"}`.
109
+ * -> `{type: "blob", mime_type: "application/pdf", content: "..."}`.
110
110
  */
111
111
  function buildInputPart(block) {
112
112
  if (!isJsonObject(block)) {
@@ -123,9 +123,9 @@ function buildInputPart(block) {
123
123
  return { type: 'file', modality: 'image', file_id: block.file_id ?? null };
124
124
  }
125
125
  if (blockType === 'file' || blockType === 'input_file' || blockType === 'output_file') {
126
- const mimeType = (0, get_data_uri_mime_type_1.getDataUriMimeType)(String(block.file_data ?? ''));
127
- if (mimeType !== null) {
128
- return { type: 'blob', mime_type: mimeType };
126
+ const blobPart = (0, get_data_uri_mime_type_1.getDataUriBlobPart)(String(block.file_data ?? ''));
127
+ if (blobPart !== null) {
128
+ return blobPart;
129
129
  }
130
130
  return {
131
131
  type: 'file',
@@ -6,8 +6,11 @@
6
6
  * final usage chunk OpenAI sends when the request sets
7
7
  * `stream_options: { include_usage: true }`. Non-streaming calls work because
8
8
  * OpenAI returns `usage` in the response body, which the instrumentor reads
9
- * directly. The Python OpenLLMetry instrumentor reads the streamed usage, so
10
- * this gap is Node-only.
9
+ * directly. The Python OpenLLMetry instrumentor reads the streamed usage but
10
+ * for its reasoning-token count, which the Python SDK's own
11
+ * `get_streaming_usage.py` stamps. The chunk's reasoning-token count
12
+ * (`usage.completion_tokens_details.reasoning_tokens`) is stamped too, as the
13
+ * non-streamed path's `get_cache_and_reasoning_telemetry.ts` stamps it.
11
14
  *
12
15
  * It wraps the instrumentor instance's `_streamingWrapPromise`: that handler
13
16
  * hands us the span and ends it only after its chunk loop, so when the usage
@@ -7,8 +7,11 @@
7
7
  * final usage chunk OpenAI sends when the request sets
8
8
  * `stream_options: { include_usage: true }`. Non-streaming calls work because
9
9
  * OpenAI returns `usage` in the response body, which the instrumentor reads
10
- * directly. The Python OpenLLMetry instrumentor reads the streamed usage, so
11
- * this gap is Node-only.
10
+ * directly. The Python OpenLLMetry instrumentor reads the streamed usage but
11
+ * for its reasoning-token count, which the Python SDK's own
12
+ * `get_streaming_usage.py` stamps. The chunk's reasoning-token count
13
+ * (`usage.completion_tokens_details.reasoning_tokens`) is stamped too, as the
14
+ * non-streamed path's `get_cache_and_reasoning_telemetry.ts` stamps it.
12
15
  *
13
16
  * It wraps the instrumentor instance's `_streamingWrapPromise`: that handler
14
17
  * hands us the span and ends it only after its chunk loop, so when the usage
@@ -21,6 +24,7 @@ const USAGE_INPUT_TOKENS_ATTR = 'gen_ai.usage.input_tokens';
21
24
  const USAGE_OUTPUT_TOKENS_ATTR = 'gen_ai.usage.output_tokens';
22
25
  const USAGE_TOTAL_TOKENS_ATTR = 'gen_ai.usage.total_tokens';
23
26
  const CACHE_READ_INPUT_TOKENS_ATTR = 'gen_ai.usage.cache_read_input_tokens';
27
+ const REASONING_TOKENS_ATTR = 'gen_ai.usage.reasoning_tokens';
24
28
  function _setUsageAttributes(span, usage) {
25
29
  if (typeof usage.prompt_tokens === 'number') {
26
30
  span.setAttribute(USAGE_INPUT_TOKENS_ATTR, usage.prompt_tokens);
@@ -37,6 +41,10 @@ function _setUsageAttributes(span, usage) {
37
41
  if (cachedTokens) {
38
42
  span.setAttribute(CACHE_READ_INPUT_TOKENS_ATTR, cachedTokens);
39
43
  }
44
+ const reasoningTokens = usage.completion_tokens_details && usage.completion_tokens_details.reasoning_tokens;
45
+ if (reasoningTokens) {
46
+ span.setAttribute(REASONING_TOKENS_ATTR, reasoningTokens);
47
+ }
40
48
  }
41
49
  class OpenAIStreamingUsageInstrumentor {
42
50
  // [instrumentation, methodName, originalFunction] for uninstrument().
@@ -19,7 +19,8 @@
19
19
  * OpenAI-compatible API (Alibaba Model Studio, xAI), and
20
20
  * - the billing of the calls to Vertex AI's OpenAI-compatible endpoint, and
21
21
  * - a `reasoning_content` output part for any provider that returns one, and
22
- * - the service tier the call was served in (flex, priority, ...).
22
+ * - the service tier the call was served in (flex, priority, ...), and
23
+ * - the reasoning effort a chat request asks for.
23
24
  */
24
25
  export declare class OpenAITelemetryInstrumentor {
25
26
  private _subInstrumentors;
@@ -20,7 +20,8 @@
20
20
  * OpenAI-compatible API (Alibaba Model Studio, xAI), and
21
21
  * - the billing of the calls to Vertex AI's OpenAI-compatible endpoint, and
22
22
  * - a `reasoning_content` output part for any provider that returns one, and
23
- * - the service tier the call was served in (flex, priority, ...).
23
+ * - the service tier the call was served in (flex, priority, ...), and
24
+ * - the reasoning effort a chat request asks for.
24
25
  */
25
26
  Object.defineProperty(exports, "__esModule", { value: true });
26
27
  exports.OpenAITelemetryInstrumentor = void 0;
@@ -33,6 +34,7 @@ const get_openai_compatible_provider_telemetry_1 = require("./get_openai_compati
33
34
  const get_responses_telemetry_1 = require("./get_responses_telemetry");
34
35
  const get_embeddings_telemetry_1 = require("./get_embeddings_telemetry");
35
36
  const get_reasoning_content_telemetry_1 = require("./get_reasoning_content_telemetry");
37
+ const get_reasoning_effort_telemetry_1 = require("./get_reasoning_effort_telemetry");
36
38
  const get_service_tier_telemetry_1 = require("./get_service_tier_telemetry");
37
39
  const get_streaming_usage_1 = require("./get_streaming_usage");
38
40
  const get_cache_and_reasoning_telemetry_1 = require("./get_cache_and_reasoning_telemetry");
@@ -57,6 +59,7 @@ class OpenAITelemetryInstrumentor {
57
59
  new get_vertex_openai_compatible_telemetry_1.VertexOpenAICompatibleInstrumentor(),
58
60
  new get_reasoning_content_telemetry_1.OpenAIReasoningContentInstrumentor(),
59
61
  new get_service_tier_telemetry_1.OpenAIServiceTierInstrumentor(),
62
+ new get_reasoning_effort_telemetry_1.OpenAIReasoningEffortInstrumentor(),
60
63
  new get_structured_output_schema_telemetry_1.TraceloopStructuredOutputSchemaInstrumentor(get_structured_output_schema_telemetry_2.readStructuredOutputSchema),
61
64
  ];
62
65
  }
@@ -1,6 +1,6 @@
1
1
  import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
2
2
  import type { ExportResult } from '@opentelemetry/core';
3
- import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
3
+ import type { ReadableSpan } from '@opentelemetry/sdk-trace';
4
4
  declare const _RETRYABLE_STATUS: Set<number>;
5
5
  type ExportFn = (spans: ReadableSpan[], resultCallback: (result: ExportResult) => void) => void;
6
6
  export declare function _exportWithRetry(originalExport: ExportFn, { maxRetries, maxRetryMs }: {
@@ -1,4 +1,4 @@
1
- import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
1
+ import type { ReadableSpan } from '@opentelemetry/sdk-trace';
2
2
  export declare const SENT_TO_WEFLAYR_GATEWAY_ATTR = "weflayr.sent_to_weflayr_gateway";
3
3
  export declare function isKnownLLMInstrumentor(span: ReadableSpan): boolean;
4
4
  export declare function wasSentToWeflayrGateway(span: ReadableSpan): boolean;
@@ -1,5 +1,5 @@
1
1
  import { type Context, type Span } from '@opentelemetry/api';
2
- import { type ReadableSpan, type SpanExporter, type SpanProcessor } from '@opentelemetry/sdk-trace-base';
2
+ import { type ReadableSpan, type SpanExporter, type SpanProcessor } from '@opentelemetry/sdk-trace';
3
3
  export declare const CONTENT_CAPTURE_ENV = "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT";
4
4
  /** Arbitrary key-value tags applied to OTel spans. */
5
5
  export type Tags = Record<string, string | number | boolean>;
@@ -17,10 +17,9 @@
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.WeflayrSpanProcessor = exports.CONTENT_CAPTURE_ENV = void 0;
19
19
  const api_1 = require("@opentelemetry/api");
20
- const sdk_trace_base_1 = require("@opentelemetry/sdk-trace-base");
20
+ const sdk_trace_1 = require("@opentelemetry/sdk-trace");
21
21
  const propagation_1 = require("./propagation");
22
22
  const span_filter_1 = require("./span-filter");
23
- const strip_media_payloads_1 = require("./strip-media-payloads");
24
23
  const logger_1 = require("../logger");
25
24
  // setup({captureMessageContent: ...}) records the choice in this env var (the
26
25
  // official OTel GenAI one). Instrumentors gate content on their own knobs
@@ -74,8 +73,8 @@ class WeflayrSpanProcessor {
74
73
  }
75
74
  this._delegate =
76
75
  exportMode === 'immediate'
77
- ? new sdk_trace_base_1.SimpleSpanProcessor(exporter)
78
- : new sdk_trace_base_1.BatchSpanProcessor(exporter);
76
+ ? new sdk_trace_1.SimpleSpanProcessor({ exporter })
77
+ : new sdk_trace_1.BatchSpanProcessor({ exporter });
79
78
  this._defaultTags = defaultTags;
80
79
  this._mask = mask;
81
80
  this._maskFunctionSource = mask ? String(mask) : null;
@@ -115,9 +114,6 @@ class WeflayrSpanProcessor {
115
114
  delete span.attributes[key];
116
115
  }
117
116
  }
118
- else {
119
- (0, strip_media_payloads_1.stripMediaPayloads)(span.attributes);
120
- }
121
117
  if (this._mask) {
122
118
  try {
123
119
  const masked = this._mask(span.attributes);
@@ -2,10 +2,15 @@
2
2
  /**
3
3
  * Send the metadata of a call to the Weflayr AI gateway in a request header.
4
4
  */
5
+ var __importDefault = (this && this.__importDefault) || function (mod) {
6
+ return (mod && mod.__esModule) ? mod : { "default": mod };
7
+ };
5
8
  Object.defineProperty(exports, "__esModule", { value: true });
6
9
  exports._state = exports.PUBLIC_GATEWAY_HOST = exports.WEFLAYR_METADATA_HEADER = void 0;
7
10
  exports.buildMetadataHeaderValue = buildMetadataHeaderValue;
8
11
  exports.enablePropagateMetadataToGateway = enablePropagateMetadataToGateway;
12
+ const node_fs_1 = __importDefault(require("node:fs"));
13
+ const node_path_1 = __importDefault(require("node:path"));
9
14
  const api_1 = require("@opentelemetry/api");
10
15
  const logger_1 = require("./logger");
11
16
  const propagation_1 = require("./otel/propagation");
@@ -155,6 +160,30 @@ function addMetadataHeader(client, options) {
155
160
  (0, logger_1.debug)('enablePropagateMetadataToGateway: could not add the metadata header');
156
161
  }
157
162
  }
163
+ // The ES module build sits next to the CommonJS one (`index.mjs` next to
164
+ // `index.js`), as openai's package exports name it.
165
+ function requireEsmOpenAI() {
166
+ const esmEntryPath = node_path_1.default.join(node_path_1.default.dirname(require.resolve('openai')), 'index.mjs');
167
+ // An openai version with no ES module build has only its CommonJS class.
168
+ if (!node_fs_1.default.existsSync(esmEntryPath)) {
169
+ return undefined;
170
+ }
171
+ return require(esmEntryPath).OpenAI;
172
+ }
173
+ function patchPrepareOptions(OpenAI) {
174
+ // `prepareOptions` is the openai client's documented hook for mutating a
175
+ // request's options, and it runs for every endpoint, streaming or not.
176
+ const originalPrepareOptions = OpenAI.prototype.prepareOptions;
177
+ if (typeof originalPrepareOptions !== 'function') {
178
+ throw new Error('weflayr.enablePropagateMetadataToGateway: the openai client has no ' +
179
+ 'prepareOptions hook, so the installed openai version cannot send the ' +
180
+ 'metadata header.');
181
+ }
182
+ OpenAI.prototype.prepareOptions = async function prepareOptions(options) {
183
+ await originalPrepareOptions.call(this, options);
184
+ addMetadataHeader(this, options);
185
+ };
186
+ }
158
187
  /**
159
188
  * Send `propagateMetadata` and `setCallName` values to the Weflayr AI gateway.
160
189
  *
@@ -196,18 +225,16 @@ function enablePropagateMetadataToGateway(gatewayBaseUrl) {
196
225
  'call the Weflayr AI gateway. Install it with: npm install openai\n' +
197
226
  `Underlying error: ${err.message}`);
198
227
  }
199
- // `prepareOptions` is the openai client's documented hook for mutating a
200
- // request's options, and it runs for every endpoint, streaming or not.
201
- const originalPrepareOptions = OpenAI.prototype.prepareOptions;
202
- if (typeof originalPrepareOptions !== 'function') {
203
- throw new Error('weflayr.enablePropagateMetadataToGateway: the openai client has no ' +
204
- 'prepareOptions hook, so the installed openai version cannot send the ' +
205
- 'metadata header.');
228
+ // openai ships a CommonJS and an ES module build, each with its own client
229
+ // class: an app writing `import OpenAI from 'openai'` gets the ES module one,
230
+ // which `require` never returns. Node loads that build with `require` too.
231
+ // It is loaded before any class is patched, so a build failing to load
232
+ // leaves both unpatched and a later call patches each once.
233
+ const esmOpenAI = requireEsmOpenAI();
234
+ patchPrepareOptions(OpenAI);
235
+ if (esmOpenAI !== undefined && esmOpenAI !== OpenAI) {
236
+ patchPrepareOptions(esmOpenAI);
206
237
  }
207
- OpenAI.prototype.prepareOptions = async function prepareOptions(options) {
208
- await originalPrepareOptions.call(this, options);
209
- addMetadataHeader(this, options);
210
- };
211
238
  exports._state.openaiClientsPatched = true;
212
239
  (0, logger_1.debug)('enablePropagateMetadataToGateway: patched the openai client gatewayHostAndPort=%s', exports._state.gatewayHostAndPort ?? exports.PUBLIC_GATEWAY_HOST);
213
240
  }
package/dist/src/setup.js CHANGED
@@ -4,7 +4,9 @@ exports._state = void 0;
4
4
  exports.setup = setup;
5
5
  exports.flush = flush;
6
6
  const api_1 = require("@opentelemetry/api");
7
- const sdk_trace_node_1 = require("@opentelemetry/sdk-trace-node");
7
+ const context_async_hooks_1 = require("@opentelemetry/context-async-hooks");
8
+ const core_1 = require("@opentelemetry/core");
9
+ const sdk_trace_1 = require("@opentelemetry/sdk-trace");
8
10
  const resources_1 = require("@opentelemetry/resources");
9
11
  const exporter_1 = require("./otel/exporter");
10
12
  const span_processor_1 = require("./otel/span-processor");
@@ -18,7 +20,7 @@ exports._state = {
18
20
  beforeExitListener: null,
19
21
  };
20
22
  // `trace.getTracerProvider()` returns a ProxyTracerProvider that forwards to
21
- // whoever called `.register()` last. Unwrap it to reach the real provider.
23
+ // whoever called `trace.setGlobalTracerProvider()` first. Unwrap it to reach the real provider.
22
24
  // The delegate/registration internals aren't part of the public TracerProvider
23
25
  // type, so this reaches into them dynamically.
24
26
  function _unwrapTracerProvider(p) {
@@ -28,7 +30,7 @@ function _unwrapTracerProvider(p) {
28
30
  }
29
31
  return p;
30
32
  }
31
- // `NodeTracerProvider` 2.x dropped the public `addSpanProcessor` API —
33
+ // The SDK `TracerProvider` has no public `addSpanProcessor` API —
32
34
  // processors are construction-time only. To attach to a provider somebody
33
35
  // else owns, we splice into the internal MultiSpanProcessor. This matches
34
36
  // the actual public OTel JS guidance for composing post-construction.
@@ -47,6 +49,16 @@ function _attachProcessorToExistingProvider(provider, processor) {
47
49
  }
48
50
  return false;
49
51
  }
52
+ // Registers the provider, an AsyncLocalStorage context manager (which
53
+ // `propagateMetadata` relies on) and the W3C trace context + baggage
54
+ // propagators. Each global is kept if something else already set it.
55
+ function _registerGlobally(tracerProvider) {
56
+ api_1.trace.setGlobalTracerProvider(tracerProvider);
57
+ api_1.context.setGlobalContextManager(new context_async_hooks_1.AsyncLocalStorageContextManager().enable());
58
+ api_1.propagation.setGlobalPropagator(new core_1.CompositePropagator({
59
+ propagators: [new core_1.W3CTraceContextPropagator(), new core_1.W3CBaggagePropagator()],
60
+ }));
61
+ }
50
62
  /**
51
63
  * Configure the Weflayr SDK.
52
64
  *
@@ -88,7 +100,7 @@ function setup(options) {
88
100
  });
89
101
  // Adopt an existing TracerProvider when there is one — e.g. the user already
90
102
  // configured Datadog/Honeycomb/OTel, or called weflayr.setup() before.
91
- // provider.register() silently refuses to override the proxy if another
103
+ // trace.setGlobalTracerProvider() silently refuses to override the proxy if another
92
104
  // provider is already wired in, so naively creating a new provider would
93
105
  // leave our processor attached to a dead one and we'd drop every LLM span.
94
106
  const currentTracerProvider = _unwrapTracerProvider(api_1.trace.getTracerProvider());
@@ -97,15 +109,15 @@ function setup(options) {
97
109
  tracerProvider = currentTracerProvider;
98
110
  }
99
111
  else {
100
- const nodeTracerProvider = new sdk_trace_node_1.NodeTracerProvider({
112
+ const sdkTracerProvider = new sdk_trace_1.TracerProvider({
101
113
  resource: (0, resources_1.resourceFromAttributes)({
102
114
  sdk_version: package_json_1.version,
103
115
  sdk_language: SDK_LANGUAGE,
104
116
  }),
105
117
  spanProcessors: [processor],
106
118
  });
107
- nodeTracerProvider.register();
108
- tracerProvider = nodeTracerProvider;
119
+ _registerGlobally(sdkTracerProvider);
120
+ tracerProvider = sdkTracerProvider;
109
121
  }
110
122
  // beforeExit fires when the event loop empties on a clean exit, giving us a
111
123
  // last async tick to drain buffered spans. It does NOT fire on process.exit()