weflayr 0.26.1 → 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.
- package/dist/package.json +8 -5
- package/dist/src/auto-instrument.d.ts +4 -3
- package/dist/src/auto-instrument.js +18 -17
- package/dist/src/get-data-uri-mime-type.d.ts +17 -0
- package/dist/src/get-data-uri-mime-type.js +25 -0
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.js +5 -1
- package/dist/src/instrumentation/anthropic/get_streaming_accumulation_telemetry.d.ts +3 -1
- package/dist/src/instrumentation/anthropic/get_streaming_accumulation_telemetry.js +6 -3
- package/dist/src/instrumentation/anthropic/get_vertex_billing_telemetry.d.ts +32 -0
- package/dist/src/instrumentation/anthropic/get_vertex_billing_telemetry.js +100 -0
- package/dist/src/instrumentation/anthropic/get_web_search_telemetry.d.ts +43 -0
- package/dist/src/instrumentation/anthropic/get_web_search_telemetry.js +147 -0
- package/dist/src/instrumentation/anthropic/set_telemetry_attributes.d.ts +5 -2
- package/dist/src/instrumentation/anthropic/set_telemetry_attributes.js +17 -4
- package/dist/src/instrumentation/bedrock/get_cache_config_telemetry.d.ts +13 -1
- package/dist/src/instrumentation/bedrock/get_cache_config_telemetry.js +56 -1
- package/dist/src/instrumentation/bedrock/get_cache_telemetry.js +8 -5
- package/dist/src/instrumentation/bedrock/get_input_message_content_telemetry.js +33 -5
- package/dist/src/instrumentation/bedrock/get_model_id_telemetry.d.ts +1 -1
- package/dist/src/instrumentation/bedrock/get_model_id_telemetry.js +1 -1
- package/dist/src/instrumentation/bedrock/get_openai_chat_format_telemetry.d.ts +45 -0
- package/dist/src/instrumentation/bedrock/get_openai_chat_format_telemetry.js +251 -0
- package/dist/src/instrumentation/bedrock/get_streaming_output_message_telemetry.d.ts +6 -5
- package/dist/src/instrumentation/bedrock/get_streaming_output_message_telemetry.js +15 -12
- package/dist/src/instrumentation/bedrock/get_tool_definitions_telemetry.d.ts +2 -2
- package/dist/src/instrumentation/bedrock/get_tool_definitions_telemetry.js +12 -4
- package/dist/src/instrumentation/bedrock/set_telemetry_attributes.d.ts +4 -2
- package/dist/src/instrumentation/bedrock/set_telemetry_attributes.js +10 -3
- package/dist/src/instrumentation/cohere/get_embeddings_telemetry.js +4 -6
- package/dist/src/instrumentation/cohere/get_input_message_content_telemetry.d.ts +23 -0
- package/dist/src/instrumentation/cohere/get_input_message_content_telemetry.js +105 -0
- package/dist/src/instrumentation/cohere/get_v2_chat_telemetry.d.ts +18 -0
- package/dist/src/instrumentation/cohere/get_v2_chat_telemetry.js +186 -0
- package/dist/src/instrumentation/cohere/set_telemetry_attributes.d.ts +3 -1
- package/dist/src/instrumentation/cohere/set_telemetry_attributes.js +5 -1
- package/dist/src/instrumentation/google/get_embeddings_telemetry.d.ts +8 -4
- package/dist/src/instrumentation/google/get_embeddings_telemetry.js +68 -35
- package/dist/src/instrumentation/google/get_provider_name.d.ts +2 -1
- package/dist/src/instrumentation/google/get_provider_name.js +2 -1
- package/dist/src/instrumentation/google/get_web_search_telemetry.d.ts +38 -0
- package/dist/src/instrumentation/google/get_web_search_telemetry.js +125 -0
- package/dist/src/instrumentation/google/set_telemetry_attributes.d.ts +2 -2
- package/dist/src/instrumentation/google/set_telemetry_attributes.js +6 -2
- package/dist/src/instrumentation/mistral/full_instrumentation.d.ts +60 -0
- package/dist/src/instrumentation/mistral/full_instrumentation.js +364 -0
- package/dist/src/instrumentation/mistral/get_input_message_content_telemetry.d.ts +35 -0
- package/dist/src/instrumentation/mistral/get_input_message_content_telemetry.js +113 -0
- package/dist/src/instrumentation/mistral/get_structured_output_schema_telemetry.d.ts +13 -0
- package/dist/src/instrumentation/mistral/get_structured_output_schema_telemetry.js +24 -0
- package/dist/src/instrumentation/mistral/get_vertex_telemetry.d.ts +54 -0
- package/dist/src/instrumentation/mistral/get_vertex_telemetry.js +153 -0
- package/dist/src/instrumentation/openai/get_bedrock_mantle_telemetry.d.ts +10 -0
- package/dist/src/instrumentation/openai/get_bedrock_mantle_telemetry.js +33 -0
- package/dist/src/instrumentation/openai/get_cache_and_reasoning_telemetry.d.ts +13 -6
- package/dist/src/instrumentation/openai/get_cache_and_reasoning_telemetry.js +17 -7
- package/dist/src/instrumentation/openai/get_cache_config_telemetry.d.ts +21 -4
- package/dist/src/instrumentation/openai/get_cache_config_telemetry.js +50 -11
- package/dist/src/instrumentation/openai/get_embeddings_telemetry.js +14 -6
- package/dist/src/instrumentation/openai/get_file_part_mime_type_telemetry.d.ts +26 -0
- package/dist/src/instrumentation/openai/get_file_part_mime_type_telemetry.js +100 -0
- package/dist/src/instrumentation/openai/get_openai_compatible_provider_telemetry.d.ts +48 -0
- package/dist/src/instrumentation/openai/get_openai_compatible_provider_telemetry.js +123 -0
- package/dist/src/instrumentation/openai/get_reasoning_content_telemetry.d.ts +28 -0
- package/dist/src/instrumentation/openai/get_reasoning_content_telemetry.js +130 -0
- package/dist/src/instrumentation/openai/get_responses_telemetry.d.ts +89 -0
- package/dist/src/instrumentation/openai/get_responses_telemetry.js +587 -0
- package/dist/src/instrumentation/openai/get_service_tier_telemetry.d.ts +20 -0
- package/dist/src/instrumentation/openai/get_service_tier_telemetry.js +84 -0
- package/dist/src/instrumentation/openai/get_structured_output_schema_telemetry.d.ts +7 -5
- package/dist/src/instrumentation/openai/get_structured_output_schema_telemetry.js +15 -8
- package/dist/src/instrumentation/openai/get_vertex_openai_compatible_telemetry.d.ts +23 -0
- package/dist/src/instrumentation/openai/get_vertex_openai_compatible_telemetry.js +69 -0
- package/dist/src/instrumentation/openai/get_web_search_telemetry.d.ts +74 -0
- package/dist/src/instrumentation/openai/get_web_search_telemetry.js +259 -0
- package/dist/src/instrumentation/openai/set_telemetry_attributes.d.ts +13 -1
- package/dist/src/instrumentation/openai/set_telemetry_attributes.js +33 -2
- package/dist/src/instrumentation/shared/get_structured_output_schema_telemetry.d.ts +1 -1
- package/dist/src/instrumentation/shared/get_structured_output_schema_telemetry.js +1 -1
- package/dist/src/instrumentation/shared/is_vertex_base_url.d.ts +17 -0
- package/dist/src/instrumentation/shared/is_vertex_base_url.js +40 -0
- package/dist/src/otel/propagation.d.ts +37 -10
- package/dist/src/otel/propagation.js +62 -22
- package/dist/src/otel/span-filter.d.ts +2 -0
- package/dist/src/otel/span-filter.js +10 -1
- package/dist/src/otel/span-processor.d.ts +1 -2
- package/dist/src/otel/span-processor.js +17 -20
- package/dist/src/otel/strip-media-payloads.d.ts +26 -0
- package/dist/src/otel/strip-media-payloads.js +135 -0
- package/dist/src/propagate-metadata-to-gateway.d.ts +37 -0
- package/dist/src/propagate-metadata-to-gateway.js +213 -0
- package/package.json +8 -5
- package/dist/src/instrumentation/vercel/full_instrumentation.d.ts +0 -3
- package/dist/src/instrumentation/vercel/full_instrumentation.js +0 -54
- package/dist/src/instrumentation/vercel/get_cache_creation_telemetry.d.ts +0 -22
- package/dist/src/instrumentation/vercel/get_cache_creation_telemetry.js +0 -66
- package/dist/src/instrumentation/vercel/get_structured_output_schema_telemetry.d.ts +0 -24
- package/dist/src/instrumentation/vercel/get_structured_output_schema_telemetry.js +0 -84
- package/dist/src/otel/vercel-ai-sdk-span-filter.d.ts +0 -12
- 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;
|