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,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service tier capture for the OpenAI chat and Responses paths.
|
|
3
|
+
*
|
|
4
|
+
* OpenLLMetry's Python instrumentor stamps the lane OpenAI served the call in
|
|
5
|
+
* (`service_tier`: "default", "flex", "priority", ...) as
|
|
6
|
+
* `openai.response.service_tier`; `@traceloop/instrumentation-openai` (Node)
|
|
7
|
+
* records nothing, so a flex or priority call would be priced at the standard
|
|
8
|
+
* rate. This stamps the same attribute, read off the complete response or off
|
|
9
|
+
* the streamed chunks (each one carries it), using the same `_endSpan` /
|
|
10
|
+
* `_streamingWrapPromise` hooks as `OpenAIReasoningContentInstrumentor`.
|
|
11
|
+
*/
|
|
12
|
+
export declare class OpenAIServiceTierInstrumentor {
|
|
13
|
+
private _patched;
|
|
14
|
+
instrument(instrumentation: any): void;
|
|
15
|
+
/** Remember the service tier the streamed chunks carry. */
|
|
16
|
+
private _patchStreamedServiceTier;
|
|
17
|
+
/** Stamp the service tier before the span ends. */
|
|
18
|
+
private _patchEndSpan;
|
|
19
|
+
uninstrument(): void;
|
|
20
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Service tier capture for the OpenAI chat and Responses paths.
|
|
4
|
+
*
|
|
5
|
+
* OpenLLMetry's Python instrumentor stamps the lane OpenAI served the call in
|
|
6
|
+
* (`service_tier`: "default", "flex", "priority", ...) as
|
|
7
|
+
* `openai.response.service_tier`; `@traceloop/instrumentation-openai` (Node)
|
|
8
|
+
* records nothing, so a flex or priority call would be priced at the standard
|
|
9
|
+
* rate. This stamps the same attribute, read off the complete response or off
|
|
10
|
+
* the streamed chunks (each one carries it), using the same `_endSpan` /
|
|
11
|
+
* `_streamingWrapPromise` hooks as `OpenAIReasoningContentInstrumentor`.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.OpenAIServiceTierInstrumentor = void 0;
|
|
15
|
+
const SERVICE_TIER_ATTR = 'openai.response.service_tier';
|
|
16
|
+
// Service tier seen in the stream, per open span: chunks arrive while the
|
|
17
|
+
// stream is consumed, and the span is ended once it is done.
|
|
18
|
+
const _streamedServiceTierBySpan = new WeakMap();
|
|
19
|
+
function _readServiceTier(responseOrChunk) {
|
|
20
|
+
const serviceTier = responseOrChunk?.service_tier;
|
|
21
|
+
return typeof serviceTier === 'string' && serviceTier ? serviceTier : undefined;
|
|
22
|
+
}
|
|
23
|
+
class OpenAIServiceTierInstrumentor {
|
|
24
|
+
// [owner, methodName, originalFunction] for uninstrument().
|
|
25
|
+
_patched = [];
|
|
26
|
+
// No-op (and never throws) if the instrumentor's internals have changed shape:
|
|
27
|
+
// the worst case is a call priced at the standard rate, never a crash.
|
|
28
|
+
instrument(instrumentation) {
|
|
29
|
+
if (!instrumentation)
|
|
30
|
+
return;
|
|
31
|
+
this._patchStreamedServiceTier(instrumentation);
|
|
32
|
+
this._patchEndSpan(instrumentation);
|
|
33
|
+
}
|
|
34
|
+
/** Remember the service tier the streamed chunks carry. */
|
|
35
|
+
_patchStreamedServiceTier(instrumentation) {
|
|
36
|
+
const original = instrumentation._streamingWrapPromise;
|
|
37
|
+
if (typeof original !== 'function')
|
|
38
|
+
return;
|
|
39
|
+
instrumentation._streamingWrapPromise = function streamingWrapWithServiceTier(streamArgs) {
|
|
40
|
+
const span = streamArgs && streamArgs.span;
|
|
41
|
+
const wrappedStream = original.call(this, streamArgs);
|
|
42
|
+
if (!span)
|
|
43
|
+
return wrappedStream;
|
|
44
|
+
return (async function* recordServiceTier() {
|
|
45
|
+
for await (const chunk of wrappedStream) {
|
|
46
|
+
const serviceTier = _readServiceTier(chunk);
|
|
47
|
+
if (serviceTier)
|
|
48
|
+
_streamedServiceTierBySpan.set(span, serviceTier);
|
|
49
|
+
yield chunk;
|
|
50
|
+
}
|
|
51
|
+
})();
|
|
52
|
+
};
|
|
53
|
+
this._patched.push([instrumentation, '_streamingWrapPromise', original]);
|
|
54
|
+
}
|
|
55
|
+
/** Stamp the service tier before the span ends. */
|
|
56
|
+
_patchEndSpan(instrumentation) {
|
|
57
|
+
const original = instrumentation._endSpan;
|
|
58
|
+
if (typeof original !== 'function')
|
|
59
|
+
return;
|
|
60
|
+
instrumentation._endSpan = function endSpanWithServiceTier(args) {
|
|
61
|
+
const span = args && args.span;
|
|
62
|
+
if (span) {
|
|
63
|
+
try {
|
|
64
|
+
const serviceTier = _readServiceTier(args.result) ?? _streamedServiceTierBySpan.get(span);
|
|
65
|
+
if (serviceTier)
|
|
66
|
+
span.setAttribute(SERVICE_TIER_ATTR, serviceTier);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Telemetry must never break the app's LLM call.
|
|
70
|
+
}
|
|
71
|
+
_streamedServiceTierBySpan.delete(span);
|
|
72
|
+
}
|
|
73
|
+
return original.call(this, args);
|
|
74
|
+
};
|
|
75
|
+
this._patched.push([instrumentation, '_endSpan', original]);
|
|
76
|
+
}
|
|
77
|
+
uninstrument() {
|
|
78
|
+
for (const [owner, methodName, original] of this._patched) {
|
|
79
|
+
owner[methodName] = original;
|
|
80
|
+
}
|
|
81
|
+
this._patched = [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
exports.OpenAIServiceTierInstrumentor = OpenAIServiceTierInstrumentor;
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
* Structured-output schema location for OpenAI requests.
|
|
3
3
|
*
|
|
4
4
|
* OpenAI pins an answer to a JSON schema with
|
|
5
|
-
* `response_format: { type: 'json_schema', json_schema: { schema } }
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* `response_format: { type: 'json_schema', json_schema: { schema } }` on the
|
|
6
|
+
* chat endpoint, and with `text: { format: { type: 'json_schema', schema } }` on
|
|
7
|
+
* the Responses one. The traceloop instrumentor records the request's model and
|
|
8
|
+
* sampling params but never the schema - which OpenAI bills as input.
|
|
8
9
|
*
|
|
9
|
-
* (The Python OpenLLMetry instrumentor stamps
|
|
10
|
-
*
|
|
10
|
+
* (The Python OpenLLMetry instrumentor stamps the chat endpoint's field
|
|
11
|
+
* natively, so the Python SDK reads only the Responses one, in
|
|
12
|
+
* `get_responses_telemetry.py` - see known_divergences.md.)
|
|
11
13
|
*/
|
|
12
14
|
export declare function readStructuredOutputSchema(params: any): unknown;
|
|
@@ -3,18 +3,25 @@
|
|
|
3
3
|
* Structured-output schema location for OpenAI requests.
|
|
4
4
|
*
|
|
5
5
|
* OpenAI pins an answer to a JSON schema with
|
|
6
|
-
* `response_format: { type: 'json_schema', json_schema: { schema } }
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* `response_format: { type: 'json_schema', json_schema: { schema } }` on the
|
|
7
|
+
* chat endpoint, and with `text: { format: { type: 'json_schema', schema } }` on
|
|
8
|
+
* the Responses one. The traceloop instrumentor records the request's model and
|
|
9
|
+
* sampling params but never the schema - which OpenAI bills as input.
|
|
9
10
|
*
|
|
10
|
-
* (The Python OpenLLMetry instrumentor stamps
|
|
11
|
-
*
|
|
11
|
+
* (The Python OpenLLMetry instrumentor stamps the chat endpoint's field
|
|
12
|
+
* natively, so the Python SDK reads only the Responses one, in
|
|
13
|
+
* `get_responses_telemetry.py` - see known_divergences.md.)
|
|
12
14
|
*/
|
|
13
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
16
|
exports.readStructuredOutputSchema = readStructuredOutputSchema;
|
|
15
17
|
function readStructuredOutputSchema(params) {
|
|
16
18
|
const responseFormat = params && params.response_format;
|
|
17
|
-
if (
|
|
18
|
-
return
|
|
19
|
-
|
|
19
|
+
if (responseFormat && responseFormat.type === 'json_schema') {
|
|
20
|
+
return responseFormat.json_schema && responseFormat.json_schema.schema;
|
|
21
|
+
}
|
|
22
|
+
const textFormat = params && params.text && params.text.format;
|
|
23
|
+
if (textFormat && textFormat.type === 'json_schema') {
|
|
24
|
+
return textFormat.schema;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
20
27
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing attribution for the openai client pointed at Vertex AI's
|
|
3
|
+
* OpenAI-compatible endpoint.
|
|
4
|
+
*
|
|
5
|
+
* Vertex serves its partner models (Grok, DeepSeek, Llama...) on
|
|
6
|
+
* `.../endpoints/openapi/chat/completions`, which the stock openai client
|
|
7
|
+
* reaches with a Google access token as its API key. The traceloop
|
|
8
|
+
* instrumentor attributes those calls to "gcp.vertex_ai", which the worker
|
|
9
|
+
* parses as a Gemini payload, while the endpoint answers in OpenAI's shape and
|
|
10
|
+
* counts usage OpenAI's way. So the chat spans get `weflayr.billing_provider`
|
|
11
|
+
* set to `vertex`, which the worker prices by (Google bills them at Vertex's
|
|
12
|
+
* prices), and `gen_ai.provider.name`, which it parses by, set to "openai". The
|
|
13
|
+
* exception is Grok (models named `xai/...`): Vertex reports its reasoning
|
|
14
|
+
* tokens outside `completion_tokens`, as xAI does, so its spans are parsed as
|
|
15
|
+
* "spacexai".
|
|
16
|
+
*
|
|
17
|
+
* Only the chat path is patched: the endpoint serves no embedding model.
|
|
18
|
+
*/
|
|
19
|
+
export declare class VertexOpenAICompatibleInstrumentor {
|
|
20
|
+
private _patched;
|
|
21
|
+
instrument(instrumentation: any): void;
|
|
22
|
+
uninstrument(): void;
|
|
23
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Billing attribution for the openai client pointed at Vertex AI's
|
|
4
|
+
* OpenAI-compatible endpoint.
|
|
5
|
+
*
|
|
6
|
+
* Vertex serves its partner models (Grok, DeepSeek, Llama...) on
|
|
7
|
+
* `.../endpoints/openapi/chat/completions`, which the stock openai client
|
|
8
|
+
* reaches with a Google access token as its API key. The traceloop
|
|
9
|
+
* instrumentor attributes those calls to "gcp.vertex_ai", which the worker
|
|
10
|
+
* parses as a Gemini payload, while the endpoint answers in OpenAI's shape and
|
|
11
|
+
* counts usage OpenAI's way. So the chat spans get `weflayr.billing_provider`
|
|
12
|
+
* set to `vertex`, which the worker prices by (Google bills them at Vertex's
|
|
13
|
+
* prices), and `gen_ai.provider.name`, which it parses by, set to "openai". The
|
|
14
|
+
* exception is Grok (models named `xai/...`): Vertex reports its reasoning
|
|
15
|
+
* tokens outside `completion_tokens`, as xAI does, so its spans are parsed as
|
|
16
|
+
* "spacexai".
|
|
17
|
+
*
|
|
18
|
+
* Only the chat path is patched: the endpoint serves no embedding model.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.VertexOpenAICompatibleInstrumentor = void 0;
|
|
22
|
+
const is_vertex_base_url_1 = require("../shared/is_vertex_base_url");
|
|
23
|
+
const get_openai_compatible_provider_telemetry_1 = require("./get_openai_compatible_provider_telemetry");
|
|
24
|
+
// traceloop's internal tag for a chat request, the value it passes to `startSpan`.
|
|
25
|
+
const CHAT_TYPE = 'chat';
|
|
26
|
+
const PROVIDER_ATTR = 'gen_ai.provider.name';
|
|
27
|
+
// The prefix Vertex names Grok models with on this endpoint, e.g. "xai/grok-4.6".
|
|
28
|
+
const XAI_MODEL_PREFIX = 'xai/';
|
|
29
|
+
const SPACEXAI_PROVIDER = 'spacexai';
|
|
30
|
+
const OPENAI_PROVIDER = 'openai';
|
|
31
|
+
class VertexOpenAICompatibleInstrumentor {
|
|
32
|
+
// [instrumentation, methodName, originalFunction] for uninstrument().
|
|
33
|
+
_patched = [];
|
|
34
|
+
instrument(instrumentation) {
|
|
35
|
+
if (!instrumentation)
|
|
36
|
+
return;
|
|
37
|
+
const original = instrumentation.startSpan;
|
|
38
|
+
if (typeof original !== 'function') {
|
|
39
|
+
throw new Error('weflayr: @traceloop/instrumentation-openai no longer defines startSpan, so chat ' +
|
|
40
|
+
"calls made to Vertex's OpenAI-compatible endpoint cannot be billed to Vertex.");
|
|
41
|
+
}
|
|
42
|
+
instrumentation.startSpan = function startSpanBilledToVertex(args) {
|
|
43
|
+
const span = original.call(this, args);
|
|
44
|
+
try {
|
|
45
|
+
if (args?.type === CHAT_TYPE &&
|
|
46
|
+
span?.isRecording?.() &&
|
|
47
|
+
(0, is_vertex_base_url_1.isVertexBaseURL)((0, get_openai_compatible_provider_telemetry_1.resolveClient)(args.client)?.baseURL)) {
|
|
48
|
+
span.setAttribute(is_vertex_base_url_1.BILLING_PROVIDER_ATTR, is_vertex_base_url_1.VERTEX_BILLING_PROVIDER);
|
|
49
|
+
const isGrok = String(args.params?.model ?? '').startsWith(XAI_MODEL_PREFIX);
|
|
50
|
+
span.setAttribute(PROVIDER_ATTR, isGrok ? SPACEXAI_PROVIDER : OPENAI_PROVIDER);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Never break span creation over an enrichment failure.
|
|
55
|
+
}
|
|
56
|
+
return span;
|
|
57
|
+
};
|
|
58
|
+
this._patched.push([instrumentation, 'startSpan', original]);
|
|
59
|
+
}
|
|
60
|
+
uninstrument() {
|
|
61
|
+
// Restore newest first, so a double instrument() unwinds back to the true
|
|
62
|
+
// original instead of reinstalling the first wrapper.
|
|
63
|
+
for (const [owner, methodName, original] of [...this._patched].reverse()) {
|
|
64
|
+
owner[methodName] = original;
|
|
65
|
+
}
|
|
66
|
+
this._patched = [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.VertexOpenAICompatibleInstrumentor = VertexOpenAICompatibleInstrumentor;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-run web search capture for OpenAI chat spans.
|
|
3
|
+
*
|
|
4
|
+
* OpenAI runs the search itself when the request carries `web_search_options`,
|
|
5
|
+
* and bills the call once whatever number of searches it ran. Unlike every other
|
|
6
|
+
* provider it is not a `tools` entry but a request parameter of its own, and the
|
|
7
|
+
* answer reports the pages it read as `url_citation` annotations on the message
|
|
8
|
+
* rather than as content blocks. The traceloop instrumentor records neither, so
|
|
9
|
+
* a searching call reaches the span as an ordinary one.
|
|
10
|
+
*
|
|
11
|
+
* Both are stamped under `weflayr.web_search.*`, in OpenAI's own vocabulary;
|
|
12
|
+
* `normalizers/openai.py` maps them to the shape every provider's web search is
|
|
13
|
+
* stored in.
|
|
14
|
+
*
|
|
15
|
+
* OpenAI reports no query and no count: it says which pages were cited, never
|
|
16
|
+
* what was typed into the search, and its usage carries no search total. So the
|
|
17
|
+
* stored count is 1 as soon as the answer cites a page, and 0 when a request
|
|
18
|
+
* that allowed searching did not produce one, which is also how OpenAI bills
|
|
19
|
+
* it, per call that used the tool rather than per search.
|
|
20
|
+
*
|
|
21
|
+
* Over a stream the annotations arrive on the deltas, so they are gathered per
|
|
22
|
+
* span as the chunks pass and stamped once the stream ends.
|
|
23
|
+
*
|
|
24
|
+
* The `/v1/responses` endpoint reports the same search in three other places,
|
|
25
|
+
* which `get_responses_telemetry.ts` stamps by calling the helpers exported
|
|
26
|
+
* here: the request enables it with a `{type: "web_search"}` entry in `tools`
|
|
27
|
+
* like every other provider, the answer carries one `web_search_call` output
|
|
28
|
+
* item per search, naming the query OpenAI never reports on chat completions,
|
|
29
|
+
* and `tool_usage.web_search.num_requests` gives the number of searches the
|
|
30
|
+
* call is billed for instead of leaving it to be assumed.
|
|
31
|
+
*/
|
|
32
|
+
/** The pages an answer cited, from every choice's annotations. The
|
|
33
|
+
* `{type, url_citation}` wrapper is dropped and the citation itself kept, since
|
|
34
|
+
* nothing else is annotated this way today. */
|
|
35
|
+
export declare function extractUrlCitations(choices: any): any[];
|
|
36
|
+
/** The `web_search_call` items of a `/v1/responses` answer, one per search the
|
|
37
|
+
* model ran, kept as OpenAI wrote them: `normalizers/openai.py` reads the id and
|
|
38
|
+
* the query off each. */
|
|
39
|
+
export declare function extractResponsesSearchItems(outputItems: any): any[];
|
|
40
|
+
/** The pages a `/v1/responses` answer cited, from the `url_citation` annotations
|
|
41
|
+
* of its message items' content blocks. The endpoint spells one citation flat
|
|
42
|
+
* (`{type: "url_citation", url, title}`) where chat completions nest it under
|
|
43
|
+
* its own type, so the annotation itself is kept and both endpoints stamp the
|
|
44
|
+
* same `{url, title, ...}` shape. */
|
|
45
|
+
export declare function extractResponsesCitations(outputItems: any): any[];
|
|
46
|
+
/** How many searches a `/v1/responses` answer is billed for, from OpenAI's
|
|
47
|
+
* `tool_usage.web_search.num_requests` or xAI's
|
|
48
|
+
* `usage.server_side_tool_usage_details.web_search_calls`, or null when the
|
|
49
|
+
* response reports neither (an answer that ran no search, or an older API
|
|
50
|
+
* version). Unlike chat completions, which report no count at all, this is the
|
|
51
|
+
* provider's own number, so it is stored rather than assumed. */
|
|
52
|
+
export declare function readBilledSearchCount(response: any): number | null;
|
|
53
|
+
/** Stamp the knobs the request's `web_search` tool entry was given. On this
|
|
54
|
+
* endpoint the search is a tool like every other provider's, so the entry's own
|
|
55
|
+
* type is dropped and only its knobs stamped, which is the shape the chat
|
|
56
|
+
* endpoint's `web_search_options` already has. */
|
|
57
|
+
export declare function setResponsesWebSearchRequestAttributes(span: any, params: any): void;
|
|
58
|
+
/** Stamp the searches a `/v1/responses` answer ran, the pages it cited and the
|
|
59
|
+
* number of searches it is billed for. */
|
|
60
|
+
export declare function setResponsesWebSearchAnswerAttributes(span: any, response: any): void;
|
|
61
|
+
export declare class OpenAIWebSearchInstrumentor {
|
|
62
|
+
private _patched;
|
|
63
|
+
instrument(instrumentation: any): void;
|
|
64
|
+
/** Stamp `web_search_options` as sent. An empty object still stamps something:
|
|
65
|
+
* it says the request allowed searching, which is what the stored tool
|
|
66
|
+
* definition records. */
|
|
67
|
+
private _patchRequestOptions;
|
|
68
|
+
/** Gather `delta.annotations` across the streamed chunks. */
|
|
69
|
+
private _patchStreamedCitations;
|
|
70
|
+
/** Stamp the citations as the span is finalized, from the answer itself on a
|
|
71
|
+
* plain call and from the gathered deltas on a streamed one. */
|
|
72
|
+
private _patchResponseCitations;
|
|
73
|
+
uninstrument(): void;
|
|
74
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Provider-run web search capture for OpenAI chat spans.
|
|
4
|
+
*
|
|
5
|
+
* OpenAI runs the search itself when the request carries `web_search_options`,
|
|
6
|
+
* and bills the call once whatever number of searches it ran. Unlike every other
|
|
7
|
+
* provider it is not a `tools` entry but a request parameter of its own, and the
|
|
8
|
+
* answer reports the pages it read as `url_citation` annotations on the message
|
|
9
|
+
* rather than as content blocks. The traceloop instrumentor records neither, so
|
|
10
|
+
* a searching call reaches the span as an ordinary one.
|
|
11
|
+
*
|
|
12
|
+
* Both are stamped under `weflayr.web_search.*`, in OpenAI's own vocabulary;
|
|
13
|
+
* `normalizers/openai.py` maps them to the shape every provider's web search is
|
|
14
|
+
* stored in.
|
|
15
|
+
*
|
|
16
|
+
* OpenAI reports no query and no count: it says which pages were cited, never
|
|
17
|
+
* what was typed into the search, and its usage carries no search total. So the
|
|
18
|
+
* stored count is 1 as soon as the answer cites a page, and 0 when a request
|
|
19
|
+
* that allowed searching did not produce one, which is also how OpenAI bills
|
|
20
|
+
* it, per call that used the tool rather than per search.
|
|
21
|
+
*
|
|
22
|
+
* Over a stream the annotations arrive on the deltas, so they are gathered per
|
|
23
|
+
* span as the chunks pass and stamped once the stream ends.
|
|
24
|
+
*
|
|
25
|
+
* The `/v1/responses` endpoint reports the same search in three other places,
|
|
26
|
+
* which `get_responses_telemetry.ts` stamps by calling the helpers exported
|
|
27
|
+
* here: the request enables it with a `{type: "web_search"}` entry in `tools`
|
|
28
|
+
* like every other provider, the answer carries one `web_search_call` output
|
|
29
|
+
* item per search, naming the query OpenAI never reports on chat completions,
|
|
30
|
+
* and `tool_usage.web_search.num_requests` gives the number of searches the
|
|
31
|
+
* call is billed for instead of leaving it to be assumed.
|
|
32
|
+
*/
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.OpenAIWebSearchInstrumentor = void 0;
|
|
35
|
+
exports.extractUrlCitations = extractUrlCitations;
|
|
36
|
+
exports.extractResponsesSearchItems = extractResponsesSearchItems;
|
|
37
|
+
exports.extractResponsesCitations = extractResponsesCitations;
|
|
38
|
+
exports.readBilledSearchCount = readBilledSearchCount;
|
|
39
|
+
exports.setResponsesWebSearchRequestAttributes = setResponsesWebSearchRequestAttributes;
|
|
40
|
+
exports.setResponsesWebSearchAnswerAttributes = setResponsesWebSearchAnswerAttributes;
|
|
41
|
+
const OPTIONS_ATTR = 'weflayr.web_search.options';
|
|
42
|
+
const CITATIONS_ATTR = 'weflayr.web_search.citations';
|
|
43
|
+
const REQUESTS_ATTR = 'weflayr.web_search.requests';
|
|
44
|
+
// The searches an answer ran, reported by /v1/responses only.
|
|
45
|
+
const OUTPUT_BLOCKS_ATTR = 'weflayr.web_search.output_blocks';
|
|
46
|
+
// The request parameter that lets the model search, and the annotation type the
|
|
47
|
+
// answer cites a page with.
|
|
48
|
+
const WEB_SEARCH_OPTIONS_PARAM = 'web_search_options';
|
|
49
|
+
const URL_CITATION_ANNOTATION_TYPE = 'url_citation';
|
|
50
|
+
// How /v1/responses spells the same three things: the tool the request enables,
|
|
51
|
+
// the output item one search becomes, and the message item carrying the
|
|
52
|
+
// answer's citations.
|
|
53
|
+
const WEB_SEARCH_TOOL_TYPE = 'web_search';
|
|
54
|
+
const WEB_SEARCH_CALL_ITEM_TYPE = 'web_search_call';
|
|
55
|
+
const MESSAGE_ITEM_TYPE = 'message';
|
|
56
|
+
// Citations gathered from a stream's deltas, keyed by the span they belong to,
|
|
57
|
+
// so concurrent streams never mix. Weak so an ended span is collectable.
|
|
58
|
+
const _streamedCitationsBySpan = new WeakMap();
|
|
59
|
+
/** The pages an answer cited, from every choice's annotations. The
|
|
60
|
+
* `{type, url_citation}` wrapper is dropped and the citation itself kept, since
|
|
61
|
+
* nothing else is annotated this way today. */
|
|
62
|
+
function extractUrlCitations(choices) {
|
|
63
|
+
if (!Array.isArray(choices))
|
|
64
|
+
return [];
|
|
65
|
+
const citations = [];
|
|
66
|
+
for (const choice of choices) {
|
|
67
|
+
for (const annotation of choice?.message?.annotations || []) {
|
|
68
|
+
if (annotation && annotation.type === URL_CITATION_ANNOTATION_TYPE) {
|
|
69
|
+
const citation = annotation[URL_CITATION_ANNOTATION_TYPE];
|
|
70
|
+
if (citation)
|
|
71
|
+
citations.push(citation);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return citations;
|
|
76
|
+
}
|
|
77
|
+
/** Stamp the pages the answer cited, and that the call is billed for one search.
|
|
78
|
+
* A request that allowed searching but cited nothing stamps neither: OpenAI
|
|
79
|
+
* bills the call that used the tool, not the one that could have. */
|
|
80
|
+
function setCitations(span, citations) {
|
|
81
|
+
if (!span || !span.isRecording || !span.isRecording() || !citations.length)
|
|
82
|
+
return;
|
|
83
|
+
span.setAttribute(CITATIONS_ATTR, JSON.stringify(citations));
|
|
84
|
+
span.setAttribute(REQUESTS_ATTR, 1);
|
|
85
|
+
}
|
|
86
|
+
/** The `web_search_call` items of a `/v1/responses` answer, one per search the
|
|
87
|
+
* model ran, kept as OpenAI wrote them: `normalizers/openai.py` reads the id and
|
|
88
|
+
* the query off each. */
|
|
89
|
+
function extractResponsesSearchItems(outputItems) {
|
|
90
|
+
if (!Array.isArray(outputItems))
|
|
91
|
+
return [];
|
|
92
|
+
return outputItems.filter((item) => item && typeof item === 'object' && item.type === WEB_SEARCH_CALL_ITEM_TYPE);
|
|
93
|
+
}
|
|
94
|
+
/** The pages a `/v1/responses` answer cited, from the `url_citation` annotations
|
|
95
|
+
* of its message items' content blocks. The endpoint spells one citation flat
|
|
96
|
+
* (`{type: "url_citation", url, title}`) where chat completions nest it under
|
|
97
|
+
* its own type, so the annotation itself is kept and both endpoints stamp the
|
|
98
|
+
* same `{url, title, ...}` shape. */
|
|
99
|
+
function extractResponsesCitations(outputItems) {
|
|
100
|
+
if (!Array.isArray(outputItems))
|
|
101
|
+
return [];
|
|
102
|
+
const citations = [];
|
|
103
|
+
for (const item of outputItems) {
|
|
104
|
+
if (!item || typeof item !== 'object' || item.type !== MESSAGE_ITEM_TYPE)
|
|
105
|
+
continue;
|
|
106
|
+
for (const block of Array.isArray(item.content) ? item.content : []) {
|
|
107
|
+
for (const annotation of Array.isArray(block?.annotations) ? block.annotations : []) {
|
|
108
|
+
if (annotation && annotation.type === URL_CITATION_ANNOTATION_TYPE) {
|
|
109
|
+
citations.push(annotation);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return citations;
|
|
115
|
+
}
|
|
116
|
+
/** How many searches a `/v1/responses` answer is billed for, from OpenAI's
|
|
117
|
+
* `tool_usage.web_search.num_requests` or xAI's
|
|
118
|
+
* `usage.server_side_tool_usage_details.web_search_calls`, or null when the
|
|
119
|
+
* response reports neither (an answer that ran no search, or an older API
|
|
120
|
+
* version). Unlike chat completions, which report no count at all, this is the
|
|
121
|
+
* provider's own number, so it is stored rather than assumed. */
|
|
122
|
+
function readBilledSearchCount(response) {
|
|
123
|
+
const numRequests = response?.tool_usage?.[WEB_SEARCH_TOOL_TYPE]?.num_requests;
|
|
124
|
+
if (typeof numRequests === 'number')
|
|
125
|
+
return numRequests;
|
|
126
|
+
const webSearchCalls = response?.usage?.server_side_tool_usage_details?.web_search_calls;
|
|
127
|
+
return typeof webSearchCalls === 'number' ? webSearchCalls : null;
|
|
128
|
+
}
|
|
129
|
+
/** Stamp the knobs the request's `web_search` tool entry was given. On this
|
|
130
|
+
* endpoint the search is a tool like every other provider's, so the entry's own
|
|
131
|
+
* type is dropped and only its knobs stamped, which is the shape the chat
|
|
132
|
+
* endpoint's `web_search_options` already has. */
|
|
133
|
+
function setResponsesWebSearchRequestAttributes(span, params) {
|
|
134
|
+
if (!span || !span.isRecording || !span.isRecording())
|
|
135
|
+
return;
|
|
136
|
+
for (const tool of Array.isArray(params?.tools) ? params.tools : []) {
|
|
137
|
+
if (tool && typeof tool === 'object' && tool.type === WEB_SEARCH_TOOL_TYPE) {
|
|
138
|
+
const options = Object.fromEntries(Object.entries(tool).filter(([name]) => name !== 'type'));
|
|
139
|
+
span.setAttribute(OPTIONS_ATTR, JSON.stringify(options));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Stamp the searches a `/v1/responses` answer ran, the pages it cited and the
|
|
145
|
+
* number of searches it is billed for. */
|
|
146
|
+
function setResponsesWebSearchAnswerAttributes(span, response) {
|
|
147
|
+
if (!span || !span.isRecording || !span.isRecording())
|
|
148
|
+
return;
|
|
149
|
+
const searches = extractResponsesSearchItems(response?.output);
|
|
150
|
+
if (searches.length)
|
|
151
|
+
span.setAttribute(OUTPUT_BLOCKS_ATTR, JSON.stringify(searches));
|
|
152
|
+
const citations = extractResponsesCitations(response?.output);
|
|
153
|
+
if (citations.length)
|
|
154
|
+
span.setAttribute(CITATIONS_ATTR, JSON.stringify(citations));
|
|
155
|
+
const billedSearchCount = readBilledSearchCount(response);
|
|
156
|
+
if (billedSearchCount)
|
|
157
|
+
span.setAttribute(REQUESTS_ATTR, billedSearchCount);
|
|
158
|
+
}
|
|
159
|
+
class OpenAIWebSearchInstrumentor {
|
|
160
|
+
// [instrumentation, methodName, originalFunction] for uninstrument().
|
|
161
|
+
_patched = [];
|
|
162
|
+
// No-op (and never throws) if the instrumentor's internals have changed shape:
|
|
163
|
+
// the worst case is a missing web search, never a crash.
|
|
164
|
+
instrument(instrumentation) {
|
|
165
|
+
if (!instrumentation)
|
|
166
|
+
return;
|
|
167
|
+
this._patchRequestOptions(instrumentation);
|
|
168
|
+
this._patchStreamedCitations(instrumentation);
|
|
169
|
+
this._patchResponseCitations(instrumentation);
|
|
170
|
+
}
|
|
171
|
+
/** Stamp `web_search_options` as sent. An empty object still stamps something:
|
|
172
|
+
* it says the request allowed searching, which is what the stored tool
|
|
173
|
+
* definition records. */
|
|
174
|
+
_patchRequestOptions(instrumentation) {
|
|
175
|
+
const original = instrumentation.startSpan;
|
|
176
|
+
if (typeof original !== 'function')
|
|
177
|
+
return;
|
|
178
|
+
instrumentation.startSpan = function startSpanWithWebSearchOptions(request) {
|
|
179
|
+
const span = original.call(this, request);
|
|
180
|
+
try {
|
|
181
|
+
const options = request?.params?.[WEB_SEARCH_OPTIONS_PARAM];
|
|
182
|
+
if (options !== undefined && options !== null) {
|
|
183
|
+
span.setAttribute(OPTIONS_ATTR, JSON.stringify(typeof options === 'object' ? options : {}));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// Never break span creation over an enrichment failure.
|
|
188
|
+
}
|
|
189
|
+
return span;
|
|
190
|
+
};
|
|
191
|
+
this._patched.push([instrumentation, 'startSpan', original]);
|
|
192
|
+
}
|
|
193
|
+
/** Gather `delta.annotations` across the streamed chunks. */
|
|
194
|
+
_patchStreamedCitations(instrumentation) {
|
|
195
|
+
const original = instrumentation._streamingWrapPromise;
|
|
196
|
+
if (typeof original !== 'function')
|
|
197
|
+
return;
|
|
198
|
+
instrumentation._streamingWrapPromise = function streamingWrapWithCitations(streamArgs) {
|
|
199
|
+
const span = streamArgs && streamArgs.span;
|
|
200
|
+
const wrappedStream = original.call(this, streamArgs);
|
|
201
|
+
if (!span)
|
|
202
|
+
return wrappedStream;
|
|
203
|
+
return (async function* accumulateCitations() {
|
|
204
|
+
for await (const chunk of wrappedStream) {
|
|
205
|
+
try {
|
|
206
|
+
for (const choice of chunk?.choices || []) {
|
|
207
|
+
for (const annotation of choice?.delta?.annotations || []) {
|
|
208
|
+
if (annotation && annotation.type === URL_CITATION_ANNOTATION_TYPE) {
|
|
209
|
+
const citation = annotation[URL_CITATION_ANNOTATION_TYPE];
|
|
210
|
+
if (citation) {
|
|
211
|
+
_streamedCitationsBySpan.set(span, [
|
|
212
|
+
...(_streamedCitationsBySpan.get(span) ?? []),
|
|
213
|
+
citation,
|
|
214
|
+
]);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// Never break the caller's stream over an enrichment failure.
|
|
222
|
+
}
|
|
223
|
+
yield chunk;
|
|
224
|
+
}
|
|
225
|
+
})();
|
|
226
|
+
};
|
|
227
|
+
this._patched.push([instrumentation, '_streamingWrapPromise', original]);
|
|
228
|
+
}
|
|
229
|
+
/** Stamp the citations as the span is finalized, from the answer itself on a
|
|
230
|
+
* plain call and from the gathered deltas on a streamed one. */
|
|
231
|
+
_patchResponseCitations(instrumentation) {
|
|
232
|
+
const original = instrumentation._endSpan;
|
|
233
|
+
if (typeof original !== 'function')
|
|
234
|
+
return;
|
|
235
|
+
instrumentation._endSpan = function endSpanWithCitations(args) {
|
|
236
|
+
try {
|
|
237
|
+
const span = args && args.span;
|
|
238
|
+
const streamed = span && _streamedCitationsBySpan.get(span);
|
|
239
|
+
setCitations(span, streamed ?? extractUrlCitations(args?.result?.choices));
|
|
240
|
+
if (span)
|
|
241
|
+
_streamedCitationsBySpan.delete(span);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// Never break span finalization over an enrichment failure.
|
|
245
|
+
}
|
|
246
|
+
return original.call(this, args);
|
|
247
|
+
};
|
|
248
|
+
this._patched.push([instrumentation, '_endSpan', original]);
|
|
249
|
+
}
|
|
250
|
+
uninstrument() {
|
|
251
|
+
// Restore newest first, so a double instrument() unwinds back to the true
|
|
252
|
+
// original instead of reinstalling the first wrapper.
|
|
253
|
+
for (const [owner, methodName, original] of [...this._patched].reverse()) {
|
|
254
|
+
owner[methodName] = original;
|
|
255
|
+
}
|
|
256
|
+
this._patched = [];
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
exports.OpenAIWebSearchInstrumentor = OpenAIWebSearchInstrumentor;
|
|
@@ -7,7 +7,19 @@
|
|
|
7
7
|
* non-streamed cached-prompt tokens (these enrich the traceloop instrumentor
|
|
8
8
|
* instance, hence the `instrumentation` argument), and
|
|
9
9
|
* - the request's structured-output schema, and
|
|
10
|
-
* -
|
|
10
|
+
* - the Responses endpoint, which it wraps but records far less of than the
|
|
11
|
+
* Python instrumentor does (and not at all when streamed), and
|
|
12
|
+
* - Bedrock Mantle's provider attribution and reasoning output, and
|
|
13
|
+
* - the provider-run web search (`web_search_options` and the answer's
|
|
14
|
+
* `url_citation` annotations), and
|
|
15
|
+
* - the mime type of an inline file (PDF, ...), which it reads from a
|
|
16
|
+
* field OpenAI's API does not have and so always records as
|
|
17
|
+
* application/octet-stream, and
|
|
18
|
+
* - the provider attribution of the vendors served over an
|
|
19
|
+
* OpenAI-compatible API (Alibaba Model Studio, xAI), and
|
|
20
|
+
* - the billing of the calls to Vertex AI's OpenAI-compatible endpoint, and
|
|
21
|
+
* - a `reasoning_content` output part for any provider that returns one, and
|
|
22
|
+
* - the service tier the call was served in (flex, priority, ...).
|
|
11
23
|
*/
|
|
12
24
|
export declare class OpenAITelemetryInstrumentor {
|
|
13
25
|
private _subInstrumentors;
|