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
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * Request-side prompt-caching config capture for OpenAI chat spans.
3
+ * Request-side param capture for OpenAI chat and Responses spans.
4
4
  *
5
5
  * OpenAI callers influence the (automatic, server-side) prompt cache with
6
6
  * request parameters the traceloop instrumentor never records:
@@ -12,15 +12,32 @@
12
12
  * message content block (`prompt_cache_options: { mode: "explicit" }`
13
13
  * disables the automatic breakpoint, requiring these to cache anything).
14
14
  *
15
+ * It covers the chat and the Responses endpoints alike: both take the same
16
+ * params, and a marker sits on a content block of either `messages` (chat) or
17
+ * `input` (Responses), recorded as `messages[i]` / `input[i]`. A Responses
18
+ * request's `instructions` count as its item 0 when it sends them, as the
19
+ * worker numbers every provider's system prompt.
20
+ *
15
21
  * Wrap `startSpan({ type, params })` (covering the streamed and non-streamed
16
22
  * paths) to stamp the values as `weflayr.cache.key` / `weflayr.cache.retention`
17
23
  * (raw retention/ttl string) and, when any content block carries the marker,
18
- * `weflayr.cache.breakpoints` (a JSON list of `{"location": "messages[i]"}`
19
- * entries — the marker itself carries no TTL, unlike Anthropic's
20
- * `cache_control`; the normalizer owns the vocabulary).
24
+ * `weflayr.cache.breakpoints`: a JSON list of `{"location": "messages[i]"}` /
25
+ * `{"location": "input[i]"}` entries for OpenAI's own `prompt_cache_breakpoint`
26
+ * marker (which carries no TTL of its own; the normalizer falls back to the
27
+ * request-level retention and owns the vocabulary) or, for a content block
28
+ * carrying Anthropic's `cache_control` convention instead (Alibaba (Qwen) is
29
+ * reached through this same openai-shape client and opts into its *explicit*
30
+ * cache this way, unlike real OpenAI), `{"location": ..., "ttl": ...}`, the raw
31
+ * `ttl` string carried on the marker itself, omitted when it sets none.
21
32
  * `prompt_cache_options: { mode: "explicit" }` is stamped as
22
33
  * `weflayr.cache.mode` so the normalizer knows not to also synthesize the
23
34
  * automatic "auto"-location breakpoint OpenAI does not create in this mode.
35
+ *
36
+ * The same wrapper stamps the output-token limit when the caller sent it as
37
+ * `max_completion_tokens`: the traceloop instrumentor reads only `max_tokens`,
38
+ * which the reasoning models (o-series, GPT-5 and later) reject, so their calls
39
+ * would record no limit at all. It lives here because this is the one place the
40
+ * request's own params are read.
24
41
  */
25
42
  Object.defineProperty(exports, "__esModule", { value: true });
26
43
  exports.OpenAICacheConfigInstrumentor = void 0;
@@ -28,21 +45,39 @@ const CACHE_KEY_ATTR = 'weflayr.cache.key';
28
45
  const CACHE_RETENTION_ATTR = 'weflayr.cache.retention';
29
46
  const CACHE_MODE_ATTR = 'weflayr.cache.mode';
30
47
  const BREAKPOINTS_ATTR = 'weflayr.cache.breakpoints';
31
- function explicitBreakpoints(params) {
48
+ const MAX_TOKENS_ATTR = 'gen_ai.request.max_tokens';
49
+ function breakpointsInItems(items, listName, firstIndex) {
32
50
  const breakpoints = [];
33
- const messages = (params && params.messages) || [];
34
- messages.forEach((message, index) => {
35
- const content = message && message.content;
51
+ if (!Array.isArray(items))
52
+ return breakpoints;
53
+ items.forEach((item, position) => {
54
+ const index = firstIndex + position;
55
+ const content = item && item.content;
36
56
  if (!Array.isArray(content))
37
57
  return;
38
58
  for (const block of content) {
39
- if (block && typeof block === 'object' && 'prompt_cache_breakpoint' in block) {
40
- breakpoints.push({ location: `messages[${index}]` });
59
+ if (!block || typeof block !== 'object')
60
+ continue;
61
+ if ('prompt_cache_breakpoint' in block) {
62
+ breakpoints.push({ location: `${listName}[${index}]` });
63
+ }
64
+ const cacheControl = block.cache_control;
65
+ if (cacheControl && typeof cacheControl === 'object') {
66
+ const entry = { location: `${listName}[${index}]` };
67
+ if (cacheControl.ttl)
68
+ entry.ttl = String(cacheControl.ttl);
69
+ breakpoints.push(entry);
41
70
  }
42
71
  }
43
72
  });
44
73
  return breakpoints;
45
74
  }
75
+ function explicitBreakpoints(params) {
76
+ return [
77
+ ...breakpointsInItems(params && params.messages, 'messages', 0),
78
+ ...breakpointsInItems(params && params.input, 'input', params && params.instructions ? 1 : 0),
79
+ ];
80
+ }
46
81
  class OpenAICacheConfigInstrumentor {
47
82
  // [instrumentation, methodName, originalFunction] for uninstrument().
48
83
  _patched = [];
@@ -54,7 +89,7 @@ class OpenAICacheConfigInstrumentor {
54
89
  const startSpanOriginal = instrumentation.startSpan;
55
90
  if (typeof startSpanOriginal !== 'function')
56
91
  return;
57
- instrumentation.startSpan = function startSpanWithCacheConfig(request) {
92
+ instrumentation.startSpan = function startSpanWithRequestParams(request) {
58
93
  const span = startSpanOriginal.call(this, request);
59
94
  try {
60
95
  const params = (request && request.params) || {};
@@ -73,6 +108,10 @@ class OpenAICacheConfigInstrumentor {
73
108
  if (breakpoints.length) {
74
109
  span.setAttribute(BREAKPOINTS_ATTR, JSON.stringify(breakpoints));
75
110
  }
111
+ // `max_tokens` is already stamped by traceloop, and no model takes both.
112
+ if (params.max_completion_tokens !== undefined && params.max_completion_tokens !== null) {
113
+ span.setAttribute(MAX_TOKENS_ATTR, params.max_completion_tokens);
114
+ }
76
115
  }
77
116
  catch {
78
117
  // Never break span creation over an enrichment failure.
@@ -15,14 +15,23 @@
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
16
  exports.OpenAIEmbeddingsInstrumentor = void 0;
17
17
  const api_1 = require("@opentelemetry/api");
18
+ const get_openai_compatible_provider_telemetry_1 = require("./get_openai_compatible_provider_telemetry");
18
19
  const TRACER_NAME = 'weflayr.instrumentation.openai_embeddings';
19
20
  const EMBEDDINGS_OPERATION = 'embeddings';
21
+ const EMBEDDINGS_INPUT_ATTR = 'weflayr.embeddings.input';
20
22
  // Return the gen_ai.system for this call, matching how the OpenAI chat
21
23
  // instrumentor attributes the vendor: Azure clients (reached via the same openai
22
- // SDK) report `azure.ai.openai`, everything else `openai`.
24
+ // SDK) report `azure.ai.openai`, Alibaba Model Studio clients report `alibaba`
25
+ // (see get_openai_compatible_provider_telemetry.ts for the exact hosts
26
+ // recognized), everything else `openai`. The other vendor that file knows, xAI,
27
+ // is not checked here: xAI serves no embedding model.
23
28
  function _detectSystem(instance) {
24
29
  const baseURL = (instance && instance._client && instance._client.baseURL) || '';
25
- return baseURL.toLowerCase().includes('openai.azure.com') ? 'azure.ai.openai' : 'openai';
30
+ if (baseURL.toLowerCase().includes('openai.azure.com'))
31
+ return 'azure.ai.openai';
32
+ if ((0, get_openai_compatible_provider_telemetry_1.isProviderBaseURL)(get_openai_compatible_provider_telemetry_1.ALIBABA_PROVIDER, baseURL))
33
+ return get_openai_compatible_provider_telemetry_1.ALIBABA_PROVIDER.providerName;
34
+ return 'openai';
26
35
  }
27
36
  function _recordError(span, error) {
28
37
  span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error?.message ?? String(error) });
@@ -50,12 +59,11 @@ class OpenAIEmbeddingsInstrumentor {
50
59
  if (body && body.model) {
51
60
  span.setAttribute('gen_ai.request.model', body.model);
52
61
  }
53
- // One user message per input element, matching the shape the chat
54
- // instrumentors emit. The span processor strips it when message-content
55
- // capture is disabled.
62
+ // The embedded inputs, as passed (one or a list); the worker shapes them.
63
+ // The span processor strips it when message-content capture is disabled.
56
64
  if (body && body.input != null) {
57
65
  const inputs = Array.isArray(body.input) ? body.input : [body.input];
58
- span.setAttribute('gen_ai.input.messages', JSON.stringify(inputs.map((content) => ({ role: 'user', parts: [{ type: 'text', content }] }))));
66
+ span.setAttribute(EMBEDDINGS_INPUT_ATTR, JSON.stringify(inputs));
59
67
  }
60
68
  return Promise.resolve(original.call(this, body, ...rest)).then((response) => {
61
69
  if (response && response.model) {
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Mime type of a file (PDF, ...) sent inline to OpenAI chat completions.
3
+ *
4
+ * The traceloop instrumentor maps a `file` content block to a blob part but
5
+ * reads its mime type from `block.file.mime_type`, a field OpenAI's API does
6
+ * not have, so every inline document is recorded as
7
+ * `application/octet-stream`. The real mime type is right there in the data URI
8
+ * the caller sent (`data:application/pdf;base64,...`), and the Python SDK keeps
9
+ * it, so this complement reads it back off the request and repairs the part.
10
+ *
11
+ * Wraps `startSpan({ type, params })`, which is where the instrumentor builds
12
+ * `gen_ai.input.messages`, so the attribute is already on the span when this
13
+ * runs. A message's content blocks map one-to-one onto the leading parts of its
14
+ * message, which is what lets the repair walk them in step.
15
+ */
16
+ /**
17
+ * `gen_ai.input.messages` with every blob part whose request block carried a
18
+ * data URI given that URI's mime type, or null when there was nothing to
19
+ * repair (so the instrumentor's own JSON is left untouched).
20
+ */
21
+ export declare function repairFileMimeTypes(inputMessagesJson: unknown, messages: unknown): string | null;
22
+ export declare class OpenAIFilePartMimeTypeInstrumentor {
23
+ private _patched;
24
+ instrument(instrumentation: any): void;
25
+ uninstrument(): void;
26
+ }
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ /**
3
+ * Mime type of a file (PDF, ...) sent inline to OpenAI chat completions.
4
+ *
5
+ * The traceloop instrumentor maps a `file` content block to a blob part but
6
+ * reads its mime type from `block.file.mime_type`, a field OpenAI's API does
7
+ * not have, so every inline document is recorded as
8
+ * `application/octet-stream`. The real mime type is right there in the data URI
9
+ * the caller sent (`data:application/pdf;base64,...`), and the Python SDK keeps
10
+ * it, so this complement reads it back off the request and repairs the part.
11
+ *
12
+ * Wraps `startSpan({ type, params })`, which is where the instrumentor builds
13
+ * `gen_ai.input.messages`, so the attribute is already on the span when this
14
+ * runs. A message's content blocks map one-to-one onto the leading parts of its
15
+ * message, which is what lets the repair walk them in step.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.OpenAIFilePartMimeTypeInstrumentor = void 0;
19
+ exports.repairFileMimeTypes = repairFileMimeTypes;
20
+ const get_data_uri_mime_type_1 = require("../../get-data-uri-mime-type");
21
+ const INPUT_MESSAGES_ATTR = 'gen_ai.input.messages';
22
+ function asObject(value) {
23
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
24
+ ? value
25
+ : null;
26
+ }
27
+ /** The mime type of the data URI a request content block carries, if any. */
28
+ function getBlockMimeType(block) {
29
+ const fileBlock = asObject(asObject(block)?.file);
30
+ if (fileBlock === null)
31
+ return null;
32
+ return (0, get_data_uri_mime_type_1.getDataUriMimeType)(String(fileBlock.file_data ?? ''));
33
+ }
34
+ /**
35
+ * `gen_ai.input.messages` with every blob part whose request block carried a
36
+ * data URI given that URI's mime type, or null when there was nothing to
37
+ * repair (so the instrumentor's own JSON is left untouched).
38
+ */
39
+ function repairFileMimeTypes(inputMessagesJson, messages) {
40
+ if (typeof inputMessagesJson !== 'string' || !Array.isArray(messages))
41
+ return null;
42
+ let parsed;
43
+ try {
44
+ parsed = JSON.parse(inputMessagesJson);
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ if (!Array.isArray(parsed))
50
+ return null;
51
+ let repaired = false;
52
+ messages.forEach((message, messageIndex) => {
53
+ const content = asObject(message)?.content;
54
+ const parts = asObject(parsed[messageIndex])?.parts;
55
+ if (!Array.isArray(content) || !Array.isArray(parts))
56
+ return;
57
+ content.forEach((block, blockIndex) => {
58
+ const mimeType = getBlockMimeType(block);
59
+ const part = asObject(parts[blockIndex]);
60
+ if (mimeType === null || part?.type !== 'blob' || part.mime_type === mimeType)
61
+ return;
62
+ part.mime_type = mimeType;
63
+ repaired = true;
64
+ });
65
+ });
66
+ return repaired ? JSON.stringify(parsed) : null;
67
+ }
68
+ class OpenAIFilePartMimeTypeInstrumentor {
69
+ // [instrumentation, methodName, originalFunction] for uninstrument().
70
+ _patched = [];
71
+ // No-op (and never throws) if the instrumentor's internals have changed shape -
72
+ // the worst case is a document recorded under a vaguer mime type, never a crash.
73
+ instrument(instrumentation) {
74
+ if (!instrumentation)
75
+ return;
76
+ const startSpanOriginal = instrumentation.startSpan;
77
+ if (typeof startSpanOriginal !== 'function')
78
+ return;
79
+ instrumentation.startSpan = function startSpanWithFileMimeTypes(request) {
80
+ const span = startSpanOriginal.call(this, request);
81
+ try {
82
+ const repaired = repairFileMimeTypes(span?.attributes?.[INPUT_MESSAGES_ATTR], request?.params?.messages);
83
+ if (repaired !== null)
84
+ span.setAttribute(INPUT_MESSAGES_ATTR, repaired);
85
+ }
86
+ catch {
87
+ // Never break span creation over an enrichment failure.
88
+ }
89
+ return span;
90
+ };
91
+ this._patched.push([instrumentation, 'startSpan', startSpanOriginal]);
92
+ }
93
+ uninstrument() {
94
+ for (const [owner, methodName, original] of this._patched) {
95
+ owner[methodName] = original;
96
+ }
97
+ this._patched = [];
98
+ }
99
+ }
100
+ exports.OpenAIFilePartMimeTypeInstrumentor = OpenAIFilePartMimeTypeInstrumentor;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Vendor detection for the OpenAI chat path of vendors served over an
3
+ * OpenAI-compatible API.
4
+ *
5
+ * Alibaba Cloud Model Studio (DashScope) serves Qwen and other models, and xAI
6
+ * serves the Grok models, over an OpenAI-compatible API, so both are reached
7
+ * with the stock `openai` SDK pointed at the vendor's own base URL. The
8
+ * traceloop instrumentor's own vendor detection (`_detectVendorFromURL`)
9
+ * recognizes neither domain and falls back to `openai`, so every such call
10
+ * would otherwise be billed and grouped as OpenAI. One instrumentor per vendor
11
+ * re-stamps the detected provider for a client whose base URL matches,
12
+ * resolving the resource to its underlying client the same way
13
+ * `OpenAIBedrockMantleInstrumentor` does (`_detectVendorFromURL` is sometimes
14
+ * called with the resource, e.g. `Chat.Completions`, instead of the client).
15
+ *
16
+ * The embeddings endpoint is not covered here: traceloop does not wrap it, so
17
+ * weflayr's own `get_embeddings_telemetry.ts` detects the vendor itself,
18
+ * through `isProviderBaseURL` below.
19
+ *
20
+ * A `/v1/responses` call is attributed differently: its span keeps
21
+ * `gen_ai.provider.name` as "openai" and gets `weflayr.billing_provider` set to
22
+ * the vendor's name instead. The worker parses a span by its
23
+ * `gen_ai.provider.name` and prices it by its billing provider, and a vendor's
24
+ * Responses API counts its usage as OpenAI's does (xAI's counts the reasoning
25
+ * tokens inside the output tokens, unlike its Chat Completions API), so the
26
+ * payload is parsed as OpenAI's and the call priced as the vendor's.
27
+ */
28
+ export interface OpenAICompatibleProvider {
29
+ providerName: string;
30
+ hostnames: Set<string>;
31
+ hostnameSuffix: string;
32
+ }
33
+ export declare const ALIBABA_PROVIDER: OpenAICompatibleProvider;
34
+ export declare const SPACEXAI_PROVIDER: OpenAICompatibleProvider;
35
+ /** The openai client a traceloop call was made through: the client itself, or
36
+ * the resource (e.g. `Chat.Completions`) whose `_client` it is. */
37
+ export declare function resolveClient(client: any): any;
38
+ /** Whether a client base URL points at this vendor, e.g.
39
+ * (SPACEXAI_PROVIDER, 'https://api.x.ai/v1') -> true. */
40
+ export declare function isProviderBaseURL(provider: OpenAICompatibleProvider, baseURL: unknown): boolean;
41
+ export declare class OpenAICompatibleProviderInstrumentor {
42
+ readonly provider: OpenAICompatibleProvider;
43
+ private _patched;
44
+ constructor(provider: OpenAICompatibleProvider);
45
+ instrument(instrumentation: any): void;
46
+ private _billResponsesCallsToProvider;
47
+ uninstrument(): void;
48
+ }
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ /**
3
+ * Vendor detection for the OpenAI chat path of vendors served over an
4
+ * OpenAI-compatible API.
5
+ *
6
+ * Alibaba Cloud Model Studio (DashScope) serves Qwen and other models, and xAI
7
+ * serves the Grok models, over an OpenAI-compatible API, so both are reached
8
+ * with the stock `openai` SDK pointed at the vendor's own base URL. The
9
+ * traceloop instrumentor's own vendor detection (`_detectVendorFromURL`)
10
+ * recognizes neither domain and falls back to `openai`, so every such call
11
+ * would otherwise be billed and grouped as OpenAI. One instrumentor per vendor
12
+ * re-stamps the detected provider for a client whose base URL matches,
13
+ * resolving the resource to its underlying client the same way
14
+ * `OpenAIBedrockMantleInstrumentor` does (`_detectVendorFromURL` is sometimes
15
+ * called with the resource, e.g. `Chat.Completions`, instead of the client).
16
+ *
17
+ * The embeddings endpoint is not covered here: traceloop does not wrap it, so
18
+ * weflayr's own `get_embeddings_telemetry.ts` detects the vendor itself,
19
+ * through `isProviderBaseURL` below.
20
+ *
21
+ * A `/v1/responses` call is attributed differently: its span keeps
22
+ * `gen_ai.provider.name` as "openai" and gets `weflayr.billing_provider` set to
23
+ * the vendor's name instead. The worker parses a span by its
24
+ * `gen_ai.provider.name` and prices it by its billing provider, and a vendor's
25
+ * Responses API counts its usage as OpenAI's does (xAI's counts the reasoning
26
+ * tokens inside the output tokens, unlike its Chat Completions API), so the
27
+ * payload is parsed as OpenAI's and the call priced as the vendor's.
28
+ */
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.OpenAICompatibleProviderInstrumentor = exports.SPACEXAI_PROVIDER = exports.ALIBABA_PROVIDER = void 0;
31
+ exports.resolveClient = resolveClient;
32
+ exports.isProviderBaseURL = isProviderBaseURL;
33
+ // traceloop's internal tag for the Responses request/response shape, the value
34
+ // it passes to `startSpan`.
35
+ const RESPONSES_TYPE = 'responses';
36
+ const PROVIDER_ATTR = 'gen_ai.provider.name';
37
+ const BILLING_PROVIDER_ATTR = 'weflayr.billing_provider';
38
+ const OPENAI_PROVIDER_NAME = 'openai';
39
+ exports.ALIBABA_PROVIDER = {
40
+ providerName: 'alibaba',
41
+ hostnames: new Set(['dashscope.aliyuncs.com', 'dashscope-intl.aliyuncs.com']),
42
+ hostnameSuffix: '.maas.aliyuncs.com',
43
+ };
44
+ exports.SPACEXAI_PROVIDER = {
45
+ providerName: 'spacexai',
46
+ hostnames: new Set(['api.x.ai']),
47
+ hostnameSuffix: '.api.x.ai',
48
+ };
49
+ /** The openai client a traceloop call was made through: the client itself, or
50
+ * the resource (e.g. `Chat.Completions`) whose `_client` it is. */
51
+ function resolveClient(client) {
52
+ return client && !client.baseURL && client._client ? client._client : client;
53
+ }
54
+ /** Whether a client base URL points at this vendor, e.g.
55
+ * (SPACEXAI_PROVIDER, 'https://api.x.ai/v1') -> true. */
56
+ function isProviderBaseURL(provider, baseURL) {
57
+ if (typeof baseURL !== 'string' || !baseURL)
58
+ return false;
59
+ let hostname;
60
+ try {
61
+ hostname = new URL(baseURL).hostname.toLowerCase();
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ return provider.hostnames.has(hostname) || hostname.endsWith(provider.hostnameSuffix);
67
+ }
68
+ class OpenAICompatibleProviderInstrumentor {
69
+ provider;
70
+ // [instrumentation, methodName, originalFunction] for uninstrument().
71
+ _patched = [];
72
+ constructor(provider) {
73
+ this.provider = provider;
74
+ }
75
+ instrument(instrumentation) {
76
+ if (!instrumentation)
77
+ return;
78
+ const original = instrumentation._detectVendorFromURL;
79
+ if (typeof original !== 'function')
80
+ return;
81
+ const provider = this.provider;
82
+ instrumentation._detectVendorFromURL = function detectVendorWithProvider(client) {
83
+ const resolved = resolveClient(client);
84
+ if (isProviderBaseURL(provider, resolved?.baseURL)) {
85
+ return { provider: provider.providerName };
86
+ }
87
+ return original.call(this, resolved);
88
+ };
89
+ this._patched.push([instrumentation, '_detectVendorFromURL', original]);
90
+ this._billResponsesCallsToProvider(instrumentation);
91
+ }
92
+ // A Responses span of this vendor is parsed as OpenAI's and billed to the
93
+ // vendor (see the module comment).
94
+ _billResponsesCallsToProvider(instrumentation) {
95
+ const original = instrumentation.startSpan;
96
+ if (typeof original !== 'function')
97
+ return;
98
+ const provider = this.provider;
99
+ instrumentation.startSpan = function startSpanBilledToProvider(args) {
100
+ const span = original.call(this, args);
101
+ try {
102
+ if (args?.type === RESPONSES_TYPE &&
103
+ span?.isRecording?.() &&
104
+ isProviderBaseURL(provider, resolveClient(args.client)?.baseURL)) {
105
+ span.setAttribute(PROVIDER_ATTR, OPENAI_PROVIDER_NAME);
106
+ span.setAttribute(BILLING_PROVIDER_ATTR, provider.providerName);
107
+ }
108
+ }
109
+ catch {
110
+ // Never break span creation over an enrichment failure.
111
+ }
112
+ return span;
113
+ };
114
+ this._patched.push([instrumentation, 'startSpan', original]);
115
+ }
116
+ uninstrument() {
117
+ for (const [owner, methodName, original] of this._patched) {
118
+ owner[methodName] = original;
119
+ }
120
+ this._patched = [];
121
+ }
122
+ }
123
+ exports.OpenAICompatibleProviderInstrumentor = OpenAICompatibleProviderInstrumentor;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * `reasoning_content` output-part capture for the OpenAI chat path.
3
+ *
4
+ * OpenLLMetry's Python instrumentor already renders a message's
5
+ * `reasoning_content` field as a `{type: "reasoning"}` output part natively (see
6
+ * `get_bedrock_mantle_telemetry.py`'s docstring), so any OpenAI-compatible
7
+ * provider returning that field just works there. `@traceloop/instrumentation-openai`
8
+ * (Node) has no equivalent at all - reasoning models routed through it (Alibaba's
9
+ * explicit `enable_thinking`, confirmed 2026-09-21; DeepSeek and others use the
10
+ * same convention) would otherwise record an output message missing their whole
11
+ * chain of thought. This brings Node to parity, independently of
12
+ * `OpenAIBedrockMantleInstrumentor`'s `reasoning` (no `_content` suffix) handling
13
+ * for Mantle's own open-weight models - a response carries one spelling or the
14
+ * other, never both, so the two complements never collide.
15
+ *
16
+ * Mirrors `OpenAIBedrockMantleInstrumentor`'s reasoning mechanics exactly (same
17
+ * `_endSpan` / `_streamingWrapPromise` hooks, same "insert ahead of the first
18
+ * non-text part" ordering), reading `reasoning_content` instead of `reasoning`.
19
+ */
20
+ export declare class OpenAIReasoningContentInstrumentor {
21
+ private _patched;
22
+ instrument(instrumentation: any): void;
23
+ /** Accumulate `delta.reasoning_content` across the streamed chunks. */
24
+ private _patchStreamedReasoning;
25
+ /** Add the reasoning part as the output message is written. */
26
+ private _patchOutputMessages;
27
+ uninstrument(): void;
28
+ }
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ /**
3
+ * `reasoning_content` output-part capture for the OpenAI chat path.
4
+ *
5
+ * OpenLLMetry's Python instrumentor already renders a message's
6
+ * `reasoning_content` field as a `{type: "reasoning"}` output part natively (see
7
+ * `get_bedrock_mantle_telemetry.py`'s docstring), so any OpenAI-compatible
8
+ * provider returning that field just works there. `@traceloop/instrumentation-openai`
9
+ * (Node) has no equivalent at all - reasoning models routed through it (Alibaba's
10
+ * explicit `enable_thinking`, confirmed 2026-09-21; DeepSeek and others use the
11
+ * same convention) would otherwise record an output message missing their whole
12
+ * chain of thought. This brings Node to parity, independently of
13
+ * `OpenAIBedrockMantleInstrumentor`'s `reasoning` (no `_content` suffix) handling
14
+ * for Mantle's own open-weight models - a response carries one spelling or the
15
+ * other, never both, so the two complements never collide.
16
+ *
17
+ * Mirrors `OpenAIBedrockMantleInstrumentor`'s reasoning mechanics exactly (same
18
+ * `_endSpan` / `_streamingWrapPromise` hooks, same "insert ahead of the first
19
+ * non-text part" ordering), reading `reasoning_content` instead of `reasoning`.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.OpenAIReasoningContentInstrumentor = void 0;
23
+ const OUTPUT_MESSAGES_ATTR = 'gen_ai.output.messages';
24
+ // Reasoning streamed so far, per open span. Streamed deltas arrive while the
25
+ // stream is consumed, but the output message is only built when the span ends,
26
+ // so the two hooks below meet here.
27
+ const _streamedReasoningBySpan = new WeakMap();
28
+ function _completeResponseReasoning(result) {
29
+ const reasoning = result?.choices?.[0]?.message?.reasoning_content;
30
+ return typeof reasoning === 'string' && reasoning ? reasoning : undefined;
31
+ }
32
+ /** The parts array with the reasoning inserted ahead of the first non-text part
33
+ * (tool calls, audio, ...), so text and refusal keep leading it. */
34
+ function _withReasoningPart(parts, reasoning) {
35
+ const reasoningPart = { type: 'reasoning', content: reasoning };
36
+ const firstNonTextIndex = parts.findIndex((part) => part?.type !== 'text' && part?.type !== 'refusal');
37
+ if (firstNonTextIndex === -1)
38
+ return [...parts, reasoningPart];
39
+ return [...parts.slice(0, firstNonTextIndex), reasoningPart, ...parts.slice(firstNonTextIndex)];
40
+ }
41
+ /** The serialized output messages with the reasoning part added, or null when the
42
+ * value isn't the expected JSON array of messages. */
43
+ function _addReasoningToOutputMessages(rawOutputMessages, reasoning) {
44
+ if (typeof rawOutputMessages !== 'string')
45
+ return null;
46
+ let messages;
47
+ try {
48
+ messages = JSON.parse(rawOutputMessages);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ if (!Array.isArray(messages) || messages.length === 0)
54
+ return null;
55
+ const [first, ...rest] = messages;
56
+ if (!first || !Array.isArray(first.parts))
57
+ return null;
58
+ return JSON.stringify([{ ...first, parts: _withReasoningPart(first.parts, reasoning) }, ...rest]);
59
+ }
60
+ class OpenAIReasoningContentInstrumentor {
61
+ // [owner, methodName, originalFunction] for uninstrument().
62
+ _patched = [];
63
+ // No-op (and never throws) if the instrumentor's internals have changed shape —
64
+ // the worst case is a missing chain of thought, never a crash.
65
+ instrument(instrumentation) {
66
+ if (!instrumentation)
67
+ return;
68
+ this._patchStreamedReasoning(instrumentation);
69
+ this._patchOutputMessages(instrumentation);
70
+ }
71
+ /** Accumulate `delta.reasoning_content` across the streamed chunks. */
72
+ _patchStreamedReasoning(instrumentation) {
73
+ const original = instrumentation._streamingWrapPromise;
74
+ if (typeof original !== 'function')
75
+ return;
76
+ instrumentation._streamingWrapPromise = function streamingWrapWithReasoningContent(streamArgs) {
77
+ const span = streamArgs && streamArgs.span;
78
+ const wrappedStream = original.call(this, streamArgs);
79
+ if (!span)
80
+ return wrappedStream;
81
+ return (async function* accumulateReasoningContent() {
82
+ for await (const chunk of wrappedStream) {
83
+ const reasoning = chunk?.choices?.[0]?.delta?.reasoning_content;
84
+ if (typeof reasoning === 'string' && reasoning) {
85
+ _streamedReasoningBySpan.set(span, (_streamedReasoningBySpan.get(span) ?? '') + reasoning);
86
+ }
87
+ yield chunk;
88
+ }
89
+ })();
90
+ };
91
+ this._patched.push([instrumentation, '_streamingWrapPromise', original]);
92
+ }
93
+ /** Add the reasoning part as the output message is written. */
94
+ _patchOutputMessages(instrumentation) {
95
+ const original = instrumentation._endSpan;
96
+ if (typeof original !== 'function')
97
+ return;
98
+ instrumentation._endSpan = function endSpanWithReasoningContent(args) {
99
+ const span = args && args.span;
100
+ const reasoning = _completeResponseReasoning(args && args.result) ??
101
+ (span ? _streamedReasoningBySpan.get(span) : undefined);
102
+ if (!span || !reasoning)
103
+ return original.call(this, args);
104
+ // The original builds the output messages and then ends the span, so the
105
+ // part is grafted on as the attribute is written rather than afterwards.
106
+ const previousSetAttribute = span.setAttribute;
107
+ span.setAttribute = function setAttributeWithReasoningContent(key, value) {
108
+ if (key === OUTPUT_MESSAGES_ATTR) {
109
+ value = _addReasoningToOutputMessages(value, reasoning) ?? value;
110
+ }
111
+ return previousSetAttribute.call(this, key, value);
112
+ };
113
+ try {
114
+ return original.call(this, args);
115
+ }
116
+ finally {
117
+ span.setAttribute = previousSetAttribute;
118
+ _streamedReasoningBySpan.delete(span);
119
+ }
120
+ };
121
+ this._patched.push([instrumentation, '_endSpan', original]);
122
+ }
123
+ uninstrument() {
124
+ for (const [owner, methodName, original] of this._patched) {
125
+ owner[methodName] = original;
126
+ }
127
+ this._patched = [];
128
+ }
129
+ }
130
+ exports.OpenAIReasoningContentInstrumentor = OpenAIReasoningContentInstrumentor;