weflayr 0.27.0 → 0.29.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 +4 -4
  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 +49 -13
  82. package/dist/src/otel/propagation.js +71 -28
  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 +4 -4
  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
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Request-side prompt-caching config capture for Bedrock invoke_model spans.
2
+ * Request-side capture for Bedrock spans: the prompt-caching config, and the
3
+ * AWS region the call was sent to.
3
4
  *
4
5
  * Bedrock callers opt into caching in the request body, in the model vendor's
5
6
  * own schema: Anthropic-on-Bedrock bodies carry `cache_control` blocks (the
@@ -12,11 +13,22 @@
12
13
  * request JSON) to stamp every breakpoint found as `weflayr.cache.breakpoints`.
13
14
  * Bedrock responses report no TTL split of the written tokens; the worker
14
15
  * derives it from these breakpoints when they all share one TTL.
16
+ *
17
+ * The region rides along: AWS bills a Bedrock call at the price of the region
18
+ * it is sent to, whatever geography a cross-region inference profile then
19
+ * routes it to, and a call carrying no region is priced by no row at all. Only
20
+ * the client names it, and `_startSpan` is handed the command but never the
21
+ * client, so `send` is wrapped to resolve the region and record it against the
22
+ * command the span is then started from. The client resolves its region
23
+ * through an async provider, hence the await before the call rather than a
24
+ * lookup inside `_startSpan`.
15
25
  */
16
26
  import { type CacheBreakpoint } from '../anthropic/get_cache_config_telemetry';
27
+ export declare function recordCommandRegion(command: object, region: string): void;
17
28
  export declare function extractBedrockCacheBreakpoints(body: any): CacheBreakpoint[];
18
29
  export declare class BedrockCacheConfigInstrumentor {
19
30
  private _patched;
20
31
  instrument(instrumentation: any): void;
32
+ private _recordRegionPerCommand;
21
33
  uninstrument(): void;
22
34
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  /**
3
- * Request-side prompt-caching config capture for Bedrock invoke_model spans.
3
+ * Request-side capture for Bedrock spans: the prompt-caching config, and the
4
+ * AWS region the call was sent to.
4
5
  *
5
6
  * Bedrock callers opt into caching in the request body, in the model vendor's
6
7
  * own schema: Anthropic-on-Bedrock bodies carry `cache_control` blocks (the
@@ -13,12 +14,32 @@
13
14
  * request JSON) to stamp every breakpoint found as `weflayr.cache.breakpoints`.
14
15
  * Bedrock responses report no TTL split of the written tokens; the worker
15
16
  * derives it from these breakpoints when they all share one TTL.
17
+ *
18
+ * The region rides along: AWS bills a Bedrock call at the price of the region
19
+ * it is sent to, whatever geography a cross-region inference profile then
20
+ * routes it to, and a call carrying no region is priced by no row at all. Only
21
+ * the client names it, and `_startSpan` is handed the command but never the
22
+ * client, so `send` is wrapped to resolve the region and record it against the
23
+ * command the span is then started from. The client resolves its region
24
+ * through an async provider, hence the await before the call rather than a
25
+ * lookup inside `_startSpan`.
16
26
  */
17
27
  Object.defineProperty(exports, "__esModule", { value: true });
18
28
  exports.BedrockCacheConfigInstrumentor = void 0;
29
+ exports.recordCommandRegion = recordCommandRegion;
19
30
  exports.extractBedrockCacheBreakpoints = extractBedrockCacheBreakpoints;
20
31
  const get_cache_config_telemetry_1 = require("../anthropic/get_cache_config_telemetry");
21
32
  const BREAKPOINTS_ATTR = 'weflayr.cache.breakpoints';
33
+ const PROVIDER_REGION_ATTR = 'weflayr.provider_region';
34
+ // The region of the client each pending command was sent with. Keyed by the
35
+ // command object, which `_startSpan` is called with synchronously, so an entry
36
+ // is read by the span of its own call and by no other.
37
+ const regionByCommand = new WeakMap();
38
+ // Record the region a command was sent with. The `send` wrapper below is the
39
+ // only production caller; the tests use it to stand in for a real client.
40
+ function recordCommandRegion(command, region) {
41
+ regionByCommand.set(command, region);
42
+ }
22
43
  // A breakpoint as location + the raw `cachePoint` `ttl` string, omitted when
23
44
  // the request sets none.
24
45
  function _cachePointEntry(location, cachePoint) {
@@ -79,6 +100,12 @@ class BedrockCacheConfigInstrumentor {
79
100
  span.setAttribute(BREAKPOINTS_ATTR, JSON.stringify(breakpoints));
80
101
  }
81
102
  }
103
+ const command = request && request.params;
104
+ const region = command && regionByCommand.get(command);
105
+ if (region) {
106
+ span.setAttribute(PROVIDER_REGION_ATTR, region);
107
+ regionByCommand.delete(command);
108
+ }
82
109
  }
83
110
  catch {
84
111
  // Never break span creation over an enrichment failure.
@@ -86,6 +113,34 @@ class BedrockCacheConfigInstrumentor {
86
113
  return span;
87
114
  };
88
115
  this._patched.push([instrumentation, '_startSpan', startSpanOriginal]);
116
+ this._recordRegionPerCommand();
117
+ }
118
+ // Wrap `send` outside the instrumentor's own wrapper, so the region is
119
+ // resolved and recorded before the span it belongs to is started.
120
+ _recordRegionPerCommand() {
121
+ let BedrockRuntimeClient;
122
+ try {
123
+ ({ BedrockRuntimeClient } = require('@aws-sdk/client-bedrock-runtime'));
124
+ }
125
+ catch {
126
+ // The caller reaches Bedrock another way: nothing to wrap.
127
+ return;
128
+ }
129
+ const sendOriginal = BedrockRuntimeClient.prototype.send;
130
+ if (typeof sendOriginal !== 'function')
131
+ return;
132
+ BedrockRuntimeClient.prototype.send = async function sendRecordingRegion(command, ...rest) {
133
+ try {
134
+ const region = await this.config.region();
135
+ if (region && command)
136
+ recordCommandRegion(command, String(region));
137
+ }
138
+ catch {
139
+ // Never fail the call over an enrichment failure.
140
+ }
141
+ return sendOriginal.apply(this, [command, ...rest]);
142
+ };
143
+ this._patched.push([BedrockRuntimeClient.prototype, 'send', sendOriginal]);
89
144
  }
90
145
  uninstrument() {
91
146
  for (const [owner, methodName, original] of this._patched) {
@@ -65,7 +65,7 @@ class BedrockPromptCachingInstrumentor {
65
65
  instrumentation._wrapPromise = function wrapPromiseWithCache(span, promise, modelId) {
66
66
  // Stamp cache tokens off the response, then hand the same response to the
67
67
  // original handler (which decodes the still-buffered body for input/output
68
- // tokens). Resolving to the original response keeps behaviour identical.
68
+ // tokens). Resolving to the original response keeps behavior identical.
69
69
  const enriched = Promise.resolve(promise).then((response) => {
70
70
  _setCacheTokens(span, response);
71
71
  return response;
@@ -74,14 +74,17 @@ class BedrockPromptCachingInstrumentor {
74
74
  };
75
75
  this._patched.push([instrumentation, '_wrapPromise', wrapPromiseOriginal]);
76
76
  }
77
- // Streamed: the instrumentor processes the trailing metadata chunk via
78
- // `_handleNovaStreamingMetadata(span, parsedResponse)` (reading
79
- // `parsedResponse.metadata.usage` for input/output) but never its cache counts.
77
+ // Streamed: the instrumentor hands every chunk to
78
+ // `_handleNovaStreamingMetadata(span, parsedResponse)` but never reads its
79
+ // cache counts: Nova's arrive in the trailing `metadata` chunk, Anthropic's
80
+ // in the opening `message_start` one.
80
81
  const metadataOriginal = instrumentation._handleNovaStreamingMetadata;
81
82
  if (typeof metadataOriginal === 'function') {
82
83
  instrumentation._handleNovaStreamingMetadata = function handleMetadataWithCache(span, parsedResponse) {
83
84
  try {
84
- const usage = parsedResponse && parsedResponse.metadata && parsedResponse.metadata.usage;
85
+ const usage = parsedResponse?.type === 'message_start'
86
+ ? parsedResponse.message?.usage
87
+ : parsedResponse?.metadata?.usage;
85
88
  _setCacheFromUsage(span, usage);
86
89
  }
87
90
  catch {
@@ -6,7 +6,7 @@
6
6
  * "amazon.nova-micro-v1:0" becomes "nova-micro-v1:0" and
7
7
  * "us.anthropic.claude-haiku-4-5-20251001-v1:0" becomes
8
8
  * "claude-haiku-4-5-20251001-v1:0". Neither stripped form matches anything in the
9
- * pricing catalogue, whose Bedrock keys are the full ids, so those calls price at
9
+ * pricing catalog, whose Bedrock keys are the full ids, so those calls price at
10
10
  * nothing.
11
11
  *
12
12
  * Wrap `_startSpan({ params })` to re-stamp the id exactly as requested (so it
@@ -7,7 +7,7 @@
7
7
  * "amazon.nova-micro-v1:0" becomes "nova-micro-v1:0" and
8
8
  * "us.anthropic.claude-haiku-4-5-20251001-v1:0" becomes
9
9
  * "claude-haiku-4-5-20251001-v1:0". Neither stripped form matches anything in the
10
- * pricing catalogue, whose Bedrock keys are the full ids, so those calls price at
10
+ * pricing catalog, whose Bedrock keys are the full ids, so those calls price at
11
11
  * nothing.
12
12
  *
13
13
  * Wrap `_startSpan({ params })` to re-stamp the id exactly as requested (so it
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Telemetry for the Bedrock models whose InvokeModel body is OpenAI's chat
3
+ * completions format (Qwen, OpenAI's gpt-oss, DeepSeek, Kimi, MiniMax...).
4
+ *
5
+ * `@traceloop/instrumentation-bedrock` reads a request and its answer only for
6
+ * the vendors it knows (Amazon, Anthropic, Cohere, AI21, Meta), so a call to one
7
+ * of these models carries no request parameter and no answer, and is named a
8
+ * text completion. This complement:
9
+ *
10
+ * - names the call a chat (`_getOperationType`), and stamps its request
11
+ * parameters once it succeeded (`_startSpan` keeps the request, which
12
+ * `_endSpan`, only called on success, does not see), as the Python SDK
13
+ * records them only off an answer;
14
+ * - stamps a non-streamed answer, its finish reason, token counts, response
15
+ * model and id (`_setResponseAttributes`);
16
+ * - accumulates a stream into the body a non-streamed call answers, chunk by
17
+ * chunk (`_handleNovaStreamingMetadata`, which the instrumentor calls for
18
+ * every chunk whatever the vendor), and stamps it from the closing chunk.
19
+ * The instrumentor stamps a stream's token counts itself, off the closing
20
+ * chunk's `amazon-bedrock-invocationMetrics`.
21
+ *
22
+ * The input messages and the tool definitions are stamped by their own
23
+ * complements, which read a `messages` body whatever its vendor.
24
+ */
25
+ export declare function isOpenaiChatFormat(modelVendor: string, requestBody: any): boolean;
26
+ /**
27
+ * Fold one streamed chunk into the body a non-streamed call answers: the text
28
+ * deltas and each tool call's argument fragments are joined, and the token
29
+ * counts come from the closing chunk's invocation metrics.
30
+ *
31
+ * Example: chunks with deltas {content: "Hel"} then {content: "lo"}, then
32
+ * finish_reason "stop" and invocation metrics {inputTokenCount: 18,
33
+ * outputTokenCount: 4} give {choices: [{message: {content: "Hello",
34
+ * tool_calls: []}, finish_reason: "stop"}], usage: {prompt_tokens: 18,
35
+ * completion_tokens: 4}, ...}.
36
+ */
37
+ export declare function accumulateStreamChunk(accumulatingBody: any, chunk: any): void;
38
+ export declare function setRequestAndUsageAttributes(span: any, requestBody: any, responseBody: any): void;
39
+ /** The finish reasons, and the answer when its content is captured. */
40
+ export declare function setOutputAttributes(span: any, responseBody: any, shouldSendPrompts: boolean): void;
41
+ export declare class BedrockOpenaiChatFormatInstrumentor {
42
+ private _patched;
43
+ instrument(instrumentation: any): void;
44
+ uninstrument(): void;
45
+ }
@@ -0,0 +1,251 @@
1
+ "use strict";
2
+ /**
3
+ * Telemetry for the Bedrock models whose InvokeModel body is OpenAI's chat
4
+ * completions format (Qwen, OpenAI's gpt-oss, DeepSeek, Kimi, MiniMax...).
5
+ *
6
+ * `@traceloop/instrumentation-bedrock` reads a request and its answer only for
7
+ * the vendors it knows (Amazon, Anthropic, Cohere, AI21, Meta), so a call to one
8
+ * of these models carries no request parameter and no answer, and is named a
9
+ * text completion. This complement:
10
+ *
11
+ * - names the call a chat (`_getOperationType`), and stamps its request
12
+ * parameters once it succeeded (`_startSpan` keeps the request, which
13
+ * `_endSpan`, only called on success, does not see), as the Python SDK
14
+ * records them only off an answer;
15
+ * - stamps a non-streamed answer, its finish reason, token counts, response
16
+ * model and id (`_setResponseAttributes`);
17
+ * - accumulates a stream into the body a non-streamed call answers, chunk by
18
+ * chunk (`_handleNovaStreamingMetadata`, which the instrumentor calls for
19
+ * every chunk whatever the vendor), and stamps it from the closing chunk.
20
+ * The instrumentor stamps a stream's token counts itself, off the closing
21
+ * chunk's `amazon-bedrock-invocationMetrics`.
22
+ *
23
+ * The input messages and the tool definitions are stamped by their own
24
+ * complements, which read a `messages` body whatever its vendor.
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.BedrockOpenaiChatFormatInstrumentor = void 0;
28
+ exports.isOpenaiChatFormat = isOpenaiChatFormat;
29
+ exports.accumulateStreamChunk = accumulateStreamChunk;
30
+ exports.setRequestAndUsageAttributes = setRequestAndUsageAttributes;
31
+ exports.setOutputAttributes = setOutputAttributes;
32
+ // The vendors the instrumentor reads itself; every other vendor's body is
33
+ // OpenAI's chat completions format.
34
+ const VENDORS_READ_BY_THE_INSTRUMENTOR = new Set(['cohere', 'anthropic', 'ai21', 'meta', 'amazon']);
35
+ const INVOCATION_METRICS_KEY = 'amazon-bedrock-invocationMetrics';
36
+ // OpenAI's finish reasons, in the OpenTelemetry GenAI vocabulary the other
37
+ // vendors' answers are recorded in.
38
+ const FINISH_REASON_BY_OPENAI_FINISH_REASON = {
39
+ stop: 'stop',
40
+ length: 'length',
41
+ tool_calls: 'tool_call',
42
+ content_filter: 'content_filter',
43
+ };
44
+ function isOpenaiChatFormat(modelVendor, requestBody) {
45
+ return (!VENDORS_READ_BY_THE_INSTRUMENTOR.has(modelVendor) &&
46
+ requestBody !== null &&
47
+ typeof requestBody === 'object' &&
48
+ Array.isArray(requestBody.messages));
49
+ }
50
+ /**
51
+ * Fold one streamed chunk into the body a non-streamed call answers: the text
52
+ * deltas and each tool call's argument fragments are joined, and the token
53
+ * counts come from the closing chunk's invocation metrics.
54
+ *
55
+ * Example: chunks with deltas {content: "Hel"} then {content: "lo"}, then
56
+ * finish_reason "stop" and invocation metrics {inputTokenCount: 18,
57
+ * outputTokenCount: 4} give {choices: [{message: {content: "Hello",
58
+ * tool_calls: []}, finish_reason: "stop"}], usage: {prompt_tokens: 18,
59
+ * completion_tokens: 4}, ...}.
60
+ */
61
+ function accumulateStreamChunk(accumulatingBody, chunk) {
62
+ accumulatingBody.id ??= chunk.id;
63
+ accumulatingBody.model ??= chunk.model;
64
+ accumulatingBody.choices ??= [{ message: { content: '', tool_calls: [] } }];
65
+ const choices = accumulatingBody.choices;
66
+ const message = choices[0].message;
67
+ for (const choice of chunk.choices || []) {
68
+ const delta = choice.delta || {};
69
+ message.content += delta.content || '';
70
+ for (const toolCallDelta of delta.tool_calls || []) {
71
+ const index = toolCallDelta.index ?? 0;
72
+ while (message.tool_calls.length <= index) {
73
+ message.tool_calls.push({ id: null, function: { name: '', arguments: '' } });
74
+ }
75
+ const toolCall = message.tool_calls[index];
76
+ toolCall.id = toolCallDelta.id || toolCall.id;
77
+ const functionDelta = toolCallDelta.function || {};
78
+ toolCall.function.name += functionDelta.name || '';
79
+ toolCall.function.arguments += functionDelta.arguments || '';
80
+ }
81
+ if (choice.finish_reason)
82
+ choices[0].finish_reason = choice.finish_reason;
83
+ }
84
+ const invocationMetrics = chunk[INVOCATION_METRICS_KEY];
85
+ if (invocationMetrics && typeof invocationMetrics === 'object') {
86
+ accumulatingBody.usage = {
87
+ prompt_tokens: invocationMetrics.inputTokenCount,
88
+ completion_tokens: invocationMetrics.outputTokenCount,
89
+ };
90
+ }
91
+ }
92
+ function setRequestAndUsageAttributes(span, requestBody, responseBody) {
93
+ const attributes = {
94
+ 'gen_ai.request.max_tokens': requestBody.max_tokens ?? requestBody.max_completion_tokens,
95
+ 'gen_ai.request.temperature': requestBody.temperature,
96
+ 'gen_ai.request.top_p': requestBody.top_p,
97
+ 'gen_ai.response.model': responseBody.model,
98
+ 'gen_ai.response.id': responseBody.id,
99
+ };
100
+ const usage = responseBody.usage || {};
101
+ const inputTokens = usage.prompt_tokens;
102
+ const outputTokens = usage.completion_tokens;
103
+ if (Number.isInteger(inputTokens) && Number.isInteger(outputTokens)) {
104
+ attributes['gen_ai.usage.input_tokens'] = inputTokens;
105
+ attributes['gen_ai.usage.output_tokens'] = outputTokens;
106
+ attributes['gen_ai.usage.total_tokens'] = inputTokens + outputTokens;
107
+ }
108
+ for (const [name, value] of Object.entries(attributes)) {
109
+ if (value !== undefined && value !== null)
110
+ span.setAttribute(name, value);
111
+ }
112
+ }
113
+ function _parseArguments(argumentsJson) {
114
+ try {
115
+ return JSON.parse(argumentsJson);
116
+ }
117
+ catch {
118
+ return argumentsJson;
119
+ }
120
+ }
121
+ function _getFinishReason(choice) {
122
+ const openaiFinishReason = choice.finish_reason || '';
123
+ return FINISH_REASON_BY_OPENAI_FINISH_REASON[openaiFinishReason] ?? openaiFinishReason;
124
+ }
125
+ /** The finish reasons, and the answer when its content is captured. */
126
+ function setOutputAttributes(span, responseBody, shouldSendPrompts) {
127
+ const choices = responseBody.choices || [];
128
+ const finishReasons = choices.filter((choice) => choice.finish_reason).map(_getFinishReason);
129
+ if (finishReasons.length)
130
+ span.setAttribute('gen_ai.response.finish_reasons', finishReasons);
131
+ if (!shouldSendPrompts)
132
+ return;
133
+ const outputMessages = choices.map((choice) => {
134
+ const message = choice.message || {};
135
+ const parts = [];
136
+ if (message.content)
137
+ parts.push({ type: 'text', content: message.content });
138
+ for (const toolCall of message.tool_calls || []) {
139
+ parts.push({
140
+ type: 'tool_call',
141
+ id: toolCall.id,
142
+ name: toolCall.function?.name,
143
+ arguments: _parseArguments(toolCall.function?.arguments || ''),
144
+ });
145
+ }
146
+ return { role: 'assistant', parts, finish_reason: _getFinishReason(choice) };
147
+ });
148
+ span.setAttribute('gen_ai.output.messages', JSON.stringify(outputMessages));
149
+ }
150
+ class BedrockOpenaiChatFormatInstrumentor {
151
+ // [instrumentation, methodName, originalFunction] for uninstrument().
152
+ _patched = [];
153
+ // No-op (and never throws) if the instrumentor's internals have changed shape:
154
+ // the worst case is a call recorded without its answer, never a crash.
155
+ instrument(instrumentation) {
156
+ if (!instrumentation)
157
+ return;
158
+ const originalGetOperationType = instrumentation._getOperationType;
159
+ const originalStartSpan = instrumentation._startSpan;
160
+ const originalSetResponseAttributes = instrumentation._setResponseAttributes;
161
+ const originalHandleStreamChunk = instrumentation._handleNovaStreamingMetadata;
162
+ const originalEndSpan = instrumentation._endSpan;
163
+ if (typeof originalGetOperationType !== 'function' ||
164
+ typeof originalStartSpan !== 'function' ||
165
+ typeof originalSetResponseAttributes !== 'function' ||
166
+ typeof originalHandleStreamChunk !== 'function' ||
167
+ typeof originalEndSpan !== 'function') {
168
+ return;
169
+ }
170
+ // span -> the request and the answer accumulated so far, for an
171
+ // OpenAI-format call.
172
+ const callBySpan = new WeakMap();
173
+ instrumentation._getOperationType = function getOperationType(vendor, requestBody) {
174
+ if (isOpenaiChatFormat(vendor, requestBody))
175
+ return 'chat';
176
+ return originalGetOperationType.call(this, vendor, requestBody);
177
+ };
178
+ instrumentation._startSpan = function startSpanRecordingOpenaiChatFormatCalls(request) {
179
+ const span = originalStartSpan.call(this, request);
180
+ try {
181
+ const input = request?.params?.input;
182
+ const requestBody = typeof input?.body === 'string' ? JSON.parse(input.body) : null;
183
+ const { modelVendor } = this._extractVendorAndModel(input?.modelId || '');
184
+ if (isOpenaiChatFormat(modelVendor, requestBody)) {
185
+ callBySpan.set(span, { requestBody, accumulatingBody: {} });
186
+ }
187
+ }
188
+ catch {
189
+ // Never break span creation over an enrichment failure.
190
+ }
191
+ return span;
192
+ };
193
+ instrumentation._setResponseAttributes = function setResponseAttributesOfOpenaiChatFormat(vendor, response, isStream = false, ...args) {
194
+ const attributes = originalSetResponseAttributes.call(this, vendor, response, isStream, ...args);
195
+ try {
196
+ if (!isStream &&
197
+ !VENDORS_READ_BY_THE_INSTRUMENTOR.has(vendor) &&
198
+ Array.isArray(response?.choices)) {
199
+ const recordedAttributes = {};
200
+ const attributeCollector = {
201
+ setAttribute(name, value) {
202
+ recordedAttributes[name] = value;
203
+ },
204
+ };
205
+ setRequestAndUsageAttributes(attributeCollector, {}, response);
206
+ setOutputAttributes(attributeCollector, response, this._shouldSendPrompts());
207
+ return { ...attributes, ...recordedAttributes };
208
+ }
209
+ }
210
+ catch {
211
+ // Never break the response handling over an enrichment failure.
212
+ }
213
+ return attributes;
214
+ };
215
+ instrumentation._handleNovaStreamingMetadata = function handleOpenaiChatFormatStreamChunk(span, parsedResponse) {
216
+ try {
217
+ const call = callBySpan.get(span);
218
+ if (call) {
219
+ accumulateStreamChunk(call.accumulatingBody, parsedResponse);
220
+ if (parsedResponse && INVOCATION_METRICS_KEY in parsedResponse) {
221
+ setRequestAndUsageAttributes(span, {}, call.accumulatingBody);
222
+ setOutputAttributes(span, call.accumulatingBody, this._shouldSendPrompts());
223
+ }
224
+ }
225
+ }
226
+ catch {
227
+ // Never break the caller's stream over an enrichment failure.
228
+ }
229
+ return originalHandleStreamChunk.call(this, span, parsedResponse);
230
+ };
231
+ instrumentation._endSpan = function endSpanWithOpenaiChatFormatRequest(endSpanArguments) {
232
+ try {
233
+ const call = callBySpan.get(endSpanArguments?.span);
234
+ if (call)
235
+ setRequestAndUsageAttributes(endSpanArguments.span, call.requestBody, {});
236
+ }
237
+ catch {
238
+ // Never break the response handling over an enrichment failure.
239
+ }
240
+ return originalEndSpan.call(this, endSpanArguments);
241
+ };
242
+ this._patched.push([instrumentation, '_getOperationType', originalGetOperationType], [instrumentation, '_startSpan', originalStartSpan], [instrumentation, '_setResponseAttributes', originalSetResponseAttributes], [instrumentation, '_handleNovaStreamingMetadata', originalHandleStreamChunk], [instrumentation, '_endSpan', originalEndSpan]);
243
+ }
244
+ uninstrument() {
245
+ for (const [owner, methodName, original] of [...this._patched].reverse()) {
246
+ owner[methodName] = original;
247
+ }
248
+ this._patched = [];
249
+ }
250
+ }
251
+ exports.BedrockOpenaiChatFormatInstrumentor = BedrockOpenaiChatFormatInstrumentor;
@@ -6,11 +6,12 @@
6
6
  * block mapper, which only accepts block arrays — so streamed Nova spans end up
7
7
  * with an assistant message whose `parts` is empty. This complement wraps the
8
8
  * instrumentor's per-chunk `_handleNovaStreamingMetadata(span, parsedResponse)`
9
- * hook: it accumulates the `contentBlockDelta` text and the `messageStop` stop
10
- * reason itself, and re-stamps a correct output message when the trailing
11
- * `metadata` chunk (always last in a Nova stream) arrives — after the
12
- * instrumentor has written its empty-parts message. The span processor strips
13
- * the attribute when message-content capture is disabled.
9
+ * hook: it accumulates the text and the stop reason itself, Nova's
10
+ * (`contentBlockDelta`, `messageStop`) and Anthropic's (`content_block_delta`,
11
+ * `message_delta`), and re-stamps a correct output message when the stream's
12
+ * last chunk arrives (Nova's trailing `metadata`, Anthropic's `message_stop`),
13
+ * after the instrumentor has written its empty-parts message. The span
14
+ * processor strips the attribute when message-content capture is disabled.
14
15
  *
15
16
  * A streamed tool call is accumulated the same way and for the same reason:
16
17
  * `contentBlockStart` opens it with an id and name, `contentBlockDelta` appends
@@ -7,11 +7,12 @@
7
7
  * block mapper, which only accepts block arrays — so streamed Nova spans end up
8
8
  * with an assistant message whose `parts` is empty. This complement wraps the
9
9
  * instrumentor's per-chunk `_handleNovaStreamingMetadata(span, parsedResponse)`
10
- * hook: it accumulates the `contentBlockDelta` text and the `messageStop` stop
11
- * reason itself, and re-stamps a correct output message when the trailing
12
- * `metadata` chunk (always last in a Nova stream) arrives — after the
13
- * instrumentor has written its empty-parts message. The span processor strips
14
- * the attribute when message-content capture is disabled.
10
+ * hook: it accumulates the text and the stop reason itself, Nova's
11
+ * (`contentBlockDelta`, `messageStop`) and Anthropic's (`content_block_delta`,
12
+ * `message_delta`), and re-stamps a correct output message when the stream's
13
+ * last chunk arrives (Nova's trailing `metadata`, Anthropic's `message_stop`),
14
+ * after the instrumentor has written its empty-parts message. The span
15
+ * processor strips the attribute when message-content capture is disabled.
15
16
  *
16
17
  * A streamed tool call is accumulated the same way and for the same reason:
17
18
  * `contentBlockStart` opens it with an id and name, `contentBlockDelta` appends
@@ -22,7 +23,7 @@
22
23
  */
23
24
  Object.defineProperty(exports, "__esModule", { value: true });
24
25
  exports.BedrockStreamingOutputMessageInstrumentor = void 0;
25
- // Nova stop reasons mapped to the OTel finish_reason values the instrumentor
26
+ // Nova's and Anthropic's stop reasons mapped to the OTel finish_reason values the instrumentor
26
27
  // uses on non-streamed spans, so streamed and non-streamed rows normalize alike.
27
28
  const FINISH_REASON_BY_STOP_REASON = {
28
29
  end_turn: 'stop',
@@ -75,18 +76,20 @@ class BedrockStreamingOutputMessageInstrumentor {
75
76
  stopReason: null,
76
77
  toolCalls: [],
77
78
  };
78
- const delta = parsedResponse &&
79
- parsedResponse.contentBlockDelta &&
80
- parsedResponse.contentBlockDelta.delta &&
81
- parsedResponse.contentBlockDelta.delta.text;
79
+ const delta = parsedResponse?.contentBlockDelta?.delta?.text ??
80
+ (parsedResponse?.type === 'content_block_delta' ? parsedResponse.delta?.text : undefined);
82
81
  if (typeof delta === 'string')
83
82
  state.text += delta;
84
- const stopReason = parsedResponse && parsedResponse.messageStop && parsedResponse.messageStop.stopReason;
83
+ const stopReason = parsedResponse?.messageStop?.stopReason ??
84
+ (parsedResponse?.type === 'message_delta'
85
+ ? parsedResponse.delta?.stop_reason
86
+ : undefined);
85
87
  if (stopReason != null)
86
88
  state.stopReason = stopReason;
87
89
  _accumulateToolCall(state, parsedResponse);
88
90
  streamStateBySpan.set(span, state);
89
- if (parsedResponse && parsedResponse.metadata && (state.text || state.toolCalls.length)) {
91
+ const isLastChunk = parsedResponse?.metadata || parsedResponse?.type === 'message_stop';
92
+ if (isLastChunk && (state.text || state.toolCalls.length)) {
90
93
  const parts = [];
91
94
  if (state.text)
92
95
  parts.push({ type: 'text', content: state.text });
@@ -5,8 +5,8 @@
5
5
  * the inference config but never for its `toolConfig`, so a tool-using call
6
6
  * carries no `gen_ai.tool.definitions`. Wrap the instrumentor's
7
7
  * `_startSpan({ params })` to stamp the definitions from the request body,
8
- * unwrapping each entry's `toolSpec` so the attribute holds the bare
9
- * `{name, description, inputSchema}` definitions rather than Bedrock's wrapper.
8
+ * unwrapping each entry's `toolSpec` (or OpenAI's `function`) so the attribute
9
+ * holds the bare definitions rather than their wrapper.
10
10
  */
11
11
  export declare class BedrockToolDefinitionsInstrumentor {
12
12
  private _patched;
@@ -6,14 +6,17 @@
6
6
  * the inference config but never for its `toolConfig`, so a tool-using call
7
7
  * carries no `gen_ai.tool.definitions`. Wrap the instrumentor's
8
8
  * `_startSpan({ params })` to stamp the definitions from the request body,
9
- * unwrapping each entry's `toolSpec` so the attribute holds the bare
10
- * `{name, description, inputSchema}` definitions rather than Bedrock's wrapper.
9
+ * unwrapping each entry's `toolSpec` (or OpenAI's `function`) so the attribute
10
+ * holds the bare definitions rather than their wrapper.
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.BedrockToolDefinitionsInstrumentor = void 0;
14
- // The bare tool definitions from an InvokeModel JSON body, e.g.
14
+ // The bare tool definitions from an InvokeModel JSON body: Nova's, e.g.
15
15
  // `{"toolConfig": {"tools": [{"toolSpec": {"name": "get_weather", ...}}]}}`
16
- // -> `[{"name": "get_weather", ...}]`.
16
+ // -> `[{"name": "get_weather", ...}]`, or OpenAI's chat completions format, e.g.
17
+ // `{"tools": [{"type": "function", "function": {"name": "get_weather", ...}}]}`
18
+ // -> `[{"name": "get_weather", ...}]`. An Anthropic body's tools, which the
19
+ // instrumentor records itself, give none.
17
20
  function _toolDefinitionsFromRequestBody(rawBody) {
18
21
  if (typeof rawBody !== 'string')
19
22
  return [];
@@ -24,6 +27,11 @@ function _toolDefinitionsFromRequestBody(rawBody) {
24
27
  catch {
25
28
  return [];
26
29
  }
30
+ if (body && Array.isArray(body.tools)) {
31
+ return body.tools
32
+ .filter((tool) => tool && typeof tool.function === 'object' && tool.function !== null)
33
+ .map((tool) => tool.function);
34
+ }
27
35
  const tools = body && body.toolConfig && body.toolConfig.tools;
28
36
  if (!Array.isArray(tools))
29
37
  return [];
@@ -7,8 +7,10 @@
7
7
  * breakpoints (`cache_control` / `cachePoint` blocks it never records), the
8
8
  * streamed output message content (its stream path formats an empty-parts
9
9
  * message), the request's tool definitions (it never reads the body's
10
- * `toolConfig`) and the tool-call output parts (its block mapper only knows
11
- * Anthropic's block shape, so Nova's `toolUse` blocks come through raw).
10
+ * `toolConfig`), the tool-call output parts (its block mapper only knows
11
+ * Anthropic's block shape, so Nova's `toolUse` blocks come through raw), and
12
+ * everything about a model whose body is OpenAI's chat completions format (it
13
+ * reads only the vendors it knows).
12
14
  */
13
15
  export declare class BedrockTelemetryInstrumentor {
14
16
  private _subInstrumentors;
@@ -8,8 +8,10 @@
8
8
  * breakpoints (`cache_control` / `cachePoint` blocks it never records), the
9
9
  * streamed output message content (its stream path formats an empty-parts
10
10
  * message), the request's tool definitions (it never reads the body's
11
- * `toolConfig`) and the tool-call output parts (its block mapper only knows
12
- * Anthropic's block shape, so Nova's `toolUse` blocks come through raw).
11
+ * `toolConfig`), the tool-call output parts (its block mapper only knows
12
+ * Anthropic's block shape, so Nova's `toolUse` blocks come through raw), and
13
+ * everything about a model whose body is OpenAI's chat completions format (it
14
+ * reads only the vendors it knows).
13
15
  */
14
16
  Object.defineProperty(exports, "__esModule", { value: true });
15
17
  exports.BedrockTelemetryInstrumentor = void 0;
@@ -18,6 +20,7 @@ const get_cache_telemetry_1 = require("./get_cache_telemetry");
18
20
  const get_input_message_content_telemetry_1 = require("./get_input_message_content_telemetry");
19
21
  const get_streaming_output_message_telemetry_1 = require("./get_streaming_output_message_telemetry");
20
22
  const get_model_id_telemetry_1 = require("./get_model_id_telemetry");
23
+ const get_openai_chat_format_telemetry_1 = require("./get_openai_chat_format_telemetry");
21
24
  const get_reasoning_output_telemetry_1 = require("./get_reasoning_output_telemetry");
22
25
  const get_tool_call_output_telemetry_1 = require("./get_tool_call_output_telemetry");
23
26
  const get_tool_definitions_telemetry_1 = require("./get_tool_definitions_telemetry");
@@ -33,14 +36,18 @@ class BedrockTelemetryInstrumentor {
33
36
  new get_tool_call_output_telemetry_1.BedrockToolCallOutputInstrumentor(),
34
37
  new get_model_id_telemetry_1.BedrockModelIdInstrumentor(),
35
38
  new get_reasoning_output_telemetry_1.BedrockReasoningOutputInstrumentor(),
39
+ new get_openai_chat_format_telemetry_1.BedrockOpenaiChatFormatInstrumentor(),
36
40
  ];
37
41
  }
38
42
  instrument(instrumentation) {
39
43
  for (const sub of this._subInstrumentors)
40
44
  sub.instrument(instrumentation);
41
45
  }
46
+ // Reversed: several complements wrap the same instrumentor method
47
+ // (`_startSpan`), so the patches have to be unwound last-patched-first or an
48
+ // earlier wrapper is written back over a later one and stays installed.
42
49
  uninstrument() {
43
- for (const sub of this._subInstrumentors)
50
+ for (const sub of [...this._subInstrumentors].reverse())
44
51
  sub.uninstrument();
45
52
  }
46
53
  }