weflayr 0.30.0 → 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/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_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/mistral/full_instrumentation.d.ts +10 -4
- package/dist/src/instrumentation/mistral/full_instrumentation.js +47 -14
- 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_streaming_usage.d.ts +5 -2
- package/dist/src/instrumentation/openai/get_streaming_usage.js +10 -2
- 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 -3
- package/dist/src/propagate-metadata-to-gateway.js +38 -11
- package/dist/src/setup.js +19 -7
- package/package.json +9 -9
|
@@ -2,20 +2,22 @@
|
|
|
2
2
|
* Streamed-accumulation gap filling for the Anthropic chat span.
|
|
3
3
|
*
|
|
4
4
|
* `@traceloop/instrumentation-anthropic` accumulates a streamed response into a
|
|
5
|
-
* result object but drops
|
|
5
|
+
* result object but drops three things on the way:
|
|
6
6
|
* - the `message_delta` chunk's `delta.stop_reason`, so streamed spans format
|
|
7
|
-
* their output message with an empty `finish_reason`,
|
|
7
|
+
* their output message with an empty `finish_reason`,
|
|
8
8
|
* - the `content_block_delta` chunks' `input_json_delta` fragments, so a
|
|
9
9
|
* streamed tool call's `tool_call` part carries no `arguments`. This covers
|
|
10
10
|
* both a caller's own `tool_use` block and the `server_tool_use` block of a
|
|
11
|
-
* provider-run web search, whose query streams the same way
|
|
11
|
+
* provider-run web search, whose query streams the same way, and
|
|
12
|
+
* - the `content_block_delta` chunks' `thinking_delta` text, so a streamed
|
|
13
|
+
* thinking block's `reasoning` part is empty.
|
|
12
14
|
*
|
|
13
15
|
* `_streamingWrapPromise(client, moduleExports, { span, type, promise })`
|
|
14
16
|
* returns `new moduleExports.APIPromise(...)` whose parse step wraps the real
|
|
15
17
|
* SDK stream with the instrumentor's accumulating iterator. This complement
|
|
16
18
|
* hands it a patched `moduleExports` whose APIPromise interposes on that parse
|
|
17
19
|
* step, observing the chunks (outside the accumulating iterator, so every chunk
|
|
18
|
-
* is seen before the iterator finishes) to capture
|
|
20
|
+
* is seen before the iterator finishes) to capture them, which are then
|
|
19
21
|
* injected into the accumulated result just before `_endSpan` formats the
|
|
20
22
|
* output message.
|
|
21
23
|
*/
|
|
@@ -3,20 +3,22 @@
|
|
|
3
3
|
* Streamed-accumulation gap filling for the Anthropic chat span.
|
|
4
4
|
*
|
|
5
5
|
* `@traceloop/instrumentation-anthropic` accumulates a streamed response into a
|
|
6
|
-
* result object but drops
|
|
6
|
+
* result object but drops three things on the way:
|
|
7
7
|
* - the `message_delta` chunk's `delta.stop_reason`, so streamed spans format
|
|
8
|
-
* their output message with an empty `finish_reason`,
|
|
8
|
+
* their output message with an empty `finish_reason`,
|
|
9
9
|
* - the `content_block_delta` chunks' `input_json_delta` fragments, so a
|
|
10
10
|
* streamed tool call's `tool_call` part carries no `arguments`. This covers
|
|
11
11
|
* both a caller's own `tool_use` block and the `server_tool_use` block of a
|
|
12
|
-
* provider-run web search, whose query streams the same way
|
|
12
|
+
* provider-run web search, whose query streams the same way, and
|
|
13
|
+
* - the `content_block_delta` chunks' `thinking_delta` text, so a streamed
|
|
14
|
+
* thinking block's `reasoning` part is empty.
|
|
13
15
|
*
|
|
14
16
|
* `_streamingWrapPromise(client, moduleExports, { span, type, promise })`
|
|
15
17
|
* returns `new moduleExports.APIPromise(...)` whose parse step wraps the real
|
|
16
18
|
* SDK stream with the instrumentor's accumulating iterator. This complement
|
|
17
19
|
* hands it a patched `moduleExports` whose APIPromise interposes on that parse
|
|
18
20
|
* step, observing the chunks (outside the accumulating iterator, so every chunk
|
|
19
|
-
* is seen before the iterator finishes) to capture
|
|
21
|
+
* is seen before the iterator finishes) to capture them, which are then
|
|
20
22
|
* injected into the accumulated result just before `_endSpan` formats the
|
|
21
23
|
* output message.
|
|
22
24
|
*/
|
|
@@ -36,7 +38,7 @@ class AnthropicStreamingAccumulationInstrumentor {
|
|
|
36
38
|
function gapsFor(span) {
|
|
37
39
|
let gaps = gapsBySpan.get(span);
|
|
38
40
|
if (!gaps) {
|
|
39
|
-
gaps = { toolArgumentsJsonByIndex: new Map() };
|
|
41
|
+
gaps = { toolArgumentsJsonByIndex: new Map(), thinkingByIndex: new Map() };
|
|
40
42
|
gapsBySpan.set(span, gaps);
|
|
41
43
|
}
|
|
42
44
|
return gaps;
|
|
@@ -58,6 +60,14 @@ class AnthropicStreamingAccumulationInstrumentor {
|
|
|
58
60
|
const argumentsByIndex = gapsFor(span).toolArgumentsJsonByIndex;
|
|
59
61
|
argumentsByIndex.set(chunk.index, (argumentsByIndex.get(chunk.index) ?? '') + chunk.delta.partial_json);
|
|
60
62
|
}
|
|
63
|
+
if (chunk &&
|
|
64
|
+
chunk.type === 'content_block_delta' &&
|
|
65
|
+
chunk.delta &&
|
|
66
|
+
chunk.delta.type === 'thinking_delta' &&
|
|
67
|
+
typeof chunk.delta.thinking === 'string') {
|
|
68
|
+
const thinkingByIndex = gapsFor(span).thinkingByIndex;
|
|
69
|
+
thinkingByIndex.set(chunk.index, (thinkingByIndex.get(chunk.index) ?? '') + chunk.delta.thinking);
|
|
70
|
+
}
|
|
61
71
|
}
|
|
62
72
|
catch {
|
|
63
73
|
// Never break the caller's stream over an enrichment failure.
|
|
@@ -124,6 +134,13 @@ class AnthropicStreamingAccumulationInstrumentor {
|
|
|
124
134
|
}
|
|
125
135
|
}
|
|
126
136
|
}
|
|
137
|
+
for (const [index, thinking] of gaps.thinkingByIndex) {
|
|
138
|
+
const block = Array.isArray(args.result.content) && args.result.content[index];
|
|
139
|
+
// Only fill the gap, as for the tool arguments above.
|
|
140
|
+
if (block && block.type === 'thinking' && !block.thinking) {
|
|
141
|
+
block.thinking = thinking;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
127
144
|
}
|
|
128
145
|
}
|
|
129
146
|
catch {
|
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
* It wraps the instrumentor instance's `_wrapPromise(span, promise, modelId)`,
|
|
15
15
|
* which resolves the send promise and enriches the span before ending it,
|
|
16
16
|
* letting us read the decoded body off the same response while the span is open.
|
|
17
|
+
*
|
|
18
|
+
* Claude on Bedrock also reports its extended-thinking count, a part of
|
|
19
|
+
* `output_tokens`, as `usage.output_tokens_details.thinking_tokens` in the
|
|
20
|
+
* response body, and in the closing `message_delta` chunk of a stream. The
|
|
21
|
+
* instrumentor drops it, so it is stamped as
|
|
22
|
+
* `gen_ai.usage.output_tokens_details.thinking_tokens`, the key the Anthropic
|
|
23
|
+
* complement stamps, matching the Python SDK.
|
|
17
24
|
*/
|
|
18
25
|
export declare class BedrockPromptCachingInstrumentor {
|
|
19
26
|
private _patched;
|
|
@@ -15,11 +15,19 @@
|
|
|
15
15
|
* It wraps the instrumentor instance's `_wrapPromise(span, promise, modelId)`,
|
|
16
16
|
* which resolves the send promise and enriches the span before ending it,
|
|
17
17
|
* letting us read the decoded body off the same response while the span is open.
|
|
18
|
+
*
|
|
19
|
+
* Claude on Bedrock also reports its extended-thinking count, a part of
|
|
20
|
+
* `output_tokens`, as `usage.output_tokens_details.thinking_tokens` in the
|
|
21
|
+
* response body, and in the closing `message_delta` chunk of a stream. The
|
|
22
|
+
* instrumentor drops it, so it is stamped as
|
|
23
|
+
* `gen_ai.usage.output_tokens_details.thinking_tokens`, the key the Anthropic
|
|
24
|
+
* complement stamps, matching the Python SDK.
|
|
18
25
|
*/
|
|
19
26
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
27
|
exports.BedrockPromptCachingInstrumentor = void 0;
|
|
21
28
|
const CACHE_READ_INPUT_TOKENS_ATTR = 'gen_ai.usage.cache_read_input_tokens';
|
|
22
29
|
const CACHE_CREATION_INPUT_TOKENS_ATTR = 'gen_ai.usage.cache_creation_input_tokens';
|
|
30
|
+
const THINKING_TOKENS_ATTR = 'gen_ai.usage.output_tokens_details.thinking_tokens';
|
|
23
31
|
// Stamp cache counts from a `usage` object onto the span: Nova's camelCase
|
|
24
32
|
// count fields, or Anthropic-on-Bedrock's native snake_case ones.
|
|
25
33
|
function _setCacheFromUsage(span, usage) {
|
|
@@ -34,6 +42,15 @@ function _setCacheFromUsage(span, usage) {
|
|
|
34
42
|
span.setAttribute(CACHE_CREATION_INPUT_TOKENS_ATTR, write);
|
|
35
43
|
}
|
|
36
44
|
}
|
|
45
|
+
// Stamp Claude's thinking count from a `usage` object onto the span.
|
|
46
|
+
function _setThinkingFromUsage(span, usage) {
|
|
47
|
+
if (!usage || !span || !span.isRecording || !span.isRecording())
|
|
48
|
+
return;
|
|
49
|
+
const thinking = usage.output_tokens_details?.thinking_tokens;
|
|
50
|
+
if (typeof thinking === 'number' && thinking) {
|
|
51
|
+
span.setAttribute(THINKING_TOKENS_ATTR, thinking);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
37
54
|
function _setCacheTokens(span, response) {
|
|
38
55
|
if (!span || !span.isRecording || !span.isRecording())
|
|
39
56
|
return;
|
|
@@ -45,7 +62,9 @@ function _setCacheTokens(span, response) {
|
|
|
45
62
|
if (!body || typeof body[Symbol.asyncIterator] === 'function')
|
|
46
63
|
return;
|
|
47
64
|
try {
|
|
48
|
-
|
|
65
|
+
const usage = JSON.parse(new TextDecoder().decode(body)).usage;
|
|
66
|
+
_setCacheFromUsage(span, usage);
|
|
67
|
+
_setThinkingFromUsage(span, usage);
|
|
49
68
|
}
|
|
50
69
|
catch {
|
|
51
70
|
// Never break span enrichment over a parse failure.
|
|
@@ -77,7 +96,8 @@ class BedrockPromptCachingInstrumentor {
|
|
|
77
96
|
// Streamed: the instrumentor hands every chunk to
|
|
78
97
|
// `_handleNovaStreamingMetadata(span, parsedResponse)` but never reads its
|
|
79
98
|
// cache counts: Nova's arrive in the trailing `metadata` chunk, Anthropic's
|
|
80
|
-
// in the opening `message_start` one.
|
|
99
|
+
// in the opening `message_start` one. Claude's thinking count arrives in its
|
|
100
|
+
// closing `message_delta` one.
|
|
81
101
|
const metadataOriginal = instrumentation._handleNovaStreamingMetadata;
|
|
82
102
|
if (typeof metadataOriginal === 'function') {
|
|
83
103
|
instrumentation._handleNovaStreamingMetadata = function handleMetadataWithCache(span, parsedResponse) {
|
|
@@ -86,6 +106,9 @@ class BedrockPromptCachingInstrumentor {
|
|
|
86
106
|
? parsedResponse.message?.usage
|
|
87
107
|
: parsedResponse?.metadata?.usage;
|
|
88
108
|
_setCacheFromUsage(span, usage);
|
|
109
|
+
if (parsedResponse?.type === 'message_delta') {
|
|
110
|
+
_setThinkingFromUsage(span, parsedResponse.usage);
|
|
111
|
+
}
|
|
89
112
|
}
|
|
90
113
|
catch {
|
|
91
114
|
// Never break the caller's stream over an enrichment failure.
|
|
@@ -19,6 +19,12 @@
|
|
|
19
19
|
* a forced tool call streams into an empty message carrying only
|
|
20
20
|
* `finish_reason: "tool_call"`. Upstream accumulates tool blocks on the Converse
|
|
21
21
|
* API path only, never on InvokeModelWithResponseStream.
|
|
22
|
+
*
|
|
23
|
+
* Anthropic's extended thinking streams as `thinking_delta`s, which upstream
|
|
24
|
+
* drops too, so they are accumulated into a `reasoning` part written before the
|
|
25
|
+
* text, the part a non-streamed answer gets (see
|
|
26
|
+
* `get_reasoning_output_telemetry.ts`). A call that only thinks, stopped by its
|
|
27
|
+
* token cap before any text, otherwise records an empty message.
|
|
22
28
|
*/
|
|
23
29
|
export declare class BedrockStreamingOutputMessageInstrumentor {
|
|
24
30
|
private _patched;
|
|
@@ -20,6 +20,12 @@
|
|
|
20
20
|
* a forced tool call streams into an empty message carrying only
|
|
21
21
|
* `finish_reason: "tool_call"`. Upstream accumulates tool blocks on the Converse
|
|
22
22
|
* API path only, never on InvokeModelWithResponseStream.
|
|
23
|
+
*
|
|
24
|
+
* Anthropic's extended thinking streams as `thinking_delta`s, which upstream
|
|
25
|
+
* drops too, so they are accumulated into a `reasoning` part written before the
|
|
26
|
+
* text, the part a non-streamed answer gets (see
|
|
27
|
+
* `get_reasoning_output_telemetry.ts`). A call that only thinks, stopped by its
|
|
28
|
+
* token cap before any text, otherwise records an empty message.
|
|
23
29
|
*/
|
|
24
30
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
31
|
exports.BedrockStreamingOutputMessageInstrumentor = void 0;
|
|
@@ -58,7 +64,8 @@ function _accumulateToolCall(state, parsedResponse) {
|
|
|
58
64
|
class BedrockStreamingOutputMessageInstrumentor {
|
|
59
65
|
// [instrumentation, methodName, originalFunction] for uninstrument().
|
|
60
66
|
_patched = [];
|
|
61
|
-
// span -> {text, stopReason} accumulated across the
|
|
67
|
+
// span -> {reasoning, text, stopReason, toolCalls} accumulated across the
|
|
68
|
+
// stream's chunks.
|
|
62
69
|
_streamStateBySpan = new WeakMap();
|
|
63
70
|
// No-op (and never throws) if the instrumentor's internals have changed shape —
|
|
64
71
|
// the worst case is an empty streamed output message, never a crash.
|
|
@@ -72,6 +79,7 @@ class BedrockStreamingOutputMessageInstrumentor {
|
|
|
72
79
|
instrumentation._handleNovaStreamingMetadata = function handleWithOutputMessage(span, parsedResponse) {
|
|
73
80
|
try {
|
|
74
81
|
const state = streamStateBySpan.get(span) || {
|
|
82
|
+
reasoning: '',
|
|
75
83
|
text: '',
|
|
76
84
|
stopReason: null,
|
|
77
85
|
toolCalls: [],
|
|
@@ -80,6 +88,12 @@ class BedrockStreamingOutputMessageInstrumentor {
|
|
|
80
88
|
(parsedResponse?.type === 'content_block_delta' ? parsedResponse.delta?.text : undefined);
|
|
81
89
|
if (typeof delta === 'string')
|
|
82
90
|
state.text += delta;
|
|
91
|
+
const thinkingDelta = parsedResponse?.type === 'content_block_delta' &&
|
|
92
|
+
parsedResponse.delta?.type === 'thinking_delta'
|
|
93
|
+
? parsedResponse.delta.thinking
|
|
94
|
+
: undefined;
|
|
95
|
+
if (typeof thinkingDelta === 'string')
|
|
96
|
+
state.reasoning += thinkingDelta;
|
|
83
97
|
const stopReason = parsedResponse?.messageStop?.stopReason ??
|
|
84
98
|
(parsedResponse?.type === 'message_delta'
|
|
85
99
|
? parsedResponse.delta?.stop_reason
|
|
@@ -89,8 +103,10 @@ class BedrockStreamingOutputMessageInstrumentor {
|
|
|
89
103
|
_accumulateToolCall(state, parsedResponse);
|
|
90
104
|
streamStateBySpan.set(span, state);
|
|
91
105
|
const isLastChunk = parsedResponse?.metadata || parsedResponse?.type === 'message_stop';
|
|
92
|
-
if (isLastChunk && (state.text || state.toolCalls.length)) {
|
|
106
|
+
if (isLastChunk && (state.reasoning || state.text || state.toolCalls.length)) {
|
|
93
107
|
const parts = [];
|
|
108
|
+
if (state.reasoning)
|
|
109
|
+
parts.push({ type: 'reasoning', content: state.reasoning });
|
|
94
110
|
if (state.text)
|
|
95
111
|
parts.push({ type: 'text', content: state.text });
|
|
96
112
|
for (const toolCall of state.toolCalls) {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
exports.BedrockTelemetryInstrumentor = void 0;
|
|
18
18
|
const get_cache_config_telemetry_1 = require("./get_cache_config_telemetry");
|
|
19
|
-
const
|
|
19
|
+
const get_cache_and_reasoning_telemetry_1 = require("./get_cache_and_reasoning_telemetry");
|
|
20
20
|
const get_input_message_content_telemetry_1 = require("./get_input_message_content_telemetry");
|
|
21
21
|
const get_streaming_output_message_telemetry_1 = require("./get_streaming_output_message_telemetry");
|
|
22
22
|
const get_model_id_telemetry_1 = require("./get_model_id_telemetry");
|
|
@@ -28,7 +28,7 @@ class BedrockTelemetryInstrumentor {
|
|
|
28
28
|
_subInstrumentors;
|
|
29
29
|
constructor() {
|
|
30
30
|
this._subInstrumentors = [
|
|
31
|
-
new
|
|
31
|
+
new get_cache_and_reasoning_telemetry_1.BedrockPromptCachingInstrumentor(),
|
|
32
32
|
new get_cache_config_telemetry_1.BedrockCacheConfigInstrumentor(),
|
|
33
33
|
new get_input_message_content_telemetry_1.BedrockInputMessageContentInstrumentor(),
|
|
34
34
|
new get_streaming_output_message_telemetry_1.BedrockStreamingOutputMessageInstrumentor(),
|
|
@@ -34,10 +34,16 @@
|
|
|
34
34
|
* under a bare block type with no mime type, which is repaired (see
|
|
35
35
|
* `get_input_message_content_telemetry.ts`).
|
|
36
36
|
* - On the response, it only forwards the flat `prompt_tokens` /
|
|
37
|
-
* `completion_tokens` counts, so prompt-cache reads
|
|
38
|
-
* `usage.prompt_tokens_details.cached_tokens`
|
|
39
|
-
* (
|
|
40
|
-
* `
|
|
37
|
+
* `completion_tokens` counts, so prompt-cache reads and the reasoning count
|
|
38
|
+
* never reach the span: `usage.prompt_tokens_details.cached_tokens` and
|
|
39
|
+
* `usage.completion_tokens_details.reasoning_tokens` (a part of
|
|
40
|
+
* `completion_tokens`) are read off the response body (or off the streamed
|
|
41
|
+
* chunks as they pass) and stamped as `gen_ai.usage.cache_read_input_tokens`
|
|
42
|
+
* and `gen_ai.usage.output_tokens_details.reasoning_tokens`.
|
|
43
|
+
* - On a stream, its accumulation of the chunks drops the `thinking` blocks, so
|
|
44
|
+
* a streamed thinking call records no reasoning part: the thinking is read off
|
|
45
|
+
* the streamed chunks as they pass and added to `gen_ai.output.messages` as it
|
|
46
|
+
* is set (see `get_streamed_reasoning_telemetry.ts`).
|
|
41
47
|
*
|
|
42
48
|
* Mistral's models on Vertex AI, called through `MistralGCP`, get the same
|
|
43
49
|
* tracing from `get_vertex_telemetry.ts`.
|
|
@@ -35,10 +35,16 @@
|
|
|
35
35
|
* under a bare block type with no mime type, which is repaired (see
|
|
36
36
|
* `get_input_message_content_telemetry.ts`).
|
|
37
37
|
* - On the response, it only forwards the flat `prompt_tokens` /
|
|
38
|
-
* `completion_tokens` counts, so prompt-cache reads
|
|
39
|
-
* `usage.prompt_tokens_details.cached_tokens`
|
|
40
|
-
* (
|
|
41
|
-
* `
|
|
38
|
+
* `completion_tokens` counts, so prompt-cache reads and the reasoning count
|
|
39
|
+
* never reach the span: `usage.prompt_tokens_details.cached_tokens` and
|
|
40
|
+
* `usage.completion_tokens_details.reasoning_tokens` (a part of
|
|
41
|
+
* `completion_tokens`) are read off the response body (or off the streamed
|
|
42
|
+
* chunks as they pass) and stamped as `gen_ai.usage.cache_read_input_tokens`
|
|
43
|
+
* and `gen_ai.usage.output_tokens_details.reasoning_tokens`.
|
|
44
|
+
* - On a stream, its accumulation of the chunks drops the `thinking` blocks, so
|
|
45
|
+
* a streamed thinking call records no reasoning part: the thinking is read off
|
|
46
|
+
* the streamed chunks as they pass and added to `gen_ai.output.messages` as it
|
|
47
|
+
* is set (see `get_streamed_reasoning_telemetry.ts`).
|
|
42
48
|
*
|
|
43
49
|
* Mistral's models on Vertex AI, called through `MistralGCP`, get the same
|
|
44
50
|
* tracing from `get_vertex_telemetry.ts`.
|
|
@@ -51,6 +57,7 @@ const logger_1 = require("../../logger");
|
|
|
51
57
|
const span_processor_1 = require("../../otel/span-processor");
|
|
52
58
|
const get_structured_output_schema_telemetry_1 = require("../shared/get_structured_output_schema_telemetry");
|
|
53
59
|
const get_input_message_content_telemetry_1 = require("./get_input_message_content_telemetry");
|
|
60
|
+
const get_streamed_reasoning_telemetry_1 = require("./get_streamed_reasoning_telemetry");
|
|
54
61
|
const get_structured_output_schema_telemetry_2 = require("./get_structured_output_schema_telemetry");
|
|
55
62
|
const get_vertex_telemetry_1 = require("./get_vertex_telemetry");
|
|
56
63
|
exports.MISTRAL_SDK_TELEMETRY_ENV = 'MISTRAL_SDK_TELEMETRY';
|
|
@@ -60,6 +67,7 @@ const CACHE_KEY_ATTR = 'weflayr.cache.key';
|
|
|
60
67
|
const REASONING_EFFORT_ATTR = 'gen_ai.request.reasoning_effort';
|
|
61
68
|
const EMBEDDINGS_INPUT_ATTR = 'weflayr.embeddings.input';
|
|
62
69
|
const CACHE_READ_INPUT_TOKENS_ATTR = 'gen_ai.usage.cache_read_input_tokens';
|
|
70
|
+
const REASONING_TOKENS_ATTR = 'gen_ai.usage.output_tokens_details.reasoning_tokens';
|
|
63
71
|
const OUTPUT_MESSAGES_ATTR = 'gen_ai.output.messages';
|
|
64
72
|
const FINISH_REASONS_ATTR = 'gen_ai.response.finish_reasons';
|
|
65
73
|
// Mistral's API vocabulary to the OTel GenAI finish reason (the value the
|
|
@@ -116,6 +124,23 @@ function toOtelOutputMessages(outputMessagesJson) {
|
|
|
116
124
|
}
|
|
117
125
|
return JSON.stringify(messages);
|
|
118
126
|
}
|
|
127
|
+
// Each streamed call's thinking by choice index, read off its chunks as they
|
|
128
|
+
// pass, until the tracing writes its output messages.
|
|
129
|
+
const streamedThinkingBySpan = new WeakMap();
|
|
130
|
+
/** `gen_ai.output.messages` with the call's streamed thinking added; unchanged
|
|
131
|
+
* for a call that streamed none, or when it cannot be added. */
|
|
132
|
+
function addSpanStreamedThinking(span, outputMessagesJson) {
|
|
133
|
+
const thinkingByChoice = streamedThinkingBySpan.get(span);
|
|
134
|
+
if (!thinkingByChoice)
|
|
135
|
+
return outputMessagesJson;
|
|
136
|
+
try {
|
|
137
|
+
return (0, get_streamed_reasoning_telemetry_1.addStreamedThinkingToOutputMessages)(outputMessagesJson, thinkingByChoice);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
(0, logger_1.debug)('mistral: streamed thinking not recorded: %s', err?.message || err);
|
|
141
|
+
return outputMessagesJson;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
119
144
|
function contentCaptureEnabled() {
|
|
120
145
|
// Read at span time (not instrument time) so a later `weflayr.setup()`
|
|
121
146
|
// toggling captureMessageContent takes effect. Defaults to off, matching the
|
|
@@ -131,7 +156,7 @@ function shapeAttributesAsTheyAreSet(span) {
|
|
|
131
156
|
if (!contentCaptureEnabled() && MESSAGE_CONTENT_ATTRIBUTES.includes(key))
|
|
132
157
|
return span;
|
|
133
158
|
if (key === OUTPUT_MESSAGES_ATTR && typeof value === 'string') {
|
|
134
|
-
return originalSetAttribute(key, toOtelOutputMessages(value));
|
|
159
|
+
return originalSetAttribute(key, toOtelOutputMessages(addSpanStreamedThinking(span, value)));
|
|
135
160
|
}
|
|
136
161
|
if (key === FINISH_REASONS_ATTR && Array.isArray(value)) {
|
|
137
162
|
return originalSetAttribute(key, value.map(toOtelFinishReason));
|
|
@@ -174,18 +199,24 @@ function setRequestBodyTelemetry(span, requestBodyJson) {
|
|
|
174
199
|
span.setAttribute(EMBEDDINGS_INPUT_ATTR, JSON.stringify(embeddedTexts));
|
|
175
200
|
}
|
|
176
201
|
}
|
|
177
|
-
function
|
|
202
|
+
function setTokenDetails(span, responseData) {
|
|
178
203
|
const usage = responseData?.usage;
|
|
179
204
|
const cachedTokens = usage?.prompt_tokens_details?.cached_tokens;
|
|
180
205
|
if (cachedTokens)
|
|
181
206
|
span.setAttribute(CACHE_READ_INPUT_TOKENS_ATTR, cachedTokens);
|
|
207
|
+
const reasoningTokens = usage?.completion_tokens_details?.reasoning_tokens;
|
|
208
|
+
if (reasoningTokens)
|
|
209
|
+
span.setAttribute(REASONING_TOKENS_ATTR, reasoningTokens);
|
|
182
210
|
}
|
|
183
211
|
/**
|
|
184
|
-
* The streamed response, unchanged, with every chunk's usage
|
|
185
|
-
* passes: the tracing hook reads the same stream, and ends the span only
|
|
186
|
-
* it is exhausted, so the cache-read
|
|
212
|
+
* The streamed response, unchanged, with every chunk's usage and thinking read
|
|
213
|
+
* as it passes: the tracing hook reads the same stream, and ends the span only
|
|
214
|
+
* once it is exhausted, so the cache-read and reasoning counts are on the span, and the
|
|
215
|
+
* thinking ready for its output messages, by then.
|
|
187
216
|
*/
|
|
188
|
-
function
|
|
217
|
+
function readTokenDetailsAndThinkingFromStream(span, response) {
|
|
218
|
+
const thinkingByChoice = new Map();
|
|
219
|
+
streamedThinkingBySpan.set(span, thinkingByChoice);
|
|
189
220
|
const reader = response.body.getReader();
|
|
190
221
|
const decoder = new TextDecoder();
|
|
191
222
|
let unfinishedLine = '';
|
|
@@ -197,10 +228,12 @@ function readCachedTokensFromStream(span, response) {
|
|
|
197
228
|
if (!data.startsWith(SERVER_SENT_EVENT_DATA_PREFIX))
|
|
198
229
|
continue;
|
|
199
230
|
try {
|
|
200
|
-
|
|
231
|
+
const chunk = JSON.parse(data.slice(SERVER_SENT_EVENT_DATA_PREFIX.length));
|
|
232
|
+
setTokenDetails(span, chunk);
|
|
233
|
+
(0, get_streamed_reasoning_telemetry_1.addStreamedThinking)(chunk, thinkingByChoice);
|
|
201
234
|
}
|
|
202
235
|
catch {
|
|
203
|
-
// "[DONE]" and any line that is not a JSON chunk carry no usage.
|
|
236
|
+
// "[DONE]" and any line that is not a JSON chunk carry no usage or thinking.
|
|
204
237
|
}
|
|
205
238
|
}
|
|
206
239
|
};
|
|
@@ -235,10 +268,10 @@ async function observeCachedTokens(span, response) {
|
|
|
235
268
|
return response;
|
|
236
269
|
const contentType = (response.headers.get('content-type') || '').toLowerCase();
|
|
237
270
|
if (contentType.includes('text/event-stream') && response.body) {
|
|
238
|
-
return
|
|
271
|
+
return readTokenDetailsAndThinkingFromStream(span, response);
|
|
239
272
|
}
|
|
240
273
|
if (contentType.includes('json')) {
|
|
241
|
-
|
|
274
|
+
setTokenDetails(span, await response.clone().json());
|
|
242
275
|
}
|
|
243
276
|
return response;
|
|
244
277
|
}
|
|
@@ -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
|
+
}
|
|
@@ -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().
|
|
@@ -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,7 +17,7 @@
|
|
|
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
23
|
const logger_1 = require("../logger");
|
|
@@ -73,8 +73,8 @@ class WeflayrSpanProcessor {
|
|
|
73
73
|
}
|
|
74
74
|
this._delegate =
|
|
75
75
|
exportMode === 'immediate'
|
|
76
|
-
? new
|
|
77
|
-
: new
|
|
76
|
+
? new sdk_trace_1.SimpleSpanProcessor({ exporter })
|
|
77
|
+
: new sdk_trace_1.BatchSpanProcessor({ exporter });
|
|
78
78
|
this._defaultTags = defaultTags;
|
|
79
79
|
this._mask = mask;
|
|
80
80
|
this._maskFunctionSource = mask ? String(mask) : null;
|