weflayr 0.29.0 → 0.30.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 (28) hide show
  1. package/dist/package.json +3 -3
  2. package/dist/src/get-data-uri-mime-type.d.ts +8 -0
  3. package/dist/src/get-data-uri-mime-type.js +18 -0
  4. package/dist/src/instrumentation/anthropic/get_cache_config_telemetry.d.ts +3 -1
  5. package/dist/src/instrumentation/anthropic/get_cache_config_telemetry.js +17 -10
  6. package/dist/src/instrumentation/bedrock/get_cache_config_telemetry.js +47 -15
  7. package/dist/src/instrumentation/bedrock/get_input_message_content_telemetry.js +23 -16
  8. package/dist/src/instrumentation/cohere/get_input_message_content_telemetry.d.ts +2 -3
  9. package/dist/src/instrumentation/cohere/get_input_message_content_telemetry.js +6 -10
  10. package/dist/src/instrumentation/google/get_cache_operations_telemetry.js +7 -3
  11. package/dist/src/instrumentation/mistral/get_input_message_content_telemetry.d.ts +1 -1
  12. package/dist/src/instrumentation/mistral/get_input_message_content_telemetry.js +5 -8
  13. package/dist/src/instrumentation/openai/get_cache_config_telemetry.d.ts +5 -3
  14. package/dist/src/instrumentation/openai/get_cache_config_telemetry.js +12 -8
  15. package/dist/src/instrumentation/openai/get_file_part_mime_type_telemetry.d.ts +5 -4
  16. package/dist/src/instrumentation/openai/get_file_part_mime_type_telemetry.js +14 -10
  17. package/dist/src/instrumentation/openai/get_reasoning_effort_telemetry.d.ts +16 -0
  18. package/dist/src/instrumentation/openai/get_reasoning_effort_telemetry.js +49 -0
  19. package/dist/src/instrumentation/openai/get_responses_telemetry.d.ts +7 -7
  20. package/dist/src/instrumentation/openai/get_responses_telemetry.js +11 -11
  21. package/dist/src/instrumentation/openai/set_telemetry_attributes.d.ts +2 -1
  22. package/dist/src/instrumentation/openai/set_telemetry_attributes.js +4 -1
  23. package/dist/src/instrumentation/shared/format_cache_breakpoint_location.d.ts +10 -0
  24. package/dist/src/instrumentation/shared/format_cache_breakpoint_location.js +21 -0
  25. package/dist/src/otel/span-processor.js +0 -4
  26. package/package.json +3 -3
  27. package/dist/src/otel/strip-media-payloads.d.ts +0 -26
  28. package/dist/src/otel/strip-media-payloads.js +0 -135
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weflayr",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "Weflayr JS/TS SDK — instrument any LLM client",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
@@ -31,8 +31,8 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@opentelemetry/api": "^1.9.1",
34
- "@opentelemetry/exporter-trace-otlp-http": "^0.222.0",
35
- "@opentelemetry/instrumentation": "^0.222.0",
34
+ "@opentelemetry/exporter-trace-otlp-http": "^0.223.0",
35
+ "@opentelemetry/instrumentation": "^0.223.0",
36
36
  "@opentelemetry/resources": "^2.8.0",
37
37
  "@opentelemetry/sdk-trace-base": "^2.8.0",
38
38
  "@opentelemetry/sdk-trace-node": "^2.8.0",
@@ -15,3 +15,11 @@
15
15
  * `"https://example.com/cat.png"` -> `null`.
16
16
  */
17
17
  export declare function getDataUriMimeType(uri: string): string | null;
18
+ /**
19
+ * The `blob` part holding the bytes of a base64 `data:` URI, or null when the
20
+ * string is not one. The ingestion API moves the bytes to S3.
21
+ *
22
+ * e.g. `"data:image/png;base64,AAA"` ->
23
+ * `{ type: "blob", mime_type: "image/png", content: "AAA" }`.
24
+ */
25
+ export declare function getDataUriBlobPart(uri: string): Record<string, string> | null;
@@ -10,6 +10,7 @@
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.getDataUriMimeType = getDataUriMimeType;
13
+ exports.getDataUriBlobPart = getDataUriBlobPart;
13
14
  const DATA_URI_PREFIX = 'data:';
14
15
  /**
15
16
  * The mime type of a `data:<mime_type>[;base64],<payload>` URI, or null when
@@ -23,3 +24,20 @@ function getDataUriMimeType(uri) {
23
24
  return null;
24
25
  return uri.slice(DATA_URI_PREFIX.length).split(';')[0].split(',')[0];
25
26
  }
27
+ /**
28
+ * The `blob` part holding the bytes of a base64 `data:` URI, or null when the
29
+ * string is not one. The ingestion API moves the bytes to S3.
30
+ *
31
+ * e.g. `"data:image/png;base64,AAA"` ->
32
+ * `{ type: "blob", mime_type: "image/png", content: "AAA" }`.
33
+ */
34
+ function getDataUriBlobPart(uri) {
35
+ const mimeType = getDataUriMimeType(uri);
36
+ const separatorIndex = uri.indexOf(',');
37
+ if (mimeType === null ||
38
+ separatorIndex === -1 ||
39
+ !uri.slice(0, separatorIndex).endsWith(';base64')) {
40
+ return null;
41
+ }
42
+ return { type: 'blob', mime_type: mimeType, content: uri.slice(separatorIndex + 1) };
43
+ }
@@ -13,7 +13,9 @@
13
13
  * paths) to stamp every breakpoint found in the request as
14
14
  * `weflayr.cache.breakpoints`: a JSON list of `{location, ttl}` where location
15
15
  * is `system`, `messages[i]`, `tools[i]` or `auto_last_block` (the top-level
16
- * parameter) and `ttl` is the raw cache_control string, omitted when the
16
+ * parameter), followed by `.content[j]` for a breakpoint on a block that is not
17
+ * the last of its system prompt or message (`formatCacheBreakpointLocation`),
18
+ * and `ttl` is the raw cache_control string, omitted when the
17
19
  * breakpoint sets none; the normalizer owns the TTL vocabulary.
18
20
  */
19
21
  export interface CacheBreakpoint {
@@ -14,12 +14,15 @@
14
14
  * paths) to stamp every breakpoint found in the request as
15
15
  * `weflayr.cache.breakpoints`: a JSON list of `{location, ttl}` where location
16
16
  * is `system`, `messages[i]`, `tools[i]` or `auto_last_block` (the top-level
17
- * parameter) and `ttl` is the raw cache_control string, omitted when the
17
+ * parameter), followed by `.content[j]` for a breakpoint on a block that is not
18
+ * the last of its system prompt or message (`formatCacheBreakpointLocation`),
19
+ * and `ttl` is the raw cache_control string, omitted when the
18
20
  * breakpoint sets none; the normalizer owns the TTL vocabulary.
19
21
  */
20
22
  Object.defineProperty(exports, "__esModule", { value: true });
21
23
  exports.AnthropicCacheConfigInstrumentor = void 0;
22
24
  exports.extractCacheBreakpoints = extractCacheBreakpoints;
25
+ const format_cache_breakpoint_location_1 = require("../shared/format_cache_breakpoint_location");
23
26
  const BREAKPOINTS_ATTR = 'weflayr.cache.breakpoints';
24
27
  // A breakpoint as location + the raw cache_control `ttl` string, omitted when
25
28
  // the request sets none.
@@ -27,22 +30,26 @@ function _breakpointEntry(location, cacheControl) {
27
30
  const ttl = cacheControl && cacheControl.ttl;
28
31
  return ttl ? { location, ttl: String(ttl) } : { location };
29
32
  }
30
- function _cacheControlsInBlocks(blocks) {
33
+ // A breakpoint per block of `blocks` carrying a `cache_control`.
34
+ function _breakpointsInBlocks(blocks, messageLocation) {
31
35
  if (!Array.isArray(blocks))
32
36
  return [];
33
- return blocks.map((block) => block && block.cache_control).filter(Boolean);
37
+ const breakpoints = [];
38
+ blocks.forEach((block, blockIndex) => {
39
+ const cacheControl = block && block.cache_control;
40
+ if (!cacheControl)
41
+ return;
42
+ const location = (0, format_cache_breakpoint_location_1.formatCacheBreakpointLocation)(messageLocation, blockIndex, blocks.length);
43
+ breakpoints.push(_breakpointEntry(location, cacheControl));
44
+ });
45
+ return breakpoints;
34
46
  }
35
47
  // Every `cache_control` breakpoint in the request, as location + the raw
36
48
  // `ttl` string (omitted when the breakpoint sets none).
37
49
  function extractCacheBreakpoints(params) {
38
- const breakpoints = [];
39
- for (const cacheControl of _cacheControlsInBlocks(params.system)) {
40
- breakpoints.push(_breakpointEntry('system', cacheControl));
41
- }
50
+ const breakpoints = _breakpointsInBlocks(params.system, 'system');
42
51
  (params.messages || []).forEach((message, index) => {
43
- for (const cacheControl of _cacheControlsInBlocks(message && message.content)) {
44
- breakpoints.push(_breakpointEntry(`messages[${index}]`, cacheControl));
45
- }
52
+ breakpoints.push(..._breakpointsInBlocks(message && message.content, `messages[${index}]`));
46
53
  });
47
54
  (params.tools || []).forEach((tool, index) => {
48
55
  if (tool && tool.cache_control) {
@@ -29,6 +29,7 @@ exports.BedrockCacheConfigInstrumentor = void 0;
29
29
  exports.recordCommandRegion = recordCommandRegion;
30
30
  exports.extractBedrockCacheBreakpoints = extractBedrockCacheBreakpoints;
31
31
  const get_cache_config_telemetry_1 = require("../anthropic/get_cache_config_telemetry");
32
+ const format_cache_breakpoint_location_1 = require("../shared/format_cache_breakpoint_location");
32
33
  const BREAKPOINTS_ATTR = 'weflayr.cache.breakpoints';
33
34
  const PROVIDER_REGION_ATTR = 'weflayr.provider_region';
34
35
  // The region of the client each pending command was sent with. Keyed by the
@@ -46,13 +47,46 @@ function _cachePointEntry(location, cachePoint) {
46
47
  const ttl = cachePoint && cachePoint.ttl;
47
48
  return ttl ? { location, ttl: String(ttl) } : { location };
48
49
  }
49
- // The first `cachePoint` among a list of blocks (attached to a content block,
50
- // or standing alone as its own block), or undefined.
51
- function _cachePointOfBlocks(blocks) {
50
+ function _isStandaloneCachePoint(block) {
51
+ return (block != null &&
52
+ typeof block === 'object' &&
53
+ Object.keys(block).length === 1 &&
54
+ block.cachePoint !== undefined);
55
+ }
56
+ // A breakpoint per `cachePoint` among a list of blocks. One attached to a
57
+ // content block marks that block, and one standing alone as its own block
58
+ // marks the content block before it, the standalone ones being no content
59
+ // block of their own. A standalone one opening the list caches the prompt up
60
+ // to `previousLocation`, the end of what comes before the list, and is left
61
+ // out when nothing does, since it then caches nothing.
62
+ // Example: [text, cachePoint, text] in messages[0] ->
63
+ // [{ location: "messages[0].content[0]" }].
64
+ function _cachePointBreakpoints(blocks, messageLocation, previousLocation) {
52
65
  if (!Array.isArray(blocks))
53
- return undefined;
54
- const block = blocks.find((entry) => entry && entry.cachePoint != null);
55
- return block && block.cachePoint;
66
+ return [];
67
+ const numContentBlocks = blocks.filter((block) => !_isStandaloneCachePoint(block)).length;
68
+ const breakpoints = [];
69
+ let numContentBlocksSoFar = 0;
70
+ for (const block of blocks) {
71
+ if (!_isStandaloneCachePoint(block))
72
+ numContentBlocksSoFar += 1;
73
+ const cachePoint = block && block.cachePoint;
74
+ if (cachePoint == null)
75
+ continue;
76
+ const markedBlockIndex = numContentBlocksSoFar - 1;
77
+ let location;
78
+ if (markedBlockIndex >= 0) {
79
+ location = (0, format_cache_breakpoint_location_1.formatCacheBreakpointLocation)(messageLocation, markedBlockIndex, numContentBlocks);
80
+ }
81
+ else if (previousLocation !== undefined) {
82
+ location = previousLocation;
83
+ }
84
+ else {
85
+ continue;
86
+ }
87
+ breakpoints.push(_cachePointEntry(location, cachePoint));
88
+ }
89
+ return breakpoints;
56
90
  }
57
91
  // Every cache breakpoint in an InvokeModel request body, as location + the
58
92
  // raw `ttl` string (omitted when the breakpoint sets none): the
@@ -60,17 +94,15 @@ function _cachePointOfBlocks(blocks) {
60
94
  // (which never overlap in one body).
61
95
  function extractBedrockCacheBreakpoints(body) {
62
96
  const breakpoints = (0, get_cache_config_telemetry_1.extractCacheBreakpoints)(body);
63
- const systemCachePoint = _cachePointOfBlocks(body.system);
64
- if (systemCachePoint != null) {
65
- breakpoints.push(_cachePointEntry('system', systemCachePoint));
66
- }
97
+ const tools = (body.toolConfig && body.toolConfig.tools) || [];
98
+ // The prompt is cached in the order tools, system, messages.
99
+ const toolsLocation = tools.length ? `tools[${tools.length - 1}]` : undefined;
100
+ breakpoints.push(..._cachePointBreakpoints(body.system, 'system', toolsLocation));
101
+ const locationBeforeMessages = body.system && body.system.length ? 'system' : toolsLocation;
67
102
  (body.messages || []).forEach((message, index) => {
68
- const cachePoint = _cachePointOfBlocks(message && message.content);
69
- if (cachePoint != null) {
70
- breakpoints.push(_cachePointEntry(`messages[${index}]`, cachePoint));
71
- }
103
+ const previousLocation = index ? `messages[${index - 1}]` : locationBeforeMessages;
104
+ breakpoints.push(..._cachePointBreakpoints(message && message.content, `messages[${index}]`, previousLocation));
72
105
  });
73
- const tools = (body.toolConfig && body.toolConfig.tools) || [];
74
106
  tools.forEach((tool, index) => {
75
107
  if (tool && tool.cachePoint != null) {
76
108
  breakpoints.push(_cachePointEntry(`tools[${index}]`, tool.cachePoint));
@@ -12,24 +12,31 @@
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.BedrockInputMessageContentInstrumentor = void 0;
14
14
  const _MEDIA_BLOCK_TYPES = ['image', 'document', 'video', 'audio'];
15
- // A byte-less stub for a media content block, null when the block is not media.
16
- // Anthropic-on-Bedrock names the media under `type` and carries its mime type in
17
- // `source.media_type`; Nova puts the media under its own key
18
- // (`{"image": {"format": "png", "source": {...}}}`) and states no mime type.
19
- function _getMediaStubPart(block) {
15
+ function _buildBlobPart(mimeType, base64Bytes) {
16
+ return typeof base64Bytes === 'string'
17
+ ? { type: 'blob', mime_type: mimeType, content: base64Bytes }
18
+ : { type: 'blob', mime_type: mimeType };
19
+ }
20
+ // A blob part holding the bytes of a media content block, null when the block
21
+ // is not media. Anthropic-on-Bedrock names the media under `type` and carries
22
+ // its base64 in `source.data` and its mime type in `source.media_type`; Nova puts
23
+ // the media under its own key
24
+ // (`{"image": {"format": "png", "source": {"bytes": ...}}}`) and states no mime
25
+ // type.
26
+ function _getMediaBlobPart(block) {
20
27
  if (_MEDIA_BLOCK_TYPES.includes(block.type)) {
21
- const source = block.source;
22
- const mediaType = source && typeof source === 'object' ? source.media_type : null;
23
- return { type: 'blob', mime_type: String(mediaType || '') };
28
+ const source = block.source && typeof block.source === 'object' ? block.source : {};
29
+ return _buildBlobPart(String(source.media_type || ''), source.data);
24
30
  }
25
- if (_MEDIA_BLOCK_TYPES.some((media) => block[media] && typeof block[media] === 'object')) {
26
- return { type: 'blob', mime_type: '' };
31
+ const novaMedia = _MEDIA_BLOCK_TYPES.find((media) => block[media] && typeof block[media] === 'object');
32
+ if (novaMedia) {
33
+ const source = block[novaMedia].source;
34
+ return _buildBlobPart('', source && typeof source === 'object' ? source.bytes : undefined);
27
35
  }
28
36
  return null;
29
37
  }
30
- // Text blocks keep their text; media blocks become a byte-less stub, so the
31
- // payload stays off the span while the analyses reading these messages still see
32
- // that the call carried content the stored text does not.
38
+ // Text blocks keep their text; media blocks become blob parts holding their
39
+ // bytes.
33
40
  function _contentToParts(content) {
34
41
  if (typeof content === 'string') {
35
42
  return [{ type: 'text', content }];
@@ -44,9 +51,9 @@ function _contentToParts(content) {
44
51
  parts.push({ type: 'text', content: block.text });
45
52
  continue;
46
53
  }
47
- const mediaStubPart = _getMediaStubPart(block);
48
- if (mediaStubPart !== null)
49
- parts.push(mediaStubPart);
54
+ const mediaBlobPart = _getMediaBlobPart(block);
55
+ if (mediaBlobPart !== null)
56
+ parts.push(mediaBlobPart);
50
57
  }
51
58
  return parts;
52
59
  }
@@ -2,9 +2,8 @@
2
2
  * Input message content for Cohere v2 chat spans.
3
3
  *
4
4
  * Mirror of the Python complement's message shaping: a v2 request's messages in
5
- * the standard role+parts shape, with media blocks reduced to a byte-less stub
6
- * so the payload stays off the span while the analyses reading these messages
7
- * still see that the call carried content the stored text does not.
5
+ * the standard role+parts shape, with an inline image as a `blob` part holding
6
+ * its bytes, which the ingestion API moves to S3.
8
7
  *
9
8
  * Python wraps the OpenLLMetry instrumentor to stamp this (that instrumentor
10
9
  * covers v2 but records its messages in a shape OTel rejects). The Node
@@ -3,9 +3,8 @@
3
3
  * Input message content for Cohere v2 chat spans.
4
4
  *
5
5
  * Mirror of the Python complement's message shaping: a v2 request's messages in
6
- * the standard role+parts shape, with media blocks reduced to a byte-less stub
7
- * so the payload stays off the span while the analyses reading these messages
8
- * still see that the call carried content the stored text does not.
6
+ * the standard role+parts shape, with an inline image as a `blob` part holding
7
+ * its bytes, which the ingestion API moves to S3.
9
8
  *
10
9
  * Python wraps the OpenLLMetry instrumentor to stamp this (that instrumentor
11
10
  * covers v2 but records its messages in a shape OTel rejects). The Node
@@ -22,8 +21,8 @@ function asObject(value) {
22
21
  : null;
23
22
  }
24
23
  /**
25
- * An image block as a part: an inline data URI keeps its mime type only, a
26
- * hosted image keeps its url (which is not a payload).
24
+ * An image block as a part: an inline data URI becomes a blob holding its
25
+ * bytes, a hosted image keeps its url.
27
26
  */
28
27
  function getImagePart(block) {
29
28
  // The JS client spells the key imageUrl; the wire (and the Python client)
@@ -31,14 +30,11 @@ function getImagePart(block) {
31
30
  const rawImageUrl = block.imageUrl ?? block.image_url;
32
31
  const imageUrl = asObject(rawImageUrl);
33
32
  const url = String((imageUrl ? imageUrl.url : rawImageUrl) ?? '');
34
- const mimeType = (0, get_data_uri_mime_type_1.getDataUriMimeType)(url);
35
- if (mimeType === null)
36
- return { type: 'uri', uri: url };
37
- return { type: 'blob', mime_type: mimeType };
33
+ return (0, get_data_uri_mime_type_1.getDataUriBlobPart)(url) ?? { type: 'uri', uri: url };
38
34
  }
39
35
  /**
40
36
  * A v2 message's `content` as normalized parts. A plain string is one text
41
- * part; a list keeps its text, turns images into a byte-less stub, and keeps
37
+ * part; a list keeps its text, turns images into blob parts, and keeps
42
38
  * any other block under its own type - naming it is what lets the analyses tell
43
39
  * a text-only prompt from one whose content was not stored.
44
40
  */
@@ -46,9 +46,13 @@ function _contentToMessage(content) {
46
46
  parts.push({ type: 'text', content: rawPart.text });
47
47
  }
48
48
  else if (rawPart && rawPart.inlineData) {
49
- // Non-text parts (e.g. audio) become a byte-less blob stub so span
50
- // attributes never carry raw media payloads.
51
- parts.push({ type: 'blob', mime_type: rawPart.inlineData.mimeType || '' });
49
+ // Non-text parts (e.g. audio) become a blob part holding the base64
50
+ // bytes, which the ingestion API moves to S3.
51
+ parts.push({
52
+ type: 'blob',
53
+ mime_type: rawPart.inlineData.mimeType || '',
54
+ content: rawPart.inlineData.data,
55
+ });
52
56
  }
53
57
  }
54
58
  return { role, parts };
@@ -6,7 +6,7 @@
6
6
  * `gen_ai.input.messages` as a bare `{"type": "<block type>"}` (the fallback
7
7
  * branch of its `formatting.ts`), so a PDF sent as `document_url` is stored
8
8
  * under a name with no mime type, where every other provider stores the
9
- * byte-less `{"type": "blob", "mime_type": "application/pdf"}`.
9
+ * `{"type": "blob", "mime_type": "application/pdf", "content": <base64>}`.
10
10
  *
11
11
  * The mime type it lacks is in the request the caller sent: the block's url is
12
12
  * a `data:` URI holding the bytes. This complement walks the parts the tracing
@@ -7,7 +7,7 @@
7
7
  * `gen_ai.input.messages` as a bare `{"type": "<block type>"}` (the fallback
8
8
  * branch of its `formatting.ts`), so a PDF sent as `document_url` is stored
9
9
  * under a name with no mime type, where every other provider stores the
10
- * byte-less `{"type": "blob", "mime_type": "application/pdf"}`.
10
+ * `{"type": "blob", "mime_type": "application/pdf", "content": <base64>}`.
11
11
  *
12
12
  * The mime type it lacks is in the request the caller sent: the block's url is
13
13
  * a `data:` URI holding the bytes. This complement walks the parts the tracing
@@ -38,20 +38,17 @@ function getBlockUrl(block) {
38
38
  return String(url ?? '');
39
39
  }
40
40
  /**
41
- * The byte-less part for a content block carrying inline bytes, or null for a
42
- * block that carries none.
41
+ * The blob part for a content block carrying inline bytes, or null for a block
42
+ * that carries none.
43
43
  *
44
44
  * e.g. `{"type": "document_url", "document_url": "data:application/pdf;base64,AAA"}`
45
- * -> `{"type": "blob", "mime_type": "application/pdf"}`.
45
+ * -> `{"type": "blob", "mime_type": "application/pdf", "content": "AAA"}`.
46
46
  */
47
47
  function getMediaStub(rawBlock) {
48
48
  const block = asObject(rawBlock);
49
49
  if (block === null)
50
50
  return null;
51
- const mimeType = (0, get_data_uri_mime_type_1.getDataUriMimeType)(getBlockUrl(block));
52
- if (mimeType === null)
53
- return null;
54
- return { type: 'blob', mime_type: mimeType };
51
+ return (0, get_data_uri_mime_type_1.getDataUriBlobPart)(getBlockUrl(block));
55
52
  }
56
53
  /** True for the `{"type": "<block type>"}` the tracing falls back to on a
57
54
  * block it does not shape: a type and nothing else. */
@@ -13,9 +13,11 @@
13
13
  *
14
14
  * It covers the chat and the Responses endpoints alike: both take the same
15
15
  * params, and a marker sits on a content block of either `messages` (chat) or
16
- * `input` (Responses), recorded as `messages[i]` / `input[i]`. A Responses
17
- * request's `instructions` count as its item 0 when it sends them, as the
18
- * worker numbers every provider's system prompt.
16
+ * `input` (Responses), recorded as `messages[i]` / `input[i]`, followed by
17
+ * `.content[j]` for a marker on a block that is not the last of its message
18
+ * (`formatCacheBreakpointLocation`). A Responses request's `instructions` count
19
+ * as its item 0 when it sends them, as the worker numbers every provider's
20
+ * system prompt.
19
21
  *
20
22
  * Wrap `startSpan({ type, params })` (covering the streamed and non-streamed
21
23
  * paths) to stamp the values as `weflayr.cache.key` / `weflayr.cache.retention`
@@ -14,9 +14,11 @@
14
14
  *
15
15
  * It covers the chat and the Responses endpoints alike: both take the same
16
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.
17
+ * `input` (Responses), recorded as `messages[i]` / `input[i]`, followed by
18
+ * `.content[j]` for a marker on a block that is not the last of its message
19
+ * (`formatCacheBreakpointLocation`). A Responses request's `instructions` count
20
+ * as its item 0 when it sends them, as the worker numbers every provider's
21
+ * system prompt.
20
22
  *
21
23
  * Wrap `startSpan({ type, params })` (covering the streamed and non-streamed
22
24
  * paths) to stamp the values as `weflayr.cache.key` / `weflayr.cache.retention`
@@ -41,6 +43,7 @@
41
43
  */
42
44
  Object.defineProperty(exports, "__esModule", { value: true });
43
45
  exports.OpenAICacheConfigInstrumentor = void 0;
46
+ const format_cache_breakpoint_location_1 = require("../shared/format_cache_breakpoint_location");
44
47
  const CACHE_KEY_ATTR = 'weflayr.cache.key';
45
48
  const CACHE_RETENTION_ATTR = 'weflayr.cache.retention';
46
49
  const CACHE_MODE_ATTR = 'weflayr.cache.mode';
@@ -55,20 +58,21 @@ function breakpointsInItems(items, listName, firstIndex) {
55
58
  const content = item && item.content;
56
59
  if (!Array.isArray(content))
57
60
  return;
58
- for (const block of content) {
61
+ content.forEach((block, blockIndex) => {
59
62
  if (!block || typeof block !== 'object')
60
- continue;
63
+ return;
64
+ const location = (0, format_cache_breakpoint_location_1.formatCacheBreakpointLocation)(`${listName}[${index}]`, blockIndex, content.length);
61
65
  if ('prompt_cache_breakpoint' in block) {
62
- breakpoints.push({ location: `${listName}[${index}]` });
66
+ breakpoints.push({ location });
63
67
  }
64
68
  const cacheControl = block.cache_control;
65
69
  if (cacheControl && typeof cacheControl === 'object') {
66
- const entry = { location: `${listName}[${index}]` };
70
+ const entry = { location };
67
71
  if (cacheControl.ttl)
68
72
  entry.ttl = String(cacheControl.ttl);
69
73
  breakpoints.push(entry);
70
74
  }
71
- }
75
+ });
72
76
  });
73
77
  return breakpoints;
74
78
  }
@@ -4,9 +4,10 @@
4
4
  * The traceloop instrumentor maps a `file` content block to a blob part but
5
5
  * reads its mime type from `block.file.mime_type`, a field OpenAI's API does
6
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.
7
+ * `application/octet-stream`. The real mime type and the bytes are right there
8
+ * in the data URI the caller sent (`data:application/pdf;base64,...`), and the
9
+ * Python SDK keeps them, so this complement reads them back off the request and
10
+ * repairs the part.
10
11
  *
11
12
  * Wraps `startSpan({ type, params })`, which is where the instrumentor builds
12
13
  * `gen_ai.input.messages`, so the attribute is already on the span when this
@@ -15,7 +16,7 @@
15
16
  */
16
17
  /**
17
18
  * `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
+ * data URI given that URI's mime type and bytes, or null when there was nothing to
19
20
  * repair (so the instrumentor's own JSON is left untouched).
20
21
  */
21
22
  export declare function repairFileMimeTypes(inputMessagesJson: unknown, messages: unknown): string | null;
@@ -5,9 +5,10 @@
5
5
  * The traceloop instrumentor maps a `file` content block to a blob part but
6
6
  * reads its mime type from `block.file.mime_type`, a field OpenAI's API does
7
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.
8
+ * `application/octet-stream`. The real mime type and the bytes are right there
9
+ * in the data URI the caller sent (`data:application/pdf;base64,...`), and the
10
+ * Python SDK keeps them, so this complement reads them back off the request and
11
+ * repairs the part.
11
12
  *
12
13
  * Wraps `startSpan({ type, params })`, which is where the instrumentor builds
13
14
  * `gen_ai.input.messages`, so the attribute is already on the span when this
@@ -24,16 +25,16 @@ function asObject(value) {
24
25
  ? value
25
26
  : null;
26
27
  }
27
- /** The mime type of the data URI a request content block carries, if any. */
28
- function getBlockMimeType(block) {
28
+ /** The blob part holding the data URI a request content block carries, if any. */
29
+ function getBlockBlobPart(block) {
29
30
  const fileBlock = asObject(asObject(block)?.file);
30
31
  if (fileBlock === null)
31
32
  return null;
32
- return (0, get_data_uri_mime_type_1.getDataUriMimeType)(String(fileBlock.file_data ?? ''));
33
+ return (0, get_data_uri_mime_type_1.getDataUriBlobPart)(String(fileBlock.file_data ?? ''));
33
34
  }
34
35
  /**
35
36
  * `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
+ * data URI given that URI's mime type and bytes, or null when there was nothing to
37
38
  * repair (so the instrumentor's own JSON is left untouched).
38
39
  */
39
40
  function repairFileMimeTypes(inputMessagesJson, messages) {
@@ -55,11 +56,14 @@ function repairFileMimeTypes(inputMessagesJson, messages) {
55
56
  if (!Array.isArray(content) || !Array.isArray(parts))
56
57
  return;
57
58
  content.forEach((block, blockIndex) => {
58
- const mimeType = getBlockMimeType(block);
59
+ const blobPart = getBlockBlobPart(block);
59
60
  const part = asObject(parts[blockIndex]);
60
- if (mimeType === null || part?.type !== 'blob' || part.mime_type === mimeType)
61
+ if (blobPart === null || part?.type !== 'blob')
61
62
  return;
62
- part.mime_type = mimeType;
63
+ if (part.mime_type === blobPart.mime_type && part.content === blobPart.content)
64
+ return;
65
+ part.mime_type = blobPart.mime_type;
66
+ part.content = blobPart.content;
63
67
  repaired = true;
64
68
  });
65
69
  });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Reasoning effort capture for the OpenAI chat endpoint.
3
+ *
4
+ * A chat request asks a model to think with `reasoning_effort` ("low",
5
+ * "medium", ...). OpenLLMetry's Python instrumentor stamps it as
6
+ * `gen_ai.request.reasoning_effort`; `@traceloop/instrumentation-openai` (Node)
7
+ * records nothing, so the stored call would not say how hard the model was
8
+ * asked to think. This stamps the same attribute when the span opens, in the
9
+ * `startSpan({ params })` hook the structured-output complement uses. The
10
+ * Responses endpoint's `reasoning.effort` is stamped by `get_responses_telemetry.ts`.
11
+ */
12
+ export declare class OpenAIReasoningEffortInstrumentor {
13
+ private _patched;
14
+ instrument(instrumentation: any): void;
15
+ uninstrument(): void;
16
+ }
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ /**
3
+ * Reasoning effort capture for the OpenAI chat endpoint.
4
+ *
5
+ * A chat request asks a model to think with `reasoning_effort` ("low",
6
+ * "medium", ...). OpenLLMetry's Python instrumentor stamps it as
7
+ * `gen_ai.request.reasoning_effort`; `@traceloop/instrumentation-openai` (Node)
8
+ * records nothing, so the stored call would not say how hard the model was
9
+ * asked to think. This stamps the same attribute when the span opens, in the
10
+ * `startSpan({ params })` hook the structured-output complement uses. The
11
+ * Responses endpoint's `reasoning.effort` is stamped by `get_responses_telemetry.ts`.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.OpenAIReasoningEffortInstrumentor = void 0;
15
+ const REASONING_EFFORT_ATTR = 'gen_ai.request.reasoning_effort';
16
+ class OpenAIReasoningEffortInstrumentor {
17
+ // [owner, methodName, originalFunction] for uninstrument().
18
+ _patched = [];
19
+ // No-op (and never throws) if the instrumentor's internals have changed shape:
20
+ // the worst case is a missing reasoning effort, never a crash.
21
+ instrument(instrumentation) {
22
+ if (!instrumentation)
23
+ return;
24
+ const startSpanOriginal = instrumentation.startSpan;
25
+ if (typeof startSpanOriginal !== 'function')
26
+ return;
27
+ instrumentation.startSpan = function startSpanWithReasoningEffort(request) {
28
+ const span = startSpanOriginal.call(this, request);
29
+ try {
30
+ const reasoningEffort = request?.params?.reasoning_effort;
31
+ if (typeof reasoningEffort === 'string' && reasoningEffort && span?.isRecording?.()) {
32
+ span.setAttribute(REASONING_EFFORT_ATTR, reasoningEffort);
33
+ }
34
+ }
35
+ catch {
36
+ // Telemetry must never break the app's LLM call.
37
+ }
38
+ return span;
39
+ };
40
+ this._patched.push([instrumentation, 'startSpan', startSpanOriginal]);
41
+ }
42
+ uninstrument() {
43
+ for (const [owner, methodName, original] of this._patched) {
44
+ owner[methodName] = original;
45
+ }
46
+ this._patched = [];
47
+ }
48
+ }
49
+ exports.OpenAIReasoningEffortInstrumentor = OpenAIReasoningEffortInstrumentor;
@@ -9,8 +9,8 @@
9
9
  * 1. Input messages. Upstream stringifies the whole `input` into a single
10
10
  * `{role: "user", parts: [{type: "text", content: "<JSON>"}]}` part. That
11
11
  * loses every role and part type, and it buries an inline image or
12
- * document's base64 inside a text string, where `strip-media-payloads`
13
- * cannot see it and the payload would reach the queue. This rebuilds the
12
+ * document's base64 inside a text string, where the ingestion API
13
+ * cannot find it. This rebuilds the
14
14
  * per-role, per-part shape the Python instrumentor emits.
15
15
  * 2. Response side. Upstream records only the output text, so a tool call or
16
16
  * a reasoning summary is lost, the finish reason is never `tool_call`, and
@@ -34,12 +34,12 @@
34
34
  * (`weflayr.document_search.requests`).
35
35
  *
36
36
  * Inline media is the one deliberate divergence from the Python instrumentor's
37
- * shape: an `input_file` carrying `file_data` becomes the byte-less
38
- * `{type: "blob", mime_type}` stub rather than upstream's
39
- * `{type: "file", file_id, filename}`, so an inline PDF pins the same stub here
37
+ * shape: an `input_file` carrying `file_data` becomes the
38
+ * `{type: "blob", mime_type, content}` part rather than upstream's
39
+ * `{type: "file", file_id, filename}`, so an inline PDF pins the same part here
40
40
  * as it does on the chat endpoint. The Python complement makes the same repair.
41
- * An `input_image` keeps its `data:` URI in a `uri` part, which the shared
42
- * `strip-media-payloads` step already rewrites to that stub.
41
+ * An `input_image` keeps its `data:` URI in a `uri` part, which the ingestion
42
+ * API moves to S3 like any other inline media.
43
43
  */
44
44
  import { type Span } from '@opentelemetry/api';
45
45
  type JsonObject = Record<string, any>;
@@ -10,8 +10,8 @@
10
10
  * 1. Input messages. Upstream stringifies the whole `input` into a single
11
11
  * `{role: "user", parts: [{type: "text", content: "<JSON>"}]}` part. That
12
12
  * loses every role and part type, and it buries an inline image or
13
- * document's base64 inside a text string, where `strip-media-payloads`
14
- * cannot see it and the payload would reach the queue. This rebuilds the
13
+ * document's base64 inside a text string, where the ingestion API
14
+ * cannot find it. This rebuilds the
15
15
  * per-role, per-part shape the Python instrumentor emits.
16
16
  * 2. Response side. Upstream records only the output text, so a tool call or
17
17
  * a reasoning summary is lost, the finish reason is never `tool_call`, and
@@ -35,12 +35,12 @@
35
35
  * (`weflayr.document_search.requests`).
36
36
  *
37
37
  * Inline media is the one deliberate divergence from the Python instrumentor's
38
- * shape: an `input_file` carrying `file_data` becomes the byte-less
39
- * `{type: "blob", mime_type}` stub rather than upstream's
40
- * `{type: "file", file_id, filename}`, so an inline PDF pins the same stub here
38
+ * shape: an `input_file` carrying `file_data` becomes the
39
+ * `{type: "blob", mime_type, content}` part rather than upstream's
40
+ * `{type: "file", file_id, filename}`, so an inline PDF pins the same part here
41
41
  * as it does on the chat endpoint. The Python complement makes the same repair.
42
- * An `input_image` keeps its `data:` URI in a `uri` part, which the shared
43
- * `strip-media-payloads` step already rewrites to that stub.
42
+ * An `input_image` keeps its `data:` URI in a `uri` part, which the ingestion
43
+ * API moves to S3 like any other inline media.
44
44
  */
45
45
  Object.defineProperty(exports, "__esModule", { value: true });
46
46
  exports.OpenAIResponsesInstrumentor = void 0;
@@ -106,7 +106,7 @@ function parseArguments(rawArguments) {
106
106
  *
107
107
  * Example: `{type: "input_text", text: "hi"}` -> `{type: "text", content: "hi"}`;
108
108
  * `{type: "input_file", filename: "a.pdf", file_data: "data:application/pdf;base64,..."}`
109
- * -> `{type: "blob", mime_type: "application/pdf"}`.
109
+ * -> `{type: "blob", mime_type: "application/pdf", content: "..."}`.
110
110
  */
111
111
  function buildInputPart(block) {
112
112
  if (!isJsonObject(block)) {
@@ -123,9 +123,9 @@ function buildInputPart(block) {
123
123
  return { type: 'file', modality: 'image', file_id: block.file_id ?? null };
124
124
  }
125
125
  if (blockType === 'file' || blockType === 'input_file' || blockType === 'output_file') {
126
- const mimeType = (0, get_data_uri_mime_type_1.getDataUriMimeType)(String(block.file_data ?? ''));
127
- if (mimeType !== null) {
128
- return { type: 'blob', mime_type: mimeType };
126
+ const blobPart = (0, get_data_uri_mime_type_1.getDataUriBlobPart)(String(block.file_data ?? ''));
127
+ if (blobPart !== null) {
128
+ return blobPart;
129
129
  }
130
130
  return {
131
131
  type: 'file',
@@ -19,7 +19,8 @@
19
19
  * OpenAI-compatible API (Alibaba Model Studio, xAI), and
20
20
  * - the billing of the calls to Vertex AI's OpenAI-compatible endpoint, and
21
21
  * - a `reasoning_content` output part for any provider that returns one, and
22
- * - the service tier the call was served in (flex, priority, ...).
22
+ * - the service tier the call was served in (flex, priority, ...), and
23
+ * - the reasoning effort a chat request asks for.
23
24
  */
24
25
  export declare class OpenAITelemetryInstrumentor {
25
26
  private _subInstrumentors;
@@ -20,7 +20,8 @@
20
20
  * OpenAI-compatible API (Alibaba Model Studio, xAI), and
21
21
  * - the billing of the calls to Vertex AI's OpenAI-compatible endpoint, and
22
22
  * - a `reasoning_content` output part for any provider that returns one, and
23
- * - the service tier the call was served in (flex, priority, ...).
23
+ * - the service tier the call was served in (flex, priority, ...), and
24
+ * - the reasoning effort a chat request asks for.
24
25
  */
25
26
  Object.defineProperty(exports, "__esModule", { value: true });
26
27
  exports.OpenAITelemetryInstrumentor = void 0;
@@ -33,6 +34,7 @@ const get_openai_compatible_provider_telemetry_1 = require("./get_openai_compati
33
34
  const get_responses_telemetry_1 = require("./get_responses_telemetry");
34
35
  const get_embeddings_telemetry_1 = require("./get_embeddings_telemetry");
35
36
  const get_reasoning_content_telemetry_1 = require("./get_reasoning_content_telemetry");
37
+ const get_reasoning_effort_telemetry_1 = require("./get_reasoning_effort_telemetry");
36
38
  const get_service_tier_telemetry_1 = require("./get_service_tier_telemetry");
37
39
  const get_streaming_usage_1 = require("./get_streaming_usage");
38
40
  const get_cache_and_reasoning_telemetry_1 = require("./get_cache_and_reasoning_telemetry");
@@ -57,6 +59,7 @@ class OpenAITelemetryInstrumentor {
57
59
  new get_vertex_openai_compatible_telemetry_1.VertexOpenAICompatibleInstrumentor(),
58
60
  new get_reasoning_content_telemetry_1.OpenAIReasoningContentInstrumentor(),
59
61
  new get_service_tier_telemetry_1.OpenAIServiceTierInstrumentor(),
62
+ new get_reasoning_effort_telemetry_1.OpenAIReasoningEffortInstrumentor(),
60
63
  new get_structured_output_schema_telemetry_1.TraceloopStructuredOutputSchemaInstrumentor(get_structured_output_schema_telemetry_2.readStructuredOutputSchema),
61
64
  ];
62
65
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The location of a cache breakpoint marked on one block of a message, shared
3
+ * by every provider complement stamping `weflayr.cache.breakpoints`.
4
+ *
5
+ * A breakpoint caches the prompt up to the end of the block it marks. On a
6
+ * message's last block, that is the end of the message, written as the message
7
+ * alone (`messages[1]`). On an earlier block, the prefix ends inside the
8
+ * message, so the location also names the block (`messages[1].content[0]`).
9
+ */
10
+ export declare function formatCacheBreakpointLocation(messageLocation: string, blockIndex: number, numBlocks: number): string;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ /**
3
+ * The location of a cache breakpoint marked on one block of a message, shared
4
+ * by every provider complement stamping `weflayr.cache.breakpoints`.
5
+ *
6
+ * A breakpoint caches the prompt up to the end of the block it marks. On a
7
+ * message's last block, that is the end of the message, written as the message
8
+ * alone (`messages[1]`). On an earlier block, the prefix ends inside the
9
+ * message, so the location also names the block (`messages[1].content[0]`).
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.formatCacheBreakpointLocation = formatCacheBreakpointLocation;
13
+ // Where a breakpoint on block `blockIndex` of a message of `numBlocks` blocks
14
+ // ends the cached prefix.
15
+ // Example: ("messages[1]", 0, 2) -> "messages[1].content[0]",
16
+ // ("messages[1]", 1, 2) -> "messages[1]".
17
+ function formatCacheBreakpointLocation(messageLocation, blockIndex, numBlocks) {
18
+ if (blockIndex === numBlocks - 1)
19
+ return messageLocation;
20
+ return `${messageLocation}.content[${blockIndex}]`;
21
+ }
@@ -20,7 +20,6 @@ const api_1 = require("@opentelemetry/api");
20
20
  const sdk_trace_base_1 = require("@opentelemetry/sdk-trace-base");
21
21
  const propagation_1 = require("./propagation");
22
22
  const span_filter_1 = require("./span-filter");
23
- const strip_media_payloads_1 = require("./strip-media-payloads");
24
23
  const logger_1 = require("../logger");
25
24
  // setup({captureMessageContent: ...}) records the choice in this env var (the
26
25
  // official OTel GenAI one). Instrumentors gate content on their own knobs
@@ -115,9 +114,6 @@ class WeflayrSpanProcessor {
115
114
  delete span.attributes[key];
116
115
  }
117
116
  }
118
- else {
119
- (0, strip_media_payloads_1.stripMediaPayloads)(span.attributes);
120
- }
121
117
  if (this._mask) {
122
118
  try {
123
119
  const masked = this._mask(span.attributes);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weflayr",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "Weflayr JS/TS SDK — instrument any LLM client",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
@@ -31,8 +31,8 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@opentelemetry/api": "^1.9.1",
34
- "@opentelemetry/exporter-trace-otlp-http": "^0.222.0",
35
- "@opentelemetry/instrumentation": "^0.222.0",
34
+ "@opentelemetry/exporter-trace-otlp-http": "^0.223.0",
35
+ "@opentelemetry/instrumentation": "^0.223.0",
36
36
  "@opentelemetry/resources": "^2.8.0",
37
37
  "@opentelemetry/sdk-trace-base": "^2.8.0",
38
38
  "@opentelemetry/sdk-trace-node": "^2.8.0",
@@ -1,26 +0,0 @@
1
- /**
2
- * Remove media payloads (base64 images, audio, documents) from the message
3
- * content span attributes, right before export.
4
- *
5
- * An inline image, audio clip or document reaches the span in one of five shapes,
6
- * depending on the instrumentor: a `blob` part carrying the base64 bytes in
7
- * `content` (OpenAI chat completions, Anthropic), a `uri` part whose uri is a
8
- * `data:` URI holding the same bytes (OpenAI Responses), Anthropic's own content
9
- * block with the bytes under `source.data` (Bedrock), an OpenAI `file` block
10
- * with the bytes under `file.file_data`, an OpenAI Responses `input_audio` block
11
- * kept whole under `content` with the bytes under `content.input_audio.data`. A single
12
- * image is hundreds of kilobytes, so keeping it would make every event on the
13
- * queue that much bigger for nothing: the payload is never read back.
14
- *
15
- * Each of those becomes `{"type": "blob", "mime_type": "<mime_type>"}`, the same
16
- * byte-less stub the google, cohere and bedrock instrumentation complements
17
- * build themselves: the analyses reading these messages still see that the call
18
- * carried content the stored text does not. A `uri` part pointing at a hosted
19
- * image is a reference, not a payload, and is kept as is.
20
- */
21
- import type { OtelSpanAttributes } from './span-processor';
22
- /**
23
- * Rewrite in place every message content attribute of `attributes` that carries
24
- * media bytes.
25
- */
26
- export declare function stripMediaPayloads(attributes: OtelSpanAttributes): void;
@@ -1,135 +0,0 @@
1
- "use strict";
2
- /**
3
- * Remove media payloads (base64 images, audio, documents) from the message
4
- * content span attributes, right before export.
5
- *
6
- * An inline image, audio clip or document reaches the span in one of five shapes,
7
- * depending on the instrumentor: a `blob` part carrying the base64 bytes in
8
- * `content` (OpenAI chat completions, Anthropic), a `uri` part whose uri is a
9
- * `data:` URI holding the same bytes (OpenAI Responses), Anthropic's own content
10
- * block with the bytes under `source.data` (Bedrock), an OpenAI `file` block
11
- * with the bytes under `file.file_data`, an OpenAI Responses `input_audio` block
12
- * kept whole under `content` with the bytes under `content.input_audio.data`. A single
13
- * image is hundreds of kilobytes, so keeping it would make every event on the
14
- * queue that much bigger for nothing: the payload is never read back.
15
- *
16
- * Each of those becomes `{"type": "blob", "mime_type": "<mime_type>"}`, the same
17
- * byte-less stub the google, cohere and bedrock instrumentation complements
18
- * build themselves: the analyses reading these messages still see that the call
19
- * carried content the stored text does not. A `uri` part pointing at a hosted
20
- * image is a reference, not a payload, and is kept as is.
21
- */
22
- Object.defineProperty(exports, "__esModule", { value: true });
23
- exports.stripMediaPayloads = stripMediaPayloads;
24
- const get_data_uri_mime_type_1 = require("../get-data-uri-mime-type");
25
- // The message content attributes whose JSON holds parts: the first two are
26
- // `[{role, parts}]`, `gen_ai.system_instructions` is a flat parts array.
27
- const MESSAGE_PARTS_ATTRIBUTES = [
28
- 'gen_ai.input.messages',
29
- 'gen_ai.output.messages',
30
- 'gen_ai.system_instructions',
31
- ];
32
- function isJsonObject(value) {
33
- return typeof value === 'object' && value !== null && !Array.isArray(value);
34
- }
35
- function stripPart(part) {
36
- if (!isJsonObject(part))
37
- return part;
38
- if (part.type === 'blob') {
39
- return { type: 'blob', mime_type: String(part.mime_type ?? '') };
40
- }
41
- if (part.type === 'uri') {
42
- const mimeType = (0, get_data_uri_mime_type_1.getDataUriMimeType)(String(part.uri ?? ''));
43
- if (mimeType !== null)
44
- return { type: 'blob', mime_type: mimeType };
45
- }
46
- // Anthropic's own content block, which the Bedrock instrumentor copies onto
47
- // the span as is: the bytes sit under source.data whatever the block's type
48
- // says ("image", "document", "video", "audio").
49
- const source = part.source;
50
- if (isJsonObject(source) && source.data != null) {
51
- return { type: 'blob', mime_type: String(source.media_type ?? '') };
52
- }
53
- // OpenAI's file block (a PDF sent to chat completions). On Node the
54
- // instrumentor already maps it to a blob part (see the openai complement that
55
- // repairs its mime type), so this is the guard for the day it stops, and the
56
- // mirror of the branch the Python SDK needs.
57
- const fileBlock = part.file;
58
- if (isJsonObject(fileBlock)) {
59
- const mimeType = (0, get_data_uri_mime_type_1.getDataUriMimeType)(String(fileBlock.file_data ?? ''));
60
- if (mimeType !== null)
61
- return { type: 'blob', mime_type: mimeType };
62
- }
63
- // An input block the OpenAI Responses instrumentor does not recognize is
64
- // recorded as `{"type": "<block type>", "content": <the block itself>}`, so an
65
- // `input_audio` block puts its base64 clip under `content.input_audio.data`.
66
- const unrecognizedBlock = part.content;
67
- const inlineAudio = isJsonObject(unrecognizedBlock) ? unrecognizedBlock.input_audio : null;
68
- if (isJsonObject(inlineAudio) && inlineAudio.data != null) {
69
- const audioFormat = inlineAudio.format;
70
- return {
71
- type: 'blob',
72
- mime_type: audioFormat ? `audio/${audioFormat}` : 'audio/unknown',
73
- };
74
- }
75
- // Mistral's document block. Its own tracing names the block by its type and
76
- // drops the payload, so what arrives is usually `{"type": "document_url"}`
77
- // already; this is what keeps the bytes off the queue if it stops dropping
78
- // them.
79
- const documentMimeType = (0, get_data_uri_mime_type_1.getDataUriMimeType)(String(part.document_url ?? ''));
80
- if (documentMimeType !== null)
81
- return { type: 'blob', mime_type: documentMimeType };
82
- return part;
83
- }
84
- /**
85
- * `items` with `stripItem` applied to each of them, or `items` itself when
86
- * every item came back unchanged - an attribute carrying no media then keeps
87
- * the instrumentor's exact JSON string.
88
- */
89
- function stripList(items, stripItem) {
90
- const stripped = items.map(stripItem);
91
- return stripped.every((item, index) => item === items[index]) ? items : stripped;
92
- }
93
- /**
94
- * One entry of a parts attribute: a message whose `parts` are stripped, or a
95
- * bare part when the attribute is a flat parts array.
96
- */
97
- function stripPartsEntry(entry) {
98
- if (isJsonObject(entry) && Array.isArray(entry.parts)) {
99
- const parts = stripList(entry.parts, stripPart);
100
- return parts === entry.parts ? entry : { ...entry, parts };
101
- }
102
- return stripPart(entry);
103
- }
104
- /**
105
- * The attribute's JSON with the media stripped from each of its entries, or
106
- * null when there was nothing to strip.
107
- */
108
- function getStrippedAttribute(value) {
109
- if (typeof value !== 'string')
110
- return null;
111
- let entries;
112
- try {
113
- entries = JSON.parse(value);
114
- }
115
- catch {
116
- return null;
117
- }
118
- if (!Array.isArray(entries))
119
- return null;
120
- const stripped = stripList(entries, stripPartsEntry);
121
- return stripped === entries ? null : JSON.stringify(stripped);
122
- }
123
- /**
124
- * Rewrite in place every message content attribute of `attributes` that carries
125
- * media bytes.
126
- */
127
- function stripMediaPayloads(attributes) {
128
- for (const key of MESSAGE_PARTS_ATTRIBUTES) {
129
- if (key in attributes) {
130
- const stripped = getStrippedAttribute(attributes[key]);
131
- if (stripped !== null)
132
- attributes[key] = stripped;
133
- }
134
- }
135
- }