weflayr 0.27.0 → 0.28.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 (99) hide show
  1. package/dist/package.json +3 -3
  2. package/dist/src/auto-instrument.d.ts +4 -3
  3. package/dist/src/auto-instrument.js +18 -17
  4. package/dist/src/get-data-uri-mime-type.d.ts +17 -0
  5. package/dist/src/get-data-uri-mime-type.js +25 -0
  6. package/dist/src/index.d.ts +3 -2
  7. package/dist/src/index.js +5 -1
  8. package/dist/src/instrumentation/anthropic/get_streaming_accumulation_telemetry.d.ts +3 -1
  9. package/dist/src/instrumentation/anthropic/get_streaming_accumulation_telemetry.js +6 -3
  10. package/dist/src/instrumentation/anthropic/get_vertex_billing_telemetry.d.ts +32 -0
  11. package/dist/src/instrumentation/anthropic/get_vertex_billing_telemetry.js +100 -0
  12. package/dist/src/instrumentation/anthropic/get_web_search_telemetry.d.ts +43 -0
  13. package/dist/src/instrumentation/anthropic/get_web_search_telemetry.js +147 -0
  14. package/dist/src/instrumentation/anthropic/set_telemetry_attributes.d.ts +5 -2
  15. package/dist/src/instrumentation/anthropic/set_telemetry_attributes.js +17 -4
  16. package/dist/src/instrumentation/bedrock/get_cache_config_telemetry.d.ts +13 -1
  17. package/dist/src/instrumentation/bedrock/get_cache_config_telemetry.js +56 -1
  18. package/dist/src/instrumentation/bedrock/get_cache_telemetry.js +8 -5
  19. package/dist/src/instrumentation/bedrock/get_model_id_telemetry.d.ts +1 -1
  20. package/dist/src/instrumentation/bedrock/get_model_id_telemetry.js +1 -1
  21. package/dist/src/instrumentation/bedrock/get_openai_chat_format_telemetry.d.ts +45 -0
  22. package/dist/src/instrumentation/bedrock/get_openai_chat_format_telemetry.js +251 -0
  23. package/dist/src/instrumentation/bedrock/get_streaming_output_message_telemetry.d.ts +6 -5
  24. package/dist/src/instrumentation/bedrock/get_streaming_output_message_telemetry.js +15 -12
  25. package/dist/src/instrumentation/bedrock/get_tool_definitions_telemetry.d.ts +2 -2
  26. package/dist/src/instrumentation/bedrock/get_tool_definitions_telemetry.js +12 -4
  27. package/dist/src/instrumentation/bedrock/set_telemetry_attributes.d.ts +4 -2
  28. package/dist/src/instrumentation/bedrock/set_telemetry_attributes.js +10 -3
  29. package/dist/src/instrumentation/cohere/get_embeddings_telemetry.js +4 -6
  30. package/dist/src/instrumentation/cohere/get_input_message_content_telemetry.d.ts +23 -0
  31. package/dist/src/instrumentation/cohere/get_input_message_content_telemetry.js +105 -0
  32. package/dist/src/instrumentation/cohere/get_v2_chat_telemetry.d.ts +18 -0
  33. package/dist/src/instrumentation/cohere/get_v2_chat_telemetry.js +186 -0
  34. package/dist/src/instrumentation/cohere/set_telemetry_attributes.d.ts +3 -1
  35. package/dist/src/instrumentation/cohere/set_telemetry_attributes.js +5 -1
  36. package/dist/src/instrumentation/google/get_embeddings_telemetry.d.ts +8 -4
  37. package/dist/src/instrumentation/google/get_embeddings_telemetry.js +68 -35
  38. package/dist/src/instrumentation/google/get_provider_name.d.ts +2 -1
  39. package/dist/src/instrumentation/google/get_provider_name.js +2 -1
  40. package/dist/src/instrumentation/google/get_web_search_telemetry.d.ts +38 -0
  41. package/dist/src/instrumentation/google/get_web_search_telemetry.js +125 -0
  42. package/dist/src/instrumentation/google/set_telemetry_attributes.d.ts +2 -2
  43. package/dist/src/instrumentation/google/set_telemetry_attributes.js +6 -2
  44. package/dist/src/instrumentation/mistral/full_instrumentation.d.ts +60 -0
  45. package/dist/src/instrumentation/mistral/full_instrumentation.js +364 -0
  46. package/dist/src/instrumentation/mistral/get_input_message_content_telemetry.d.ts +35 -0
  47. package/dist/src/instrumentation/mistral/get_input_message_content_telemetry.js +113 -0
  48. package/dist/src/instrumentation/mistral/get_structured_output_schema_telemetry.d.ts +13 -0
  49. package/dist/src/instrumentation/mistral/get_structured_output_schema_telemetry.js +24 -0
  50. package/dist/src/instrumentation/mistral/get_vertex_telemetry.d.ts +54 -0
  51. package/dist/src/instrumentation/mistral/get_vertex_telemetry.js +153 -0
  52. package/dist/src/instrumentation/openai/get_bedrock_mantle_telemetry.d.ts +10 -0
  53. package/dist/src/instrumentation/openai/get_bedrock_mantle_telemetry.js +33 -0
  54. package/dist/src/instrumentation/openai/get_cache_and_reasoning_telemetry.d.ts +13 -6
  55. package/dist/src/instrumentation/openai/get_cache_and_reasoning_telemetry.js +17 -7
  56. package/dist/src/instrumentation/openai/get_cache_config_telemetry.d.ts +21 -4
  57. package/dist/src/instrumentation/openai/get_cache_config_telemetry.js +50 -11
  58. package/dist/src/instrumentation/openai/get_embeddings_telemetry.js +14 -6
  59. package/dist/src/instrumentation/openai/get_file_part_mime_type_telemetry.d.ts +26 -0
  60. package/dist/src/instrumentation/openai/get_file_part_mime_type_telemetry.js +100 -0
  61. package/dist/src/instrumentation/openai/get_openai_compatible_provider_telemetry.d.ts +48 -0
  62. package/dist/src/instrumentation/openai/get_openai_compatible_provider_telemetry.js +123 -0
  63. package/dist/src/instrumentation/openai/get_reasoning_content_telemetry.d.ts +28 -0
  64. package/dist/src/instrumentation/openai/get_reasoning_content_telemetry.js +130 -0
  65. package/dist/src/instrumentation/openai/get_responses_telemetry.d.ts +89 -0
  66. package/dist/src/instrumentation/openai/get_responses_telemetry.js +587 -0
  67. package/dist/src/instrumentation/openai/get_service_tier_telemetry.d.ts +20 -0
  68. package/dist/src/instrumentation/openai/get_service_tier_telemetry.js +84 -0
  69. package/dist/src/instrumentation/openai/get_structured_output_schema_telemetry.d.ts +7 -5
  70. package/dist/src/instrumentation/openai/get_structured_output_schema_telemetry.js +15 -8
  71. package/dist/src/instrumentation/openai/get_vertex_openai_compatible_telemetry.d.ts +23 -0
  72. package/dist/src/instrumentation/openai/get_vertex_openai_compatible_telemetry.js +69 -0
  73. package/dist/src/instrumentation/openai/get_web_search_telemetry.d.ts +74 -0
  74. package/dist/src/instrumentation/openai/get_web_search_telemetry.js +259 -0
  75. package/dist/src/instrumentation/openai/set_telemetry_attributes.d.ts +13 -1
  76. package/dist/src/instrumentation/openai/set_telemetry_attributes.js +33 -2
  77. package/dist/src/instrumentation/shared/get_structured_output_schema_telemetry.d.ts +1 -1
  78. package/dist/src/instrumentation/shared/get_structured_output_schema_telemetry.js +1 -1
  79. package/dist/src/instrumentation/shared/is_vertex_base_url.d.ts +17 -0
  80. package/dist/src/instrumentation/shared/is_vertex_base_url.js +40 -0
  81. package/dist/src/otel/propagation.d.ts +37 -10
  82. package/dist/src/otel/propagation.js +62 -22
  83. package/dist/src/otel/span-filter.d.ts +2 -0
  84. package/dist/src/otel/span-filter.js +10 -1
  85. package/dist/src/otel/span-processor.d.ts +1 -2
  86. package/dist/src/otel/span-processor.js +17 -20
  87. package/dist/src/otel/strip-media-payloads.d.ts +26 -0
  88. package/dist/src/otel/strip-media-payloads.js +135 -0
  89. package/dist/src/propagate-metadata-to-gateway.d.ts +37 -0
  90. package/dist/src/propagate-metadata-to-gateway.js +213 -0
  91. package/package.json +3 -3
  92. package/dist/src/instrumentation/vercel/full_instrumentation.d.ts +0 -3
  93. package/dist/src/instrumentation/vercel/full_instrumentation.js +0 -54
  94. package/dist/src/instrumentation/vercel/get_cache_creation_telemetry.d.ts +0 -22
  95. package/dist/src/instrumentation/vercel/get_cache_creation_telemetry.js +0 -66
  96. package/dist/src/instrumentation/vercel/get_structured_output_schema_telemetry.d.ts +0 -24
  97. package/dist/src/instrumentation/vercel/get_structured_output_schema_telemetry.js +0 -84
  98. package/dist/src/otel/vercel-ai-sdk-span-filter.d.ts +0 -12
  99. package/dist/src/otel/vercel-ai-sdk-span-filter.js +0 -88
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ /**
3
+ * Provider-run web search (Google Search grounding) capture for Gemini spans.
4
+ *
5
+ * Gemini runs the search itself when the request config carries the
6
+ * `googleSearch` tool, and bills the call once per grounded request whatever
7
+ * number of queries it ran. Neither instrumentor records any of it: the
8
+ * `googleSearch` tool is nowhere in the span, `candidates[].groundingMetadata`
9
+ * is dropped, and so is `usageMetadata.toolUsePromptTokenCount`, input tokens
10
+ * the call is billed for, counted SEPARATELY from `promptTokenCount`, so a
11
+ * 14-token prompt that grounds bills 14 + 65.
12
+ *
13
+ * All three are stamped under `weflayr.web_search.*`; `normalizers/google.py`
14
+ * maps them to the shape every provider's web search is stored in.
15
+ *
16
+ * The `@google/genai` client answers in camelCase where the Python one answers
17
+ * in snake_case, so the stamped JSON is written in snake_case: both SDKs must
18
+ * produce the same event for the same call, and the normalizer reads one
19
+ * spelling.
20
+ *
21
+ * `groundingMetadata.searchEntryPoint` is not stamped: it is several kilobytes
22
+ * of styled HTML for rendering Google's "Search suggestions" widget, which
23
+ * nothing reads back.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.extractWebSearchToolDefinitions = extractWebSearchToolDefinitions;
27
+ exports.setWebSearchToolTelemetry = setWebSearchToolTelemetry;
28
+ exports.extractGroundingBlocks = extractGroundingBlocks;
29
+ exports.setWebSearchResultTelemetry = setWebSearchResultTelemetry;
30
+ const TOOL_DEFINITIONS_ATTR = 'weflayr.web_search.tool_definitions';
31
+ const OUTPUT_BLOCKS_ATTR = 'weflayr.web_search.output_blocks';
32
+ const REQUESTS_ATTR = 'weflayr.web_search.requests';
33
+ const TOOL_USE_PROMPT_TOKENS_ATTR = 'weflayr.web_search.tool_use_prompt_tokens';
34
+ // The config fields naming Google's own search tool, as the client spells them
35
+ // and as the stored attribute does. googleSearch is the current one;
36
+ // googleSearchRetrieval is what Gemini 1.5 took, still accepted, and grounds
37
+ // the same way.
38
+ const WEB_SEARCH_TOOL_FIELDS = [
39
+ ['googleSearch', 'google_search'],
40
+ ['googleSearchRetrieval', 'google_search_retrieval'],
41
+ ];
42
+ /** `value` with every key spelled snake_case, recursing into objects and arrays.
43
+ * The JS client takes the tool's settings in camelCase (`timeRangeFilter`)
44
+ * where the Python one takes them in snake_case and dumps them as it took
45
+ * them, so without this the two SDKs would store the same request's settings
46
+ * under different names. */
47
+ function withSnakeCaseKeys(value) {
48
+ if (Array.isArray(value))
49
+ return value.map(withSnakeCaseKeys);
50
+ if (!value || typeof value !== 'object')
51
+ return value;
52
+ return Object.fromEntries(Object.entries(value).map(([key, nested]) => [
53
+ key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`),
54
+ withSnakeCaseKeys(nested),
55
+ ]));
56
+ }
57
+ /** The request's Google Search tools, as configured, under the names the Python
58
+ * SDK stamps. e.g. a config carrying `{ tools: [{ googleSearch: {} }] }` ->
59
+ * `[{ google_search: {} }]`, and one setting a time range ->
60
+ * `[{ google_search: { time_range_filter: {...} } }]`. */
61
+ function extractWebSearchToolDefinitions(config) {
62
+ const tools = config && config.tools;
63
+ if (!Array.isArray(tools))
64
+ return [];
65
+ const definitions = [];
66
+ for (const tool of tools) {
67
+ if (!tool)
68
+ continue;
69
+ for (const [clientField, storedField] of WEB_SEARCH_TOOL_FIELDS) {
70
+ if (tool[clientField] !== undefined && tool[clientField] !== null) {
71
+ definitions.push({ [storedField]: withSnakeCaseKeys(tool[clientField]) });
72
+ }
73
+ }
74
+ }
75
+ return definitions;
76
+ }
77
+ function setWebSearchToolTelemetry(span, config) {
78
+ if (!span || !span.isRecording || !span.isRecording() || !config)
79
+ return;
80
+ const definitions = extractWebSearchToolDefinitions(config);
81
+ if (definitions.length) {
82
+ span.setAttribute(TOOL_DEFINITIONS_ATTR, JSON.stringify(definitions));
83
+ }
84
+ }
85
+ /** The queries and pages of a grounded candidate, in the snake_case shape the
86
+ * normalizer reads. Returns an empty array when nothing was grounded. */
87
+ function extractGroundingBlocks(response) {
88
+ for (const candidate of (response && response.candidates) || []) {
89
+ const grounding = candidate && candidate.groundingMetadata;
90
+ if (!grounding)
91
+ continue;
92
+ const queries = grounding.webSearchQueries || [];
93
+ const chunks = grounding.groundingChunks || [];
94
+ if (!queries.length && !chunks.length)
95
+ continue;
96
+ return [
97
+ {
98
+ web_search_queries: queries,
99
+ // The chunk's own `web: {uri, title}` shape is already the same in both
100
+ // clients, so it is kept verbatim.
101
+ grounding_chunks: chunks,
102
+ },
103
+ ];
104
+ }
105
+ return [];
106
+ }
107
+ /** Stamp what the grounded answer searched for, that it is billed for one
108
+ * grounded request, and the input tokens the search results added.
109
+ *
110
+ * Over a stream each chunk re-reports the grounding gathered so far, so the
111
+ * attribute is re-stamped rather than accumulated. */
112
+ function setWebSearchResultTelemetry(span, response) {
113
+ if (!span || !span.isRecording || !span.isRecording())
114
+ return;
115
+ const blocks = extractGroundingBlocks(response);
116
+ if (blocks.length) {
117
+ span.setAttribute(OUTPUT_BLOCKS_ATTR, JSON.stringify(blocks));
118
+ // Google bills a grounded request once, however many queries it ran.
119
+ span.setAttribute(REQUESTS_ATTR, 1);
120
+ }
121
+ const toolUsePromptTokens = response && response.usageMetadata?.toolUsePromptTokenCount;
122
+ if (toolUsePromptTokens) {
123
+ span.setAttribute(TOOL_USE_PROMPT_TOKENS_ATTR, toolUsePromptTokens);
124
+ }
125
+ }
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Wires every weflayr-supplied patch the traceloop Google instrumentor misses:
5
5
  * - chat span enrichment (one wrap of generateContent{,Stream}Internal that
6
- * stamps provider name + audio tokens + cache tokens — see the get_*_telemetry
7
- * files), and
6
+ * stamps provider name + audio tokens + cache tokens + the provider-run web
7
+ * search, see the get_*_telemetry files), and
8
8
  * - the embeddings endpoint it doesn't cover at all.
9
9
  *
10
10
  * The public arrows delegate to these private prototype methods while the
@@ -4,8 +4,8 @@
4
4
  *
5
5
  * Wires every weflayr-supplied patch the traceloop Google instrumentor misses:
6
6
  * - chat span enrichment (one wrap of generateContent{,Stream}Internal that
7
- * stamps provider name + audio tokens + cache tokens — see the get_*_telemetry
8
- * files), and
7
+ * stamps provider name + audio tokens + cache tokens + the provider-run web
8
+ * search, see the get_*_telemetry files), and
9
9
  * - the embeddings endpoint it doesn't cover at all.
10
10
  *
11
11
  * The public arrows delegate to these private prototype methods while the
@@ -19,6 +19,7 @@ const get_audio_telemetry_1 = require("./get_audio_telemetry");
19
19
  const get_cache_telemetry_1 = require("./get_cache_telemetry");
20
20
  const get_reasoning_telemetry_1 = require("./get_reasoning_telemetry");
21
21
  const get_structured_output_schema_telemetry_1 = require("./get_structured_output_schema_telemetry");
22
+ const get_web_search_telemetry_1 = require("./get_web_search_telemetry");
22
23
  const get_embeddings_telemetry_1 = require("./get_embeddings_telemetry");
23
24
  const get_cache_operations_telemetry_1 = require("./get_cache_operations_telemetry");
24
25
  class GoogleTelemetryInstrumentor {
@@ -57,11 +58,13 @@ class GoogleTelemetryInstrumentor {
57
58
  const requestConfig = args[0] && args[0].config;
58
59
  (0, get_cache_telemetry_1.setCacheConfigTelemetry)(span, requestConfig);
59
60
  (0, get_structured_output_schema_telemetry_1.setInputStructuredOutputSchemaTelemetry)(span, requestConfig);
61
+ (0, get_web_search_telemetry_1.setWebSearchToolTelemetry)(span, requestConfig);
60
62
  if (!stream) {
61
63
  return Promise.resolve(original.apply(this, args)).then((response) => {
62
64
  (0, get_audio_telemetry_1.setAudioTelemetry)(span, response);
63
65
  (0, get_cache_telemetry_1.setCacheTelemetry)(span, response);
64
66
  (0, get_reasoning_telemetry_1.setReasoningTelemetry)(span, response);
67
+ (0, get_web_search_telemetry_1.setWebSearchResultTelemetry)(span, response);
65
68
  return response;
66
69
  });
67
70
  }
@@ -76,6 +79,7 @@ class GoogleTelemetryInstrumentor {
76
79
  try {
77
80
  (0, get_cache_telemetry_1.setCacheTelemetry)(span, chunk);
78
81
  (0, get_reasoning_telemetry_1.setReasoningTelemetry)(span, chunk);
82
+ (0, get_web_search_telemetry_1.setWebSearchResultTelemetry)(span, chunk);
79
83
  }
80
84
  catch {
81
85
  // Never break the caller's stream over an enrichment failure.
@@ -0,0 +1,60 @@
1
+ /**
2
+ * OTel instrumentor for the Mistral SDK, backed by its built-in tracing.
3
+ *
4
+ * `@mistralai/mistralai` (v2) ships its own OpenTelemetry tracing: a
5
+ * `TracingHook` that opens one `gen_ai.*` span per request, following the
6
+ * GenAI semantic conventions, including token usage on streaming responses and
7
+ * error details on failures. It sends its spans to the global TracerProvider
8
+ * (the one `weflayr.setup()` installs) only once the `MISTRAL_SDK_TELEMETRY`
9
+ * environment variable says "global", which this instrumentor sets unless the
10
+ * user chose otherwise. traceloop ships no Mistral instrumentor.
11
+ *
12
+ * The package is ESM only and keeps the functions that build the span
13
+ * attributes to itself, so the complements below wrap the methods of its
14
+ * exported `TracingHook` class, which hand over the span and the request body:
15
+ *
16
+ * - The built-in tracing always records message content
17
+ * (`gen_ai.input.messages` / `gen_ai.output.messages`), ignoring the standard
18
+ * `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` switch that
19
+ * `weflayr.setup({ captureMessageContent })` drives. Unless capture is
20
+ * enabled, the content attributes are removed from the span once the request
21
+ * is traced, and never set on it afterwards.
22
+ * - It forwards the API's raw `finish_reason` ("tool_calls") and the tool call
23
+ * `arguments` as the raw JSON string, where the other instrumentors emit the
24
+ * OTel GenAI "tool_call" and a parsed arguments object. `gen_ai.output.messages`
25
+ * (and `gen_ai.response.finish_reasons`) are rewritten to that shape as they
26
+ * are set, so a Mistral tool call normalizes like every other provider's.
27
+ * - On the request, it records the sampling params, messages and tool
28
+ * definitions but not the requested structured-output schema, the
29
+ * `reasoning_effort` or the `prompt_cache_key`: they are read off the request
30
+ * body (the effort lands on `gen_ai.request.reasoning_effort` and the key on
31
+ * `weflayr.cache.key`, as on OpenAI spans). It also misses the embeddings
32
+ * input, stamped here as `weflayr.embeddings.input`, the way the other
33
+ * embeddings instrumentors do, and records media parts other than images
34
+ * under a bare block type with no mime type, which is repaired (see
35
+ * `get_input_message_content_telemetry.ts`).
36
+ * - On the response, it only forwards the flat `prompt_tokens` /
37
+ * `completion_tokens` counts, so prompt-cache reads never reach the span:
38
+ * `usage.prompt_tokens_details.cached_tokens` is read off the response body
39
+ * (or off the streamed chunks as they pass) and stamped as
40
+ * `gen_ai.usage.cache_read_input_tokens`.
41
+ *
42
+ * Mistral's models on Vertex AI, called through `MistralGCP`, get the same
43
+ * tracing from `get_vertex_telemetry.ts`.
44
+ */
45
+ import { type LoadModule } from './get_vertex_telemetry';
46
+ export declare const MISTRAL_SDK_TELEMETRY_ENV = "MISTRAL_SDK_TELEMETRY";
47
+ export declare const MISTRAL_TRACING_HOOK_MODULE = "@mistralai/mistralai/hooks/tracing";
48
+ export declare class MistralInstrumentor {
49
+ private readonly loadModule;
50
+ private readonly canTheSdkLoadTheOtelApi;
51
+ private static activeInstance;
52
+ private _tracingHookClass;
53
+ private _originalBeforeRequest;
54
+ private _originalAfterSuccess;
55
+ private _ownsTelemetryEnv;
56
+ private readonly _vertexInstrumentor;
57
+ constructor(loadModule?: LoadModule, canTheSdkLoadTheOtelApi?: () => boolean);
58
+ instrument(): Promise<void>;
59
+ uninstrument(): void;
60
+ }
@@ -0,0 +1,364 @@
1
+ "use strict";
2
+ /**
3
+ * OTel instrumentor for the Mistral SDK, backed by its built-in tracing.
4
+ *
5
+ * `@mistralai/mistralai` (v2) ships its own OpenTelemetry tracing: a
6
+ * `TracingHook` that opens one `gen_ai.*` span per request, following the
7
+ * GenAI semantic conventions, including token usage on streaming responses and
8
+ * error details on failures. It sends its spans to the global TracerProvider
9
+ * (the one `weflayr.setup()` installs) only once the `MISTRAL_SDK_TELEMETRY`
10
+ * environment variable says "global", which this instrumentor sets unless the
11
+ * user chose otherwise. traceloop ships no Mistral instrumentor.
12
+ *
13
+ * The package is ESM only and keeps the functions that build the span
14
+ * attributes to itself, so the complements below wrap the methods of its
15
+ * exported `TracingHook` class, which hand over the span and the request body:
16
+ *
17
+ * - The built-in tracing always records message content
18
+ * (`gen_ai.input.messages` / `gen_ai.output.messages`), ignoring the standard
19
+ * `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` switch that
20
+ * `weflayr.setup({ captureMessageContent })` drives. Unless capture is
21
+ * enabled, the content attributes are removed from the span once the request
22
+ * is traced, and never set on it afterwards.
23
+ * - It forwards the API's raw `finish_reason` ("tool_calls") and the tool call
24
+ * `arguments` as the raw JSON string, where the other instrumentors emit the
25
+ * OTel GenAI "tool_call" and a parsed arguments object. `gen_ai.output.messages`
26
+ * (and `gen_ai.response.finish_reasons`) are rewritten to that shape as they
27
+ * are set, so a Mistral tool call normalizes like every other provider's.
28
+ * - On the request, it records the sampling params, messages and tool
29
+ * definitions but not the requested structured-output schema, the
30
+ * `reasoning_effort` or the `prompt_cache_key`: they are read off the request
31
+ * body (the effort lands on `gen_ai.request.reasoning_effort` and the key on
32
+ * `weflayr.cache.key`, as on OpenAI spans). It also misses the embeddings
33
+ * input, stamped here as `weflayr.embeddings.input`, the way the other
34
+ * embeddings instrumentors do, and records media parts other than images
35
+ * under a bare block type with no mime type, which is repaired (see
36
+ * `get_input_message_content_telemetry.ts`).
37
+ * - On the response, it only forwards the flat `prompt_tokens` /
38
+ * `completion_tokens` counts, so prompt-cache reads never reach the span:
39
+ * `usage.prompt_tokens_details.cached_tokens` is read off the response body
40
+ * (or off the streamed chunks as they pass) and stamped as
41
+ * `gen_ai.usage.cache_read_input_tokens`.
42
+ *
43
+ * Mistral's models on Vertex AI, called through `MistralGCP`, get the same
44
+ * tracing from `get_vertex_telemetry.ts`.
45
+ */
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.MistralInstrumentor = exports.MISTRAL_TRACING_HOOK_MODULE = exports.MISTRAL_SDK_TELEMETRY_ENV = void 0;
48
+ const node_module_1 = require("node:module");
49
+ const node_url_1 = require("node:url");
50
+ const logger_1 = require("../../logger");
51
+ const span_processor_1 = require("../../otel/span-processor");
52
+ const get_structured_output_schema_telemetry_1 = require("../shared/get_structured_output_schema_telemetry");
53
+ const get_input_message_content_telemetry_1 = require("./get_input_message_content_telemetry");
54
+ const get_structured_output_schema_telemetry_2 = require("./get_structured_output_schema_telemetry");
55
+ const get_vertex_telemetry_1 = require("./get_vertex_telemetry");
56
+ exports.MISTRAL_SDK_TELEMETRY_ENV = 'MISTRAL_SDK_TELEMETRY';
57
+ exports.MISTRAL_TRACING_HOOK_MODULE = '@mistralai/mistralai/hooks/tracing';
58
+ const OTEL_API_PACKAGE = '@opentelemetry/api';
59
+ const CACHE_KEY_ATTR = 'weflayr.cache.key';
60
+ const REASONING_EFFORT_ATTR = 'gen_ai.request.reasoning_effort';
61
+ const EMBEDDINGS_INPUT_ATTR = 'weflayr.embeddings.input';
62
+ const CACHE_READ_INPUT_TOKENS_ATTR = 'gen_ai.usage.cache_read_input_tokens';
63
+ const OUTPUT_MESSAGES_ATTR = 'gen_ai.output.messages';
64
+ const FINISH_REASONS_ATTR = 'gen_ai.response.finish_reasons';
65
+ // Mistral's API vocabulary to the OTel GenAI finish reason (the value the
66
+ // OpenAI/Anthropic instrumentors emit); any other value passes through unchanged.
67
+ const OTEL_FINISH_REASON_BY_MISTRAL_FINISH_REASON = {
68
+ tool_calls: 'tool_call',
69
+ };
70
+ const MESSAGE_CONTENT_ATTRIBUTES = [
71
+ 'gen_ai.input.messages',
72
+ 'gen_ai.output.messages',
73
+ 'gen_ai.system_instructions',
74
+ 'gen_ai.tool.definitions',
75
+ 'gen_ai.request.structured_output_schema',
76
+ ];
77
+ const SERVER_SENT_EVENT_DATA_PREFIX = 'data:';
78
+ function toOtelFinishReason(finishReason) {
79
+ return typeof finishReason === 'string'
80
+ ? (OTEL_FINISH_REASON_BY_MISTRAL_FINISH_REASON[finishReason] ?? finishReason)
81
+ : finishReason;
82
+ }
83
+ /** The tool call arguments as an object when they are a JSON string; a string
84
+ * the model did not close as valid JSON is kept as is rather than lost. */
85
+ function parseToolCallArguments(toolCallArguments) {
86
+ if (typeof toolCallArguments !== 'string')
87
+ return toolCallArguments;
88
+ try {
89
+ return JSON.parse(toolCallArguments);
90
+ }
91
+ catch {
92
+ return toolCallArguments;
93
+ }
94
+ }
95
+ /**
96
+ * `gen_ai.output.messages` with "tool_calls" finish reasons renamed to
97
+ * "tool_call" and tool call arguments parsed from Mistral's JSON string.
98
+ *
99
+ * Example: `[{"role": "assistant", "parts": [{"type": "tool_call", "name":
100
+ * "get_weather", "arguments": "{\"city\": \"Paris\"}"}], "finish_reason":
101
+ * "tool_calls"}]` becomes `[{"role": "assistant", "parts": [{"type":
102
+ * "tool_call", "name": "get_weather", "arguments": {"city": "Paris"}}],
103
+ * "finish_reason": "tool_call"}]`.
104
+ */
105
+ function toOtelOutputMessages(outputMessagesJson) {
106
+ const messages = JSON.parse(outputMessagesJson);
107
+ for (const message of messages) {
108
+ if ('finish_reason' in message) {
109
+ message.finish_reason = toOtelFinishReason(message.finish_reason);
110
+ }
111
+ for (const part of message.parts || []) {
112
+ if (part.type === 'tool_call' && 'arguments' in part) {
113
+ part.arguments = parseToolCallArguments(part.arguments);
114
+ }
115
+ }
116
+ }
117
+ return JSON.stringify(messages);
118
+ }
119
+ function contentCaptureEnabled() {
120
+ // Read at span time (not instrument time) so a later `weflayr.setup()`
121
+ // toggling captureMessageContent takes effect. Defaults to off, matching the
122
+ // OTel GenAI convention for this variable.
123
+ return (process.env[span_processor_1.CONTENT_CAPTURE_ENV] || '').trim().toLowerCase() === 'true';
124
+ }
125
+ /** Make the span's attribute setter apply the OTel output shape, and drop the
126
+ * content attributes while capture is disabled. The tracing sets every
127
+ * attribute it records through it, one key at a time. */
128
+ function shapeAttributesAsTheyAreSet(span) {
129
+ const originalSetAttribute = span.setAttribute.bind(span);
130
+ span.setAttribute = (key, value) => {
131
+ if (!contentCaptureEnabled() && MESSAGE_CONTENT_ATTRIBUTES.includes(key))
132
+ return span;
133
+ if (key === OUTPUT_MESSAGES_ATTR && typeof value === 'string') {
134
+ return originalSetAttribute(key, toOtelOutputMessages(value));
135
+ }
136
+ if (key === FINISH_REASONS_ATTR && Array.isArray(value)) {
137
+ return originalSetAttribute(key, value.map(toOtelFinishReason));
138
+ }
139
+ return originalSetAttribute(key, value);
140
+ };
141
+ }
142
+ /** Remove the content attributes the tracing set while it traced the request,
143
+ * before the setter above was in place. */
144
+ function dropRequestContentAttributes(span) {
145
+ if (contentCaptureEnabled())
146
+ return;
147
+ const attributes = span.attributes;
148
+ if (!attributes)
149
+ return;
150
+ for (const key of MESSAGE_CONTENT_ATTRIBUTES)
151
+ delete attributes[key];
152
+ }
153
+ function setRequestBodyTelemetry(span, requestBodyJson) {
154
+ let requestBody;
155
+ try {
156
+ requestBody = JSON.parse(requestBodyJson);
157
+ }
158
+ catch {
159
+ return;
160
+ }
161
+ if (typeof requestBody !== 'object' || requestBody === null)
162
+ return;
163
+ const body = requestBody;
164
+ (0, get_structured_output_schema_telemetry_1.setStructuredOutputSchemaTelemetry)(span, (0, get_structured_output_schema_telemetry_2.extractStructuredOutputSchema)(body));
165
+ (0, get_input_message_content_telemetry_1.setInputMessageContentTelemetry)(span, body);
166
+ if (body.reasoning_effort)
167
+ span.setAttribute(REASONING_EFFORT_ATTR, String(body.reasoning_effort));
168
+ if (body.prompt_cache_key)
169
+ span.setAttribute(CACHE_KEY_ATTR, String(body.prompt_cache_key));
170
+ // The embeddings request's `input` is one text or a list of texts; the
171
+ // weflayr span processor strips it when content capture is off.
172
+ const embeddedTexts = typeof body.input === 'string' ? [body.input] : body.input;
173
+ if (Array.isArray(embeddedTexts) && embeddedTexts.length > 0) {
174
+ span.setAttribute(EMBEDDINGS_INPUT_ATTR, JSON.stringify(embeddedTexts));
175
+ }
176
+ }
177
+ function setCacheReadTokens(span, responseData) {
178
+ const usage = responseData?.usage;
179
+ const cachedTokens = usage?.prompt_tokens_details?.cached_tokens;
180
+ if (cachedTokens)
181
+ span.setAttribute(CACHE_READ_INPUT_TOKENS_ATTR, cachedTokens);
182
+ }
183
+ /**
184
+ * The streamed response, unchanged, with every chunk's usage read as it
185
+ * passes: the tracing hook reads the same stream, and ends the span only once
186
+ * it is exhausted, so the cache-read count is on the span by then.
187
+ */
188
+ function readCachedTokensFromStream(span, response) {
189
+ const reader = response.body.getReader();
190
+ const decoder = new TextDecoder();
191
+ let unfinishedLine = '';
192
+ const readLines = (text) => {
193
+ const lines = (unfinishedLine + text).split('\n');
194
+ unfinishedLine = lines.pop() ?? '';
195
+ for (const line of lines) {
196
+ const data = line.trim();
197
+ if (!data.startsWith(SERVER_SENT_EVENT_DATA_PREFIX))
198
+ continue;
199
+ try {
200
+ setCacheReadTokens(span, JSON.parse(data.slice(SERVER_SENT_EVENT_DATA_PREFIX.length)));
201
+ }
202
+ catch {
203
+ // "[DONE]" and any line that is not a JSON chunk carry no usage.
204
+ }
205
+ }
206
+ };
207
+ const observedBody = new ReadableStream({
208
+ async pull(controller) {
209
+ try {
210
+ const { done, value } = await reader.read();
211
+ if (done) {
212
+ readLines(decoder.decode() + '\n');
213
+ controller.close();
214
+ return;
215
+ }
216
+ readLines(decoder.decode(value, { stream: true }));
217
+ controller.enqueue(value);
218
+ }
219
+ catch (err) {
220
+ controller.error(err);
221
+ }
222
+ },
223
+ async cancel(reason) {
224
+ await reader.cancel(reason);
225
+ },
226
+ }, { highWaterMark: 0 });
227
+ return new Response(observedBody, {
228
+ status: response.status,
229
+ statusText: response.statusText,
230
+ headers: response.headers,
231
+ });
232
+ }
233
+ async function observeCachedTokens(span, response) {
234
+ if (!span?.isRecording() || response.bodyUsed)
235
+ return response;
236
+ const contentType = (response.headers.get('content-type') || '').toLowerCase();
237
+ if (contentType.includes('text/event-stream') && response.body) {
238
+ return readCachedTokensFromStream(span, response);
239
+ }
240
+ if (contentType.includes('json')) {
241
+ setCacheReadTokens(span, await response.clone().json());
242
+ }
243
+ return response;
244
+ }
245
+ /**
246
+ * Load an ESM-only package, found the way `require` finds a package: from the
247
+ * app's node_modules and NODE_PATH, which a bare `import()` from this file
248
+ * would not search. It resolves to the same file the app's own import of the
249
+ * package loads, so both share one module, and so one `TracingHook` class.
250
+ */
251
+ function loadEsmModule(specifier) {
252
+ return import((0, node_url_1.pathToFileURL)(require.resolve(specifier)).href);
253
+ }
254
+ /**
255
+ * Whether the Mistral SDK can load `@opentelemetry/api`, an optional peer
256
+ * dependency it resolves from its own install location: without it, its
257
+ * tracing does nothing and says nothing. Installed next to weflayr, which
258
+ * depends on it, it is usually found there, but not in every layout (pnpm, a
259
+ * weflayr installed apart from the app).
260
+ */
261
+ function canTheMistralSdkLoadTheOtelApi() {
262
+ try {
263
+ (0, node_module_1.createRequire)(require.resolve(exports.MISTRAL_TRACING_HOOK_MODULE)).resolve(OTEL_API_PACKAGE);
264
+ return true;
265
+ }
266
+ catch {
267
+ return false;
268
+ }
269
+ }
270
+ class MistralInstrumentor {
271
+ loadModule;
272
+ canTheSdkLoadTheOtelApi;
273
+ // The instance that patched the Mistral SDK, undefined while it is unpatched:
274
+ // autoInstrument builds a new instance on every call, and patching twice
275
+ // would record every Vertex call twice.
276
+ static activeInstance;
277
+ _tracingHookClass;
278
+ _originalBeforeRequest;
279
+ _originalAfterSuccess;
280
+ _ownsTelemetryEnv = false;
281
+ _vertexInstrumentor;
282
+ constructor(loadModule = loadEsmModule, canTheSdkLoadTheOtelApi = canTheMistralSdkLoadTheOtelApi) {
283
+ this.loadModule = loadModule;
284
+ this.canTheSdkLoadTheOtelApi = canTheSdkLoadTheOtelApi;
285
+ this._vertexInstrumentor = new get_vertex_telemetry_1.MistralVertexInstrumentor(loadModule);
286
+ }
287
+ async instrument() {
288
+ let tracingModule;
289
+ try {
290
+ tracingModule = await this.loadModule(exports.MISTRAL_TRACING_HOOK_MODULE);
291
+ }
292
+ catch (err) {
293
+ throw new Error('weflayr.autoInstrument(MISTRAL): the @mistralai/mistralai client is not installed. ' +
294
+ 'Install it with: npm install @mistralai/mistralai\n' +
295
+ `Underlying error: ${err?.message || err}`);
296
+ }
297
+ if (!this.canTheSdkLoadTheOtelApi()) {
298
+ throw new Error('weflayr.autoInstrument(MISTRAL): @mistralai/mistralai cannot load @opentelemetry/api, ' +
299
+ 'so its tracing would record nothing. Install it next to it with: npm install @opentelemetry/api');
300
+ }
301
+ if (MistralInstrumentor.activeInstance)
302
+ return;
303
+ // "global" routes the SDK's spans to the global TracerProvider (the one
304
+ // weflayr.setup() installed). Any explicit user choice is kept.
305
+ if (!(exports.MISTRAL_SDK_TELEMETRY_ENV in process.env)) {
306
+ process.env[exports.MISTRAL_SDK_TELEMETRY_ENV] = 'global';
307
+ this._ownsTelemetryEnv = true;
308
+ }
309
+ const { TracingHook, TRACING_SPAN_KEY, TRACING_BODY_KEY } = tracingModule;
310
+ const originalBeforeRequest = TracingHook.prototype.beforeRequest;
311
+ const originalAfterSuccess = TracingHook.prototype.afterSuccess;
312
+ TracingHook.prototype.beforeRequest = async function beforeRequestWithWeflayrComplements(hookContext, request) {
313
+ const tracedRequest = await originalBeforeRequest.call(this, hookContext, request);
314
+ // Never break the user's Mistral call over an enrichment failure.
315
+ try {
316
+ const span = hookContext?.[TRACING_SPAN_KEY];
317
+ if (span?.isRecording()) {
318
+ dropRequestContentAttributes(span);
319
+ const requestBodyJson = hookContext[TRACING_BODY_KEY];
320
+ if (requestBodyJson)
321
+ setRequestBodyTelemetry(span, requestBodyJson);
322
+ // After the request complements: the content filter applies to what
323
+ // the tracing sets, as the response comes in.
324
+ shapeAttributesAsTheyAreSet(span);
325
+ }
326
+ }
327
+ catch (err) {
328
+ (0, logger_1.debug)('mistral: request complements not stamped: %s', err?.message || err);
329
+ }
330
+ return tracedRequest;
331
+ };
332
+ TracingHook.prototype.afterSuccess = async function afterSuccessWithCacheReadTokens(hookContext, response) {
333
+ let observedResponse = response;
334
+ try {
335
+ observedResponse = await observeCachedTokens(hookContext?.[TRACING_SPAN_KEY], response);
336
+ }
337
+ catch (err) {
338
+ (0, logger_1.debug)('mistral: cache-read tokens not stamped: %s', err?.message || err);
339
+ }
340
+ return originalAfterSuccess.call(this, hookContext, observedResponse);
341
+ };
342
+ this._tracingHookClass = TracingHook;
343
+ this._originalBeforeRequest = originalBeforeRequest;
344
+ this._originalAfterSuccess = originalAfterSuccess;
345
+ await this._vertexInstrumentor.instrument(tracingModule);
346
+ MistralInstrumentor.activeInstance = this;
347
+ }
348
+ uninstrument() {
349
+ if (!this._tracingHookClass)
350
+ return;
351
+ this._tracingHookClass.prototype.beforeRequest = this._originalBeforeRequest;
352
+ this._tracingHookClass.prototype.afterSuccess = this._originalAfterSuccess;
353
+ this._tracingHookClass = undefined;
354
+ this._originalBeforeRequest = undefined;
355
+ this._originalAfterSuccess = undefined;
356
+ this._vertexInstrumentor.uninstrument();
357
+ MistralInstrumentor.activeInstance = undefined;
358
+ if (this._ownsTelemetryEnv) {
359
+ delete process.env[exports.MISTRAL_SDK_TELEMETRY_ENV];
360
+ this._ownsTelemetryEnv = false;
361
+ }
362
+ }
363
+ }
364
+ exports.MistralInstrumentor = MistralInstrumentor;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Media parts of the input messages on Mistral chat spans.
3
+ *
4
+ * The SDK's built-in tracing shapes exactly two kinds of content block: text,
5
+ * and `image_url`, which it records as a `uri` part. Every other block reaches
6
+ * `gen_ai.input.messages` as a bare `{"type": "<block type>"}` (the fallback
7
+ * branch of its `formatting.ts`), so a PDF sent as `document_url` is stored
8
+ * under a name with no mime type, where every other provider stores the
9
+ * byte-less `{"type": "blob", "mime_type": "application/pdf"}`.
10
+ *
11
+ * The mime type it lacks is in the request the caller sent: the block's url is
12
+ * a `data:` URI holding the bytes. This complement walks the parts the tracing
13
+ * just stamped alongside the request's blocks and replaces those nameless parts
14
+ * with the same stub the other providers produce. Blocks the tracing did shape,
15
+ * and blocks whose url points at a hosted file rather than at inline bytes, are
16
+ * left alone.
17
+ */
18
+ import type { Span } from '@opentelemetry/api';
19
+ export declare const INPUT_MESSAGES_ATTRIBUTE = "gen_ai.input.messages";
20
+ /**
21
+ * `gen_ai.input.messages` with every nameless media part replaced by its blob
22
+ * stub, or null when there was nothing to repair (the tracing's own JSON is
23
+ * then left untouched).
24
+ *
25
+ * A message's content blocks map one-to-one onto its parts, in order, which is
26
+ * what lets the repair walk them in step.
27
+ */
28
+ export declare function repairMediaInputParts(inputMessagesJson: unknown, requestMessages: unknown): string | null;
29
+ /**
30
+ * Re-stamp `gen_ai.input.messages` when the request carries inline media.
31
+ *
32
+ * Only ever rewrites an attribute the tracing already set, so it never adds
33
+ * message content the tracing did not record.
34
+ */
35
+ export declare function setInputMessageContentTelemetry(span: Span, requestBody: Record<string, unknown>): void;