weflayr 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/package.json +3 -3
  2. package/dist/src/auto-instrument.d.ts +4 -3
  3. package/dist/src/auto-instrument.js +18 -17
  4. package/dist/src/get-data-uri-mime-type.d.ts +17 -0
  5. package/dist/src/get-data-uri-mime-type.js +25 -0
  6. package/dist/src/index.d.ts +3 -2
  7. package/dist/src/index.js +5 -1
  8. package/dist/src/instrumentation/anthropic/get_streaming_accumulation_telemetry.d.ts +3 -1
  9. package/dist/src/instrumentation/anthropic/get_streaming_accumulation_telemetry.js +6 -3
  10. package/dist/src/instrumentation/anthropic/get_vertex_billing_telemetry.d.ts +32 -0
  11. package/dist/src/instrumentation/anthropic/get_vertex_billing_telemetry.js +100 -0
  12. package/dist/src/instrumentation/anthropic/get_web_search_telemetry.d.ts +43 -0
  13. package/dist/src/instrumentation/anthropic/get_web_search_telemetry.js +147 -0
  14. package/dist/src/instrumentation/anthropic/set_telemetry_attributes.d.ts +5 -2
  15. package/dist/src/instrumentation/anthropic/set_telemetry_attributes.js +17 -4
  16. package/dist/src/instrumentation/bedrock/get_cache_config_telemetry.d.ts +13 -1
  17. package/dist/src/instrumentation/bedrock/get_cache_config_telemetry.js +56 -1
  18. package/dist/src/instrumentation/bedrock/get_cache_telemetry.js +8 -5
  19. package/dist/src/instrumentation/bedrock/get_model_id_telemetry.d.ts +1 -1
  20. package/dist/src/instrumentation/bedrock/get_model_id_telemetry.js +1 -1
  21. package/dist/src/instrumentation/bedrock/get_openai_chat_format_telemetry.d.ts +45 -0
  22. package/dist/src/instrumentation/bedrock/get_openai_chat_format_telemetry.js +251 -0
  23. package/dist/src/instrumentation/bedrock/get_streaming_output_message_telemetry.d.ts +6 -5
  24. package/dist/src/instrumentation/bedrock/get_streaming_output_message_telemetry.js +15 -12
  25. package/dist/src/instrumentation/bedrock/get_tool_definitions_telemetry.d.ts +2 -2
  26. package/dist/src/instrumentation/bedrock/get_tool_definitions_telemetry.js +12 -4
  27. package/dist/src/instrumentation/bedrock/set_telemetry_attributes.d.ts +4 -2
  28. package/dist/src/instrumentation/bedrock/set_telemetry_attributes.js +10 -3
  29. package/dist/src/instrumentation/cohere/get_embeddings_telemetry.js +4 -6
  30. package/dist/src/instrumentation/cohere/get_input_message_content_telemetry.d.ts +23 -0
  31. package/dist/src/instrumentation/cohere/get_input_message_content_telemetry.js +105 -0
  32. package/dist/src/instrumentation/cohere/get_v2_chat_telemetry.d.ts +18 -0
  33. package/dist/src/instrumentation/cohere/get_v2_chat_telemetry.js +186 -0
  34. package/dist/src/instrumentation/cohere/set_telemetry_attributes.d.ts +3 -1
  35. package/dist/src/instrumentation/cohere/set_telemetry_attributes.js +5 -1
  36. package/dist/src/instrumentation/google/get_embeddings_telemetry.d.ts +8 -4
  37. package/dist/src/instrumentation/google/get_embeddings_telemetry.js +68 -35
  38. package/dist/src/instrumentation/google/get_provider_name.d.ts +2 -1
  39. package/dist/src/instrumentation/google/get_provider_name.js +2 -1
  40. package/dist/src/instrumentation/google/get_web_search_telemetry.d.ts +38 -0
  41. package/dist/src/instrumentation/google/get_web_search_telemetry.js +125 -0
  42. package/dist/src/instrumentation/google/set_telemetry_attributes.d.ts +2 -2
  43. package/dist/src/instrumentation/google/set_telemetry_attributes.js +6 -2
  44. package/dist/src/instrumentation/mistral/full_instrumentation.d.ts +60 -0
  45. package/dist/src/instrumentation/mistral/full_instrumentation.js +364 -0
  46. package/dist/src/instrumentation/mistral/get_input_message_content_telemetry.d.ts +35 -0
  47. package/dist/src/instrumentation/mistral/get_input_message_content_telemetry.js +113 -0
  48. package/dist/src/instrumentation/mistral/get_structured_output_schema_telemetry.d.ts +13 -0
  49. package/dist/src/instrumentation/mistral/get_structured_output_schema_telemetry.js +24 -0
  50. package/dist/src/instrumentation/mistral/get_vertex_telemetry.d.ts +54 -0
  51. package/dist/src/instrumentation/mistral/get_vertex_telemetry.js +153 -0
  52. package/dist/src/instrumentation/openai/get_bedrock_mantle_telemetry.d.ts +10 -0
  53. package/dist/src/instrumentation/openai/get_bedrock_mantle_telemetry.js +33 -0
  54. package/dist/src/instrumentation/openai/get_cache_and_reasoning_telemetry.d.ts +13 -6
  55. package/dist/src/instrumentation/openai/get_cache_and_reasoning_telemetry.js +17 -7
  56. package/dist/src/instrumentation/openai/get_cache_config_telemetry.d.ts +21 -4
  57. package/dist/src/instrumentation/openai/get_cache_config_telemetry.js +50 -11
  58. package/dist/src/instrumentation/openai/get_embeddings_telemetry.js +14 -6
  59. package/dist/src/instrumentation/openai/get_file_part_mime_type_telemetry.d.ts +26 -0
  60. package/dist/src/instrumentation/openai/get_file_part_mime_type_telemetry.js +100 -0
  61. package/dist/src/instrumentation/openai/get_openai_compatible_provider_telemetry.d.ts +48 -0
  62. package/dist/src/instrumentation/openai/get_openai_compatible_provider_telemetry.js +123 -0
  63. package/dist/src/instrumentation/openai/get_reasoning_content_telemetry.d.ts +28 -0
  64. package/dist/src/instrumentation/openai/get_reasoning_content_telemetry.js +130 -0
  65. package/dist/src/instrumentation/openai/get_responses_telemetry.d.ts +89 -0
  66. package/dist/src/instrumentation/openai/get_responses_telemetry.js +587 -0
  67. package/dist/src/instrumentation/openai/get_service_tier_telemetry.d.ts +20 -0
  68. package/dist/src/instrumentation/openai/get_service_tier_telemetry.js +84 -0
  69. package/dist/src/instrumentation/openai/get_structured_output_schema_telemetry.d.ts +7 -5
  70. package/dist/src/instrumentation/openai/get_structured_output_schema_telemetry.js +15 -8
  71. package/dist/src/instrumentation/openai/get_vertex_openai_compatible_telemetry.d.ts +23 -0
  72. package/dist/src/instrumentation/openai/get_vertex_openai_compatible_telemetry.js +69 -0
  73. package/dist/src/instrumentation/openai/get_web_search_telemetry.d.ts +74 -0
  74. package/dist/src/instrumentation/openai/get_web_search_telemetry.js +259 -0
  75. package/dist/src/instrumentation/openai/set_telemetry_attributes.d.ts +13 -1
  76. package/dist/src/instrumentation/openai/set_telemetry_attributes.js +33 -2
  77. package/dist/src/instrumentation/shared/get_structured_output_schema_telemetry.d.ts +1 -1
  78. package/dist/src/instrumentation/shared/get_structured_output_schema_telemetry.js +1 -1
  79. package/dist/src/instrumentation/shared/is_vertex_base_url.d.ts +17 -0
  80. package/dist/src/instrumentation/shared/is_vertex_base_url.js +40 -0
  81. package/dist/src/otel/propagation.d.ts +37 -10
  82. package/dist/src/otel/propagation.js +62 -22
  83. package/dist/src/otel/span-filter.d.ts +2 -0
  84. package/dist/src/otel/span-filter.js +10 -1
  85. package/dist/src/otel/span-processor.d.ts +1 -2
  86. package/dist/src/otel/span-processor.js +17 -20
  87. package/dist/src/otel/strip-media-payloads.d.ts +26 -0
  88. package/dist/src/otel/strip-media-payloads.js +135 -0
  89. package/dist/src/propagate-metadata-to-gateway.d.ts +37 -0
  90. package/dist/src/propagate-metadata-to-gateway.js +213 -0
  91. package/package.json +3 -3
  92. package/dist/src/instrumentation/vercel/full_instrumentation.d.ts +0 -3
  93. package/dist/src/instrumentation/vercel/full_instrumentation.js +0 -54
  94. package/dist/src/instrumentation/vercel/get_cache_creation_telemetry.d.ts +0 -22
  95. package/dist/src/instrumentation/vercel/get_cache_creation_telemetry.js +0 -66
  96. package/dist/src/instrumentation/vercel/get_structured_output_schema_telemetry.d.ts +0 -24
  97. package/dist/src/instrumentation/vercel/get_structured_output_schema_telemetry.js +0 -84
  98. package/dist/src/otel/vercel-ai-sdk-span-filter.d.ts +0 -12
  99. package/dist/src/otel/vercel-ai-sdk-span-filter.js +0 -88
@@ -0,0 +1,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 } }`. The
6
- * traceloop instrumentor records the request's model and sampling params but
7
- * never the schema - which OpenAI bills as input.
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 this one natively, so the Python
10
- * SDK has no OpenAI counterpart to this file - see known_divergences.md.)
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 } }`. The
7
- * traceloop instrumentor records the request's model and sampling params but
8
- * never the schema - which OpenAI bills as input.
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 this one natively, so the Python
11
- * SDK has no OpenAI counterpart to this file - see known_divergences.md.)
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 (!responseFormat || responseFormat.type !== 'json_schema')
18
- return undefined;
19
- return responseFormat.json_schema && responseFormat.json_schema.schema;
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
- * - Bedrock Mantle's provider attribution and reasoning output.
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;