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
@@ -0,0 +1,89 @@
1
+ /**
2
+ * OpenAI Responses (`/v1/responses`) telemetry complement.
3
+ *
4
+ * `@traceloop/instrumentation-openai` wraps `Responses.create` but records far
5
+ * less than the Python OpenLLMetry instrumentor does for the same call, so
6
+ * without this complement the two weflayr SDKs would store different rows for
7
+ * the same request. Three gaps, all closed here:
8
+ *
9
+ * 1. Input messages. Upstream stringifies the whole `input` into a single
10
+ * `{role: "user", parts: [{type: "text", content: "<JSON>"}]}` part. That
11
+ * loses every role and part type, and it buries an inline image or
12
+ * document's base64 inside a text string, where `strip-media-payloads`
13
+ * cannot see it and the payload would reach the queue. This rebuilds the
14
+ * per-role, per-part shape the Python instrumentor emits.
15
+ * 2. Response side. Upstream records only the output text, so a tool call or
16
+ * a reasoning summary is lost, the finish reason is never `tool_call`, and
17
+ * `input_tokens_details` / `output_tokens_details` (cached tokens, the
18
+ * cache-write count, reasoning tokens) are dropped entirely.
19
+ * 3. Streamed calls are not instrumented at all upstream: its wrapper ends
20
+ * the span with request attributes only and returns the stream untouched,
21
+ * so a streamed call would be stored as a real call that cost nothing.
22
+ * This takes over that one branch: `stream: true` bypasses the upstream
23
+ * wrapper and runs our own span, accumulating the stream's events.
24
+ *
25
+ * Gap 3's takeover ends the span when the stream is iterated, which is how a
26
+ * stream is consumed in practice. A stream handed to `tee()` or
27
+ * `toReadableStream()` instead is never iterated, so its span never ends and
28
+ * the call is not recorded: a missing call, never a call recorded as free.
29
+ *
30
+ * It also stamps xAI's document searches, which neither instrumentor records:
31
+ * xAI reads a document attached to a Responses request by searching it with its
32
+ * own tool, and bills each search, reported as
33
+ * `usage.server_side_tool_usage_details.document_search_calls`
34
+ * (`weflayr.document_search.requests`).
35
+ *
36
+ * Inline media is the one deliberate divergence from the Python instrumentor's
37
+ * shape: an `input_file` carrying `file_data` becomes the byte-less
38
+ * `{type: "blob", mime_type}` stub rather than upstream's
39
+ * `{type: "file", file_id, filename}`, so an inline PDF pins the same stub here
40
+ * as it does on the chat endpoint. The Python complement makes the same repair.
41
+ * An `input_image` keeps its `data:` URI in a `uri` part, which the shared
42
+ * `strip-media-payloads` step already rewrites to that stub.
43
+ */
44
+ import { type Span } from '@opentelemetry/api';
45
+ type JsonObject = Record<string, any>;
46
+ /** The request's `instructions` + `input` as OTel input messages. */
47
+ export declare function buildResponsesInputMessages(params: JsonObject): JsonObject[];
48
+ /** The request's function tools, in the flat shape the Responses API takes. */
49
+ export declare function buildResponsesToolDefinitions(params: JsonObject): JsonObject[];
50
+ /** The response's text, tool calls and reasoning summaries as one OTel message. */
51
+ export declare function buildResponsesOutputMessages(response: JsonObject): JsonObject[];
52
+ /** How many document searches a Responses answer is billed for, from xAI's
53
+ * `usage.server_side_tool_usage_details.document_search_calls`, or null when the
54
+ * usage reports none, e.g. `{server_side_tool_usage_details:
55
+ * {document_search_calls: 1}}` -> 1. */
56
+ export declare function readBilledDocumentSearchCount(usage: any): number | null;
57
+ /**
58
+ * Wrap a Responses stream's iterator so its events pass through untouched while
59
+ * the last response object seen is stamped on the span, which ends with the
60
+ * iteration.
61
+ *
62
+ * Mutates `stream` in place and returns it, so the caller still hands the app
63
+ * the exact object the openai client returned.
64
+ */
65
+ export declare function recordResponsesStreamEvents(stream: any, span: Span, sendPrompts: boolean): any;
66
+ export declare function traceStreamedCall({ instrumentation, openaiCreate, client, params, rest, }: {
67
+ instrumentation: any;
68
+ openaiCreate: any;
69
+ client: any;
70
+ params: any;
71
+ rest: any[];
72
+ }): any;
73
+ export declare class OpenAIResponsesInstrumentor {
74
+ private _patched;
75
+ instrument(instrumentation: any): void;
76
+ uninstrument(): void;
77
+ private _patchRequestSide;
78
+ private _patchResponseSide;
79
+ /**
80
+ * Own the streamed branch upstream leaves uninstrumented.
81
+ *
82
+ * Our wrapper sits outside upstream's, so a non-streamed call is delegated
83
+ * straight to it. A streamed call instead calls the function upstream wrapped
84
+ * (shimmer's `__original`), so only one span is ever created, and ends that
85
+ * span once the stream's events are consumed.
86
+ */
87
+ private _takeOverStreamedCalls;
88
+ }
89
+ export {};
@@ -0,0 +1,587 @@
1
+ "use strict";
2
+ /**
3
+ * OpenAI Responses (`/v1/responses`) telemetry complement.
4
+ *
5
+ * `@traceloop/instrumentation-openai` wraps `Responses.create` but records far
6
+ * less than the Python OpenLLMetry instrumentor does for the same call, so
7
+ * without this complement the two weflayr SDKs would store different rows for
8
+ * the same request. Three gaps, all closed here:
9
+ *
10
+ * 1. Input messages. Upstream stringifies the whole `input` into a single
11
+ * `{role: "user", parts: [{type: "text", content: "<JSON>"}]}` part. That
12
+ * loses every role and part type, and it buries an inline image or
13
+ * document's base64 inside a text string, where `strip-media-payloads`
14
+ * cannot see it and the payload would reach the queue. This rebuilds the
15
+ * per-role, per-part shape the Python instrumentor emits.
16
+ * 2. Response side. Upstream records only the output text, so a tool call or
17
+ * a reasoning summary is lost, the finish reason is never `tool_call`, and
18
+ * `input_tokens_details` / `output_tokens_details` (cached tokens, the
19
+ * cache-write count, reasoning tokens) are dropped entirely.
20
+ * 3. Streamed calls are not instrumented at all upstream: its wrapper ends
21
+ * the span with request attributes only and returns the stream untouched,
22
+ * so a streamed call would be stored as a real call that cost nothing.
23
+ * This takes over that one branch: `stream: true` bypasses the upstream
24
+ * wrapper and runs our own span, accumulating the stream's events.
25
+ *
26
+ * Gap 3's takeover ends the span when the stream is iterated, which is how a
27
+ * stream is consumed in practice. A stream handed to `tee()` or
28
+ * `toReadableStream()` instead is never iterated, so its span never ends and
29
+ * the call is not recorded: a missing call, never a call recorded as free.
30
+ *
31
+ * It also stamps xAI's document searches, which neither instrumentor records:
32
+ * xAI reads a document attached to a Responses request by searching it with its
33
+ * own tool, and bills each search, reported as
34
+ * `usage.server_side_tool_usage_details.document_search_calls`
35
+ * (`weflayr.document_search.requests`).
36
+ *
37
+ * Inline media is the one deliberate divergence from the Python instrumentor's
38
+ * shape: an `input_file` carrying `file_data` becomes the byte-less
39
+ * `{type: "blob", mime_type}` stub rather than upstream's
40
+ * `{type: "file", file_id, filename}`, so an inline PDF pins the same stub here
41
+ * as it does on the chat endpoint. The Python complement makes the same repair.
42
+ * An `input_image` keeps its `data:` URI in a `uri` part, which the shared
43
+ * `strip-media-payloads` step already rewrites to that stub.
44
+ */
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.OpenAIResponsesInstrumentor = void 0;
47
+ exports.buildResponsesInputMessages = buildResponsesInputMessages;
48
+ exports.buildResponsesToolDefinitions = buildResponsesToolDefinitions;
49
+ exports.buildResponsesOutputMessages = buildResponsesOutputMessages;
50
+ exports.readBilledDocumentSearchCount = readBilledDocumentSearchCount;
51
+ exports.recordResponsesStreamEvents = recordResponsesStreamEvents;
52
+ exports.traceStreamedCall = traceStreamedCall;
53
+ const api_1 = require("@opentelemetry/api");
54
+ const get_data_uri_mime_type_1 = require("../../get-data-uri-mime-type");
55
+ const get_web_search_telemetry_1 = require("./get_web_search_telemetry");
56
+ // traceloop's internal tag for the Responses request/response shape (its
57
+ // `RESPONSES_TYPE`), the value it passes to `startSpan` / `_endSpan`.
58
+ const RESPONSES_TYPE = 'responses';
59
+ const INPUT_MESSAGES_ATTR = 'gen_ai.input.messages';
60
+ const OUTPUT_MESSAGES_ATTR = 'gen_ai.output.messages';
61
+ const TOOL_DEFINITIONS_ATTR = 'gen_ai.tool.definitions';
62
+ const FINISH_REASONS_ATTR = 'gen_ai.response.finish_reasons';
63
+ const RESPONSE_ID_ATTR = 'gen_ai.response.id';
64
+ const RESPONSE_MODEL_ATTR = 'gen_ai.response.model';
65
+ const INPUT_TOKENS_ATTR = 'gen_ai.usage.input_tokens';
66
+ const OUTPUT_TOKENS_ATTR = 'gen_ai.usage.output_tokens';
67
+ const TOTAL_TOKENS_ATTR = 'gen_ai.usage.total_tokens';
68
+ // The dotted spelling the Python instrumentor emits on this path; the
69
+ // normalizer reads it and the underscored chat spelling alike.
70
+ const CACHE_READ_INPUT_TOKENS_ATTR = 'gen_ai.usage.cache_read.input_tokens';
71
+ const CACHE_CREATION_INPUT_TOKENS_ATTR = 'gen_ai.usage.cache_creation_input_tokens';
72
+ const REASONING_TOKENS_ATTR = 'gen_ai.usage.reasoning_tokens';
73
+ const REQUEST_REASONING_EFFORT_ATTR = 'gen_ai.request.reasoning_effort';
74
+ const REQUEST_REASONING_SUMMARY_ATTR = 'gen_ai.request.reasoning_summary';
75
+ const RESPONSE_REASONING_EFFORT_ATTR = 'gen_ai.response.reasoning_effort';
76
+ const RESPONSE_SERVICE_TIER_ATTR = 'openai.response.service_tier';
77
+ const DOCUMENT_SEARCH_REQUESTS_ATTR = 'weflayr.document_search.requests';
78
+ const TRACER_NAME = 'weflayr.instrumentation.openai_responses';
79
+ // The output block types that make a completed response a tool call rather than
80
+ // a plain answer, mirroring the Python instrumentor's finish-reason derivation.
81
+ const TOOL_CALL_BLOCK_TYPES = [
82
+ 'function_call',
83
+ 'file_search_call',
84
+ 'web_search_call',
85
+ 'computer_call',
86
+ 'code_interpreter_call',
87
+ ];
88
+ function isJsonObject(value) {
89
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
90
+ }
91
+ /** A tool call's arguments as an object, or the raw string when it is not JSON. */
92
+ function parseArguments(rawArguments) {
93
+ if (rawArguments == null)
94
+ return null;
95
+ if (isJsonObject(rawArguments))
96
+ return rawArguments;
97
+ try {
98
+ return JSON.parse(String(rawArguments));
99
+ }
100
+ catch {
101
+ return rawArguments;
102
+ }
103
+ }
104
+ /**
105
+ * One `input` content block as an OTel message part.
106
+ *
107
+ * Example: `{type: "input_text", text: "hi"}` -> `{type: "text", content: "hi"}`;
108
+ * `{type: "input_file", filename: "a.pdf", file_data: "data:application/pdf;base64,..."}`
109
+ * -> `{type: "blob", mime_type: "application/pdf"}`.
110
+ */
111
+ function buildInputPart(block) {
112
+ if (!isJsonObject(block)) {
113
+ return { type: 'text', content: String(block) };
114
+ }
115
+ const blockType = String(block.type ?? '');
116
+ if (blockType === 'text' || blockType === 'input_text' || blockType === 'output_text') {
117
+ return { type: 'text', content: block.text ?? '' };
118
+ }
119
+ if (blockType === 'image' || blockType === 'input_image' || blockType === 'output_image') {
120
+ if (block.image_url) {
121
+ return { type: 'uri', modality: 'image', uri: block.image_url };
122
+ }
123
+ return { type: 'file', modality: 'image', file_id: block.file_id ?? null };
124
+ }
125
+ if (blockType === 'file' || blockType === 'input_file' || blockType === 'output_file') {
126
+ const mimeType = (0, get_data_uri_mime_type_1.getDataUriMimeType)(String(block.file_data ?? ''));
127
+ if (mimeType !== null) {
128
+ return { type: 'blob', mime_type: mimeType };
129
+ }
130
+ return {
131
+ type: 'file',
132
+ file_id: block.file_id ?? null,
133
+ filename: block.filename ?? null,
134
+ };
135
+ }
136
+ // An unrecognized block keeps its own type, with the block itself as content.
137
+ return { type: blockType || 'unknown', content: block };
138
+ }
139
+ /** One `input` item as an OTel message, or null for an item type with no message. */
140
+ function buildInputMessage(item) {
141
+ if (!isJsonObject(item)) {
142
+ return { role: 'user', parts: [{ type: 'text', content: String(item) }] };
143
+ }
144
+ const itemType = String(item.type ?? 'message');
145
+ if (itemType === 'message') {
146
+ const content = item.content;
147
+ const parts = Array.isArray(content)
148
+ ? content.map(buildInputPart)
149
+ : [{ type: 'text', content: content ?? '' }];
150
+ return { role: item.role ?? 'user', parts };
151
+ }
152
+ if (itemType === 'function_call') {
153
+ return {
154
+ role: 'assistant',
155
+ parts: [
156
+ {
157
+ type: 'tool_call',
158
+ name: item.name ?? null,
159
+ id: item.id ?? item.call_id ?? null,
160
+ arguments: parseArguments(item.arguments),
161
+ },
162
+ ],
163
+ };
164
+ }
165
+ if (itemType === 'function_call_output') {
166
+ return {
167
+ role: 'tool',
168
+ parts: [
169
+ { type: 'tool_call_response', id: item.call_id ?? null, response: item.output ?? '' },
170
+ ],
171
+ };
172
+ }
173
+ return null;
174
+ }
175
+ /** The request's `instructions` + `input` as OTel input messages. */
176
+ function buildResponsesInputMessages(params) {
177
+ const messages = [];
178
+ if (params.instructions) {
179
+ messages.push({
180
+ role: 'system',
181
+ parts: [{ type: 'text', content: params.instructions }],
182
+ });
183
+ }
184
+ const input = params.input;
185
+ if (typeof input === 'string') {
186
+ messages.push({ role: 'user', parts: [{ type: 'text', content: input }] });
187
+ }
188
+ else if (Array.isArray(input)) {
189
+ for (const item of input) {
190
+ const message = buildInputMessage(item);
191
+ if (message)
192
+ messages.push(message);
193
+ }
194
+ }
195
+ return messages;
196
+ }
197
+ /** The request's function tools, in the flat shape the Responses API takes. */
198
+ function buildResponsesToolDefinitions(params) {
199
+ const tools = Array.isArray(params.tools) ? params.tools : [];
200
+ const definitions = [];
201
+ for (const tool of tools) {
202
+ if (!isJsonObject(tool) || tool.type !== 'function')
203
+ continue;
204
+ const definition = { type: 'function' };
205
+ if (tool.name)
206
+ definition.name = tool.name;
207
+ if (tool.description)
208
+ definition.description = tool.description;
209
+ if (tool.parameters)
210
+ definition.parameters = tool.parameters;
211
+ definitions.push(definition);
212
+ }
213
+ return definitions;
214
+ }
215
+ /** The response's `status` as an OTel finish reason. */
216
+ function deriveFinishReason(response) {
217
+ const status = response.status;
218
+ if (!status)
219
+ return '';
220
+ if (status === 'completed') {
221
+ const output = Array.isArray(response.output) ? response.output : [];
222
+ const callsATool = output.some((block) => isJsonObject(block) && TOOL_CALL_BLOCK_TYPES.includes(String(block.type)));
223
+ return callsATool ? 'tool_call' : 'stop';
224
+ }
225
+ if (status === 'incomplete') {
226
+ const reason = isJsonObject(response.incomplete_details)
227
+ ? response.incomplete_details.reason
228
+ : null;
229
+ return reason === 'content_filter' ? 'content_filter' : 'length';
230
+ }
231
+ if (status === 'failed' || status === 'cancelled')
232
+ return 'error';
233
+ return '';
234
+ }
235
+ /** The answer's text, from `output_text` or by joining the output text blocks. */
236
+ function readOutputText(response) {
237
+ if (typeof response.output_text === 'string')
238
+ return response.output_text;
239
+ const output = Array.isArray(response.output) ? response.output : [];
240
+ const texts = [];
241
+ for (const block of output) {
242
+ if (!isJsonObject(block) || block.type !== 'message')
243
+ continue;
244
+ const content = Array.isArray(block.content) ? block.content : [];
245
+ for (const part of content) {
246
+ if (isJsonObject(part) && part.type === 'output_text' && typeof part.text === 'string') {
247
+ texts.push(part.text);
248
+ }
249
+ }
250
+ }
251
+ return texts.join('');
252
+ }
253
+ /** The response's text, tool calls and reasoning summaries as one OTel message. */
254
+ function buildResponsesOutputMessages(response) {
255
+ const parts = [];
256
+ const outputText = readOutputText(response);
257
+ if (outputText) {
258
+ parts.push({ type: 'text', content: outputText });
259
+ }
260
+ const output = Array.isArray(response.output) ? response.output : [];
261
+ for (const block of output) {
262
+ if (!isJsonObject(block))
263
+ continue;
264
+ if (block.type === 'function_call') {
265
+ parts.push({
266
+ type: 'tool_call',
267
+ name: block.name ?? null,
268
+ id: block.id ?? null,
269
+ arguments: parseArguments(block.arguments),
270
+ });
271
+ }
272
+ else if (block.type === 'reasoning') {
273
+ const summary = block.summary;
274
+ if (summary != null && !(Array.isArray(summary) && summary.length === 0)) {
275
+ parts.push({
276
+ type: 'reasoning',
277
+ content: typeof summary === 'string' ? summary : JSON.stringify(summary),
278
+ });
279
+ }
280
+ }
281
+ }
282
+ if (!parts.length)
283
+ return [];
284
+ return [{ role: 'assistant', parts, finish_reason: deriveFinishReason(response) }];
285
+ }
286
+ function setUsageAttributes(span, usage) {
287
+ if (typeof usage.input_tokens === 'number') {
288
+ span.setAttribute(INPUT_TOKENS_ATTR, usage.input_tokens);
289
+ }
290
+ if (typeof usage.output_tokens === 'number') {
291
+ span.setAttribute(OUTPUT_TOKENS_ATTR, usage.output_tokens);
292
+ }
293
+ if (typeof usage.total_tokens === 'number') {
294
+ span.setAttribute(TOTAL_TOKENS_ATTR, usage.total_tokens);
295
+ }
296
+ const inputDetails = isJsonObject(usage.input_tokens_details) ? usage.input_tokens_details : {};
297
+ if (inputDetails.cached_tokens) {
298
+ span.setAttribute(CACHE_READ_INPUT_TOKENS_ATTR, inputDetails.cached_tokens);
299
+ }
300
+ if (inputDetails.cache_write_tokens) {
301
+ span.setAttribute(CACHE_CREATION_INPUT_TOKENS_ATTR, inputDetails.cache_write_tokens);
302
+ }
303
+ const outputDetails = isJsonObject(usage.output_tokens_details)
304
+ ? usage.output_tokens_details
305
+ : {};
306
+ if (outputDetails.reasoning_tokens) {
307
+ span.setAttribute(REASONING_TOKENS_ATTR, outputDetails.reasoning_tokens);
308
+ }
309
+ }
310
+ /** How many document searches a Responses answer is billed for, from xAI's
311
+ * `usage.server_side_tool_usage_details.document_search_calls`, or null when the
312
+ * usage reports none, e.g. `{server_side_tool_usage_details:
313
+ * {document_search_calls: 1}}` -> 1. */
314
+ function readBilledDocumentSearchCount(usage) {
315
+ const count = usage?.server_side_tool_usage_details?.document_search_calls;
316
+ return typeof count === 'number' && count > 0 ? count : null;
317
+ }
318
+ /** Stamp everything the response carries: ids, usage, reasoning, messages. */
319
+ function setResponseAttributes(span, response, sendPrompts) {
320
+ if (!isJsonObject(response) || !span.isRecording())
321
+ return;
322
+ if (response.id)
323
+ span.setAttribute(RESPONSE_ID_ATTR, String(response.id));
324
+ if (response.model)
325
+ span.setAttribute(RESPONSE_MODEL_ATTR, String(response.model));
326
+ if (response.service_tier) {
327
+ span.setAttribute(RESPONSE_SERVICE_TIER_ATTR, String(response.service_tier));
328
+ }
329
+ if (isJsonObject(response.usage))
330
+ setUsageAttributes(span, response.usage);
331
+ const documentSearchCount = readBilledDocumentSearchCount(response.usage);
332
+ if (documentSearchCount !== null) {
333
+ span.setAttribute(DOCUMENT_SEARCH_REQUESTS_ATTR, documentSearchCount);
334
+ }
335
+ if (isJsonObject(response.reasoning) && response.reasoning.effort) {
336
+ span.setAttribute(RESPONSE_REASONING_EFFORT_ATTR, String(response.reasoning.effort));
337
+ }
338
+ const finishReason = deriveFinishReason(response);
339
+ if (finishReason)
340
+ span.setAttribute(FINISH_REASONS_ATTR, [finishReason]);
341
+ // Stamped whatever `sendPrompts` is: the span processor strips the searches
342
+ // and the citations as content, and keeps the billed count, which is not.
343
+ (0, get_web_search_telemetry_1.setResponsesWebSearchAnswerAttributes)(span, response);
344
+ if (sendPrompts) {
345
+ span.setAttribute(OUTPUT_MESSAGES_ATTR, JSON.stringify(buildResponsesOutputMessages(response)));
346
+ }
347
+ }
348
+ /** Stamp what only the request knows: its messages, tools and reasoning ask. */
349
+ function setRequestAttributes(span, params, sendPrompts) {
350
+ if (!isJsonObject(params) || !span.isRecording())
351
+ return;
352
+ if (isJsonObject(params.reasoning)) {
353
+ if (params.reasoning.effort) {
354
+ span.setAttribute(REQUEST_REASONING_EFFORT_ATTR, String(params.reasoning.effort));
355
+ }
356
+ if (params.reasoning.summary) {
357
+ span.setAttribute(REQUEST_REASONING_SUMMARY_ATTR, String(params.reasoning.summary));
358
+ }
359
+ }
360
+ (0, get_web_search_telemetry_1.setResponsesWebSearchRequestAttributes)(span, params);
361
+ if (!sendPrompts)
362
+ return;
363
+ span.setAttribute(INPUT_MESSAGES_ATTR, JSON.stringify(buildResponsesInputMessages(params)));
364
+ const definitions = buildResponsesToolDefinitions(params);
365
+ if (definitions.length) {
366
+ span.setAttribute(TOOL_DEFINITIONS_ATTR, JSON.stringify(definitions));
367
+ }
368
+ }
369
+ /**
370
+ * The response a streamed call built, assembled from its events.
371
+ *
372
+ * Every Responses stream event carries the whole response object under
373
+ * `response` on its lifecycle events (`response.created`,
374
+ * `response.completed`, `response.incomplete`, `response.failed`), so the
375
+ * terminal one already holds the output blocks and the usage. Keeping the last
376
+ * one seen is enough: no per-delta accumulation.
377
+ */
378
+ function readEventResponse(event) {
379
+ if (!isJsonObject(event))
380
+ return null;
381
+ return isJsonObject(event.response) ? event.response : null;
382
+ }
383
+ /**
384
+ * Upstream's own content-capture gate, so a span carries message content on
385
+ * exactly the calls its spans would. Falls through to capturing when the gate
386
+ * cannot be read: the span processor strips content when capture is off, so
387
+ * this can never leak.
388
+ */
389
+ function shouldSendPrompts(instrumentation) {
390
+ try {
391
+ if (typeof instrumentation._shouldSendPrompts === 'function') {
392
+ return instrumentation._shouldSendPrompts();
393
+ }
394
+ }
395
+ catch {
396
+ // An instrumentor whose gate threw: treat content as capturable.
397
+ }
398
+ return true;
399
+ }
400
+ /**
401
+ * Wrap a Responses stream's iterator so its events pass through untouched while
402
+ * the last response object seen is stamped on the span, which ends with the
403
+ * iteration.
404
+ *
405
+ * Mutates `stream` in place and returns it, so the caller still hands the app
406
+ * the exact object the openai client returned.
407
+ */
408
+ function recordResponsesStreamEvents(stream, span, sendPrompts) {
409
+ if (!stream || typeof stream[Symbol.asyncIterator] !== 'function') {
410
+ span.end();
411
+ return stream;
412
+ }
413
+ const originalIterator = stream[Symbol.asyncIterator].bind(stream);
414
+ stream[Symbol.asyncIterator] = async function* recordEvents() {
415
+ let lastResponse = null;
416
+ try {
417
+ for await (const event of originalIterator()) {
418
+ const response = readEventResponse(event);
419
+ if (response)
420
+ lastResponse = response;
421
+ yield event;
422
+ }
423
+ }
424
+ catch (error) {
425
+ span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error?.message ?? String(error) });
426
+ span.recordException(error);
427
+ throw error;
428
+ }
429
+ finally {
430
+ try {
431
+ if (lastResponse)
432
+ setResponseAttributes(span, lastResponse, sendPrompts);
433
+ }
434
+ catch {
435
+ // Never let an enrichment failure swallow the caller's stream.
436
+ }
437
+ span.end();
438
+ }
439
+ };
440
+ return stream;
441
+ }
442
+ function traceStreamedCall({ instrumentation, openaiCreate, client, params, rest, }) {
443
+ const span = startStreamedSpan(instrumentation, params, client);
444
+ if (!span)
445
+ return openaiCreate.call(client, params, ...rest);
446
+ const sendPrompts = shouldSendPrompts(instrumentation);
447
+ const streamPromise = api_1.context.with(api_1.trace.setSpan(api_1.context.active(), span), () => openaiCreate.call(client, params, ...rest));
448
+ // Registered before the caller's own handler, so the iterator is already
449
+ // wrapped by the time they iterate. The returned promise is upstream's own,
450
+ // so the caller still gets the exact object the openai client returns.
451
+ Promise.resolve(streamPromise).then((stream) => {
452
+ try {
453
+ recordResponsesStreamEvents(stream, span, sendPrompts);
454
+ }
455
+ catch {
456
+ // A telemetry failure must never reject this handler's promise: nothing
457
+ // handles it, and Node terminates the app on an unhandled rejection.
458
+ span.end();
459
+ }
460
+ }, (error) => {
461
+ span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error?.message ?? String(error) });
462
+ span.recordException(error);
463
+ span.end();
464
+ });
465
+ return streamPromise;
466
+ }
467
+ // Reuse upstream's request-attribute logic (and, through the patch above, our
468
+ // own) so a streamed span carries exactly what a non-streamed one carries.
469
+ function startStreamedSpan(instrumentation, params, client) {
470
+ try {
471
+ if (typeof instrumentation.startSpan === 'function') {
472
+ return instrumentation.startSpan({ type: RESPONSES_TYPE, params, client });
473
+ }
474
+ const span = api_1.trace.getTracer(TRACER_NAME).startSpan(`chat ${params && params.model}`);
475
+ setRequestAttributes(span, params, shouldSendPrompts(instrumentation));
476
+ return span;
477
+ }
478
+ catch {
479
+ return null;
480
+ }
481
+ }
482
+ class OpenAIResponsesInstrumentor {
483
+ // [owner, methodName, originalFunction] for uninstrument().
484
+ _patched = [];
485
+ // No-op (and never throws) if the instrumentor's internals or the openai
486
+ // client have changed shape: the worst case is the upstream telemetry,
487
+ // never a crash.
488
+ instrument(instrumentation) {
489
+ this._patchRequestSide(instrumentation);
490
+ this._patchResponseSide(instrumentation);
491
+ this._takeOverStreamedCalls(instrumentation);
492
+ }
493
+ uninstrument() {
494
+ for (const [owner, methodName, original] of this._patched) {
495
+ owner[methodName] = original;
496
+ }
497
+ this._patched = [];
498
+ }
499
+ // Rebuild the input messages and tool definitions upstream stamps as one
500
+ // stringified text part. Runs for a non-streamed call (upstream calls
501
+ // startSpan) and for a streamed one (the takeover below calls it).
502
+ _patchRequestSide(instrumentation) {
503
+ const original = instrumentation && instrumentation.startSpan;
504
+ if (typeof original !== 'function')
505
+ return;
506
+ instrumentation.startSpan = function startSpanWithResponsesShape(args) {
507
+ const span = original.call(this, args);
508
+ try {
509
+ if (args && args.type === RESPONSES_TYPE) {
510
+ setRequestAttributes(span, args.params, shouldSendPrompts(instrumentation));
511
+ }
512
+ }
513
+ catch {
514
+ // Never break span creation over an enrichment failure.
515
+ }
516
+ return span;
517
+ };
518
+ this._patched.push([instrumentation, 'startSpan', original]);
519
+ }
520
+ // Own the response side of a Responses call and end its span here. Upstream's
521
+ // handler stamps the same two attributes from the answer's text alone, so
522
+ // delegating to it would replace a tool call with an empty text part and
523
+ // "stop", and it ends the span immediately after, leaving nowhere to repair
524
+ // them. Every other call type is handed straight to it.
525
+ _patchResponseSide(instrumentation) {
526
+ const original = instrumentation && instrumentation._endSpan;
527
+ if (typeof original !== 'function')
528
+ return;
529
+ instrumentation._endSpan = function endSpanWithResponsesShape(args) {
530
+ if (!args || args.type !== RESPONSES_TYPE || !args.span) {
531
+ return original.call(this, args);
532
+ }
533
+ try {
534
+ setResponseAttributes(args.span, args.result, shouldSendPrompts(instrumentation));
535
+ }
536
+ catch {
537
+ // Never break span finalization over an enrichment failure.
538
+ }
539
+ args.span.end();
540
+ };
541
+ this._patched.push([instrumentation, '_endSpan', original]);
542
+ }
543
+ /**
544
+ * Own the streamed branch upstream leaves uninstrumented.
545
+ *
546
+ * Our wrapper sits outside upstream's, so a non-streamed call is delegated
547
+ * straight to it. A streamed call instead calls the function upstream wrapped
548
+ * (shimmer's `__original`), so only one span is ever created, and ends that
549
+ * span once the stream's events are consumed.
550
+ */
551
+ _takeOverStreamedCalls(instrumentation) {
552
+ let Responses;
553
+ try {
554
+ // The package entry, not the `resources/responses/responses` submodule:
555
+ // upstream's hook is registered on the package name, so requiring it here
556
+ // is what runs its patch when the app has not imported `openai` yet. Were
557
+ // we to patch first, our wrapper would sit under upstream's instead of
558
+ // outside it, and a streamed call would get one span from each.
559
+ Responses = require('openai').OpenAI?.Responses;
560
+ }
561
+ catch {
562
+ // No openai client installed: nothing to take over.
563
+ return;
564
+ }
565
+ const owner = Responses && Responses.prototype;
566
+ const upstreamWrapper = owner && owner.create;
567
+ if (typeof upstreamWrapper !== 'function')
568
+ return;
569
+ // Set by shimmer when upstream wrapped `create`; absent when it did not, in
570
+ // which case its wrapper IS the original and there is no span to avoid.
571
+ const openaiCreate = upstreamWrapper.__original || upstreamWrapper;
572
+ owner.create = function createWithStreamedResponses(params, ...rest) {
573
+ if (!params || !params.stream) {
574
+ return upstreamWrapper.call(this, params, ...rest);
575
+ }
576
+ return traceStreamedCall({
577
+ instrumentation,
578
+ openaiCreate,
579
+ client: this,
580
+ params,
581
+ rest,
582
+ });
583
+ };
584
+ this._patched.push([owner, 'create', upstreamWrapper]);
585
+ }
586
+ }
587
+ exports.OpenAIResponsesInstrumentor = OpenAIResponsesInstrumentor;