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.
- package/dist/package.json +3 -3
- package/dist/src/get-data-uri-mime-type.d.ts +8 -0
- package/dist/src/get-data-uri-mime-type.js +18 -0
- package/dist/src/instrumentation/anthropic/get_cache_config_telemetry.d.ts +3 -1
- package/dist/src/instrumentation/anthropic/get_cache_config_telemetry.js +17 -10
- package/dist/src/instrumentation/bedrock/get_cache_config_telemetry.js +47 -15
- package/dist/src/instrumentation/bedrock/get_input_message_content_telemetry.js +23 -16
- package/dist/src/instrumentation/cohere/get_input_message_content_telemetry.d.ts +2 -3
- package/dist/src/instrumentation/cohere/get_input_message_content_telemetry.js +6 -10
- package/dist/src/instrumentation/google/get_cache_operations_telemetry.js +7 -3
- package/dist/src/instrumentation/mistral/get_input_message_content_telemetry.d.ts +1 -1
- package/dist/src/instrumentation/mistral/get_input_message_content_telemetry.js +5 -8
- package/dist/src/instrumentation/openai/get_cache_config_telemetry.d.ts +5 -3
- package/dist/src/instrumentation/openai/get_cache_config_telemetry.js +12 -8
- package/dist/src/instrumentation/openai/get_file_part_mime_type_telemetry.d.ts +5 -4
- package/dist/src/instrumentation/openai/get_file_part_mime_type_telemetry.js +14 -10
- package/dist/src/instrumentation/openai/get_reasoning_effort_telemetry.d.ts +16 -0
- package/dist/src/instrumentation/openai/get_reasoning_effort_telemetry.js +49 -0
- package/dist/src/instrumentation/openai/get_responses_telemetry.d.ts +7 -7
- package/dist/src/instrumentation/openai/get_responses_telemetry.js +11 -11
- package/dist/src/instrumentation/openai/set_telemetry_attributes.d.ts +2 -1
- package/dist/src/instrumentation/openai/set_telemetry_attributes.js +4 -1
- package/dist/src/instrumentation/shared/format_cache_breakpoint_location.d.ts +10 -0
- package/dist/src/instrumentation/shared/format_cache_breakpoint_location.js +21 -0
- package/dist/src/otel/span-processor.js +0 -4
- package/package.json +3 -3
- package/dist/src/otel/strip-media-payloads.d.ts +0 -26
- 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.
|
|
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.
|
|
35
|
-
"@opentelemetry/instrumentation": "^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)
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
54
|
-
const
|
|
55
|
-
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
69
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
|
31
|
-
//
|
|
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
|
|
48
|
-
if (
|
|
49
|
-
parts.push(
|
|
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
|
|
6
|
-
*
|
|
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
|
|
7
|
-
*
|
|
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
|
|
26
|
-
* hosted image keeps its url
|
|
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
|
-
|
|
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
|
|
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
|
|
50
|
-
//
|
|
51
|
-
parts.push({
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
42
|
-
*
|
|
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
|
-
|
|
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]
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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]
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
-
|
|
61
|
+
content.forEach((block, blockIndex) => {
|
|
59
62
|
if (!block || typeof block !== 'object')
|
|
60
|
-
|
|
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
|
|
66
|
+
breakpoints.push({ location });
|
|
63
67
|
}
|
|
64
68
|
const cacheControl = block.cache_control;
|
|
65
69
|
if (cacheControl && typeof cacheControl === 'object') {
|
|
66
|
-
const entry = { location
|
|
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
|
|
8
|
-
* the caller sent (`data:application/pdf;base64,...`), and the
|
|
9
|
-
*
|
|
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
|
|
9
|
-
* the caller sent (`data:application/pdf;base64,...`), and the
|
|
10
|
-
*
|
|
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
|
|
28
|
-
function
|
|
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.
|
|
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
|
|
59
|
+
const blobPart = getBlockBlobPart(block);
|
|
59
60
|
const part = asObject(parts[blockIndex]);
|
|
60
|
-
if (
|
|
61
|
+
if (blobPart === null || part?.type !== 'blob')
|
|
61
62
|
return;
|
|
62
|
-
part.mime_type
|
|
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
|
|
13
|
-
* cannot
|
|
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
|
|
38
|
-
* `{type: "blob", mime_type}`
|
|
39
|
-
* `{type: "file", file_id, filename}`, so an inline PDF pins the same
|
|
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
|
|
42
|
-
*
|
|
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
|
|
14
|
-
* cannot
|
|
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
|
|
39
|
-
* `{type: "blob", mime_type}`
|
|
40
|
-
* `{type: "file", file_id, filename}`, so an inline PDF pins the same
|
|
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
|
|
43
|
-
*
|
|
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
|
|
127
|
-
if (
|
|
128
|
-
return
|
|
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.
|
|
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.
|
|
35
|
-
"@opentelemetry/instrumentation": "^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
|
-
}
|