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.
- package/dist/package.json +9 -9
- package/dist/src/api/generated/facade.gen.d.ts +1436 -10
- package/dist/src/api/generated/facade.gen.js +24 -1
- package/dist/src/api/generated/index.d.ts +2 -2
- package/dist/src/api/generated/index.js +24 -1
- package/dist/src/api/generated/sdk.gen.d.ts +97 -5
- package/dist/src/api/generated/sdk.gen.js +256 -5
- package/dist/src/api/generated/types.gen.d.ts +4527 -741
- package/dist/src/get-data-uri-mime-type.d.ts +8 -0
- package/dist/src/get-data-uri-mime-type.js +18 -0
- package/dist/src/instrumentation/anthropic/get_streaming_accumulation_telemetry.d.ts +6 -4
- package/dist/src/instrumentation/anthropic/get_streaming_accumulation_telemetry.js +22 -5
- package/dist/src/instrumentation/bedrock/{get_cache_telemetry.d.ts → get_cache_and_reasoning_telemetry.d.ts} +7 -0
- package/dist/src/instrumentation/bedrock/{get_cache_telemetry.js → get_cache_and_reasoning_telemetry.js} +25 -2
- package/dist/src/instrumentation/bedrock/get_input_message_content_telemetry.js +23 -16
- package/dist/src/instrumentation/bedrock/get_streaming_output_message_telemetry.d.ts +6 -0
- package/dist/src/instrumentation/bedrock/get_streaming_output_message_telemetry.js +18 -2
- package/dist/src/instrumentation/bedrock/set_telemetry_attributes.js +2 -2
- package/dist/src/instrumentation/cohere/get_input_message_content_telemetry.d.ts +2 -3
- package/dist/src/instrumentation/cohere/get_input_message_content_telemetry.js +6 -10
- package/dist/src/instrumentation/google/get_cache_operations_telemetry.js +7 -3
- package/dist/src/instrumentation/mistral/full_instrumentation.d.ts +10 -4
- package/dist/src/instrumentation/mistral/full_instrumentation.js +47 -14
- package/dist/src/instrumentation/mistral/get_input_message_content_telemetry.d.ts +1 -1
- package/dist/src/instrumentation/mistral/get_input_message_content_telemetry.js +5 -8
- package/dist/src/instrumentation/mistral/get_streamed_reasoning_telemetry.d.ts +38 -0
- package/dist/src/instrumentation/mistral/get_streamed_reasoning_telemetry.js +72 -0
- package/dist/src/instrumentation/openai/get_file_part_mime_type_telemetry.d.ts +5 -4
- package/dist/src/instrumentation/openai/get_file_part_mime_type_telemetry.js +14 -10
- package/dist/src/instrumentation/openai/get_reasoning_effort_telemetry.d.ts +16 -0
- package/dist/src/instrumentation/openai/get_reasoning_effort_telemetry.js +49 -0
- package/dist/src/instrumentation/openai/get_responses_telemetry.d.ts +7 -7
- package/dist/src/instrumentation/openai/get_responses_telemetry.js +11 -11
- package/dist/src/instrumentation/openai/get_streaming_usage.d.ts +5 -2
- package/dist/src/instrumentation/openai/get_streaming_usage.js +10 -2
- package/dist/src/instrumentation/openai/set_telemetry_attributes.d.ts +2 -1
- package/dist/src/instrumentation/openai/set_telemetry_attributes.js +4 -1
- package/dist/src/otel/exporter.d.ts +1 -1
- package/dist/src/otel/span-filter.d.ts +1 -1
- package/dist/src/otel/span-processor.d.ts +1 -1
- package/dist/src/otel/span-processor.js +3 -7
- package/dist/src/propagate-metadata-to-gateway.js +38 -11
- package/dist/src/setup.js +19 -7
- package/package.json +9 -9
- package/dist/src/otel/strip-media-payloads.d.ts +0 -26
- 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
|
|
8
|
-
* the caller sent (`data:application/pdf;base64,...`), and the
|
|
9
|
-
*
|
|
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
|
|
9
|
-
* the caller sent (`data:application/pdf;base64,...`), and the
|
|
10
|
-
*
|
|
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
|
|
28
|
-
function
|
|
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.
|
|
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
|
|
59
|
+
const blobPart = getBlockBlobPart(block);
|
|
59
60
|
const part = asObject(parts[blockIndex]);
|
|
60
|
-
if (
|
|
61
|
+
if (blobPart === null || part?.type !== 'blob')
|
|
61
62
|
return;
|
|
62
|
-
part.mime_type
|
|
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
|
|
13
|
-
* cannot
|
|
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
|
|
38
|
-
* `{type: "blob", mime_type}`
|
|
39
|
-
* `{type: "file", file_id, filename}`, so an inline PDF pins the same
|
|
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
|
|
42
|
-
*
|
|
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
|
|
14
|
-
* cannot
|
|
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
|
|
39
|
-
* `{type: "blob", mime_type}`
|
|
40
|
-
* `{type: "file", file_id, filename}`, so an inline PDF pins the same
|
|
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
|
|
43
|
-
*
|
|
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
|
|
127
|
-
if (
|
|
128
|
-
return
|
|
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
|
|
10
|
-
*
|
|
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
|
|
11
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
78
|
-
: new
|
|
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
|
-
//
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
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
|
|
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
|
-
// `
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
108
|
-
tracerProvider =
|
|
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()
|