weflayr 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package.json +3 -3
- package/dist/src/auto-instrument.d.ts +4 -3
- package/dist/src/auto-instrument.js +18 -17
- package/dist/src/get-data-uri-mime-type.d.ts +17 -0
- package/dist/src/get-data-uri-mime-type.js +25 -0
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.js +5 -1
- package/dist/src/instrumentation/anthropic/get_streaming_accumulation_telemetry.d.ts +3 -1
- package/dist/src/instrumentation/anthropic/get_streaming_accumulation_telemetry.js +6 -3
- package/dist/src/instrumentation/anthropic/get_vertex_billing_telemetry.d.ts +32 -0
- package/dist/src/instrumentation/anthropic/get_vertex_billing_telemetry.js +100 -0
- package/dist/src/instrumentation/anthropic/get_web_search_telemetry.d.ts +43 -0
- package/dist/src/instrumentation/anthropic/get_web_search_telemetry.js +147 -0
- package/dist/src/instrumentation/anthropic/set_telemetry_attributes.d.ts +5 -2
- package/dist/src/instrumentation/anthropic/set_telemetry_attributes.js +17 -4
- package/dist/src/instrumentation/bedrock/get_cache_config_telemetry.d.ts +13 -1
- package/dist/src/instrumentation/bedrock/get_cache_config_telemetry.js +56 -1
- package/dist/src/instrumentation/bedrock/get_cache_telemetry.js +8 -5
- package/dist/src/instrumentation/bedrock/get_model_id_telemetry.d.ts +1 -1
- package/dist/src/instrumentation/bedrock/get_model_id_telemetry.js +1 -1
- package/dist/src/instrumentation/bedrock/get_openai_chat_format_telemetry.d.ts +45 -0
- package/dist/src/instrumentation/bedrock/get_openai_chat_format_telemetry.js +251 -0
- package/dist/src/instrumentation/bedrock/get_streaming_output_message_telemetry.d.ts +6 -5
- package/dist/src/instrumentation/bedrock/get_streaming_output_message_telemetry.js +15 -12
- package/dist/src/instrumentation/bedrock/get_tool_definitions_telemetry.d.ts +2 -2
- package/dist/src/instrumentation/bedrock/get_tool_definitions_telemetry.js +12 -4
- package/dist/src/instrumentation/bedrock/set_telemetry_attributes.d.ts +4 -2
- package/dist/src/instrumentation/bedrock/set_telemetry_attributes.js +10 -3
- package/dist/src/instrumentation/cohere/get_embeddings_telemetry.js +4 -6
- package/dist/src/instrumentation/cohere/get_input_message_content_telemetry.d.ts +23 -0
- package/dist/src/instrumentation/cohere/get_input_message_content_telemetry.js +105 -0
- package/dist/src/instrumentation/cohere/get_v2_chat_telemetry.d.ts +18 -0
- package/dist/src/instrumentation/cohere/get_v2_chat_telemetry.js +186 -0
- package/dist/src/instrumentation/cohere/set_telemetry_attributes.d.ts +3 -1
- package/dist/src/instrumentation/cohere/set_telemetry_attributes.js +5 -1
- package/dist/src/instrumentation/google/get_embeddings_telemetry.d.ts +8 -4
- package/dist/src/instrumentation/google/get_embeddings_telemetry.js +68 -35
- package/dist/src/instrumentation/google/get_provider_name.d.ts +2 -1
- package/dist/src/instrumentation/google/get_provider_name.js +2 -1
- package/dist/src/instrumentation/google/get_web_search_telemetry.d.ts +38 -0
- package/dist/src/instrumentation/google/get_web_search_telemetry.js +125 -0
- package/dist/src/instrumentation/google/set_telemetry_attributes.d.ts +2 -2
- package/dist/src/instrumentation/google/set_telemetry_attributes.js +6 -2
- package/dist/src/instrumentation/mistral/full_instrumentation.d.ts +60 -0
- package/dist/src/instrumentation/mistral/full_instrumentation.js +364 -0
- package/dist/src/instrumentation/mistral/get_input_message_content_telemetry.d.ts +35 -0
- package/dist/src/instrumentation/mistral/get_input_message_content_telemetry.js +113 -0
- package/dist/src/instrumentation/mistral/get_structured_output_schema_telemetry.d.ts +13 -0
- package/dist/src/instrumentation/mistral/get_structured_output_schema_telemetry.js +24 -0
- package/dist/src/instrumentation/mistral/get_vertex_telemetry.d.ts +54 -0
- package/dist/src/instrumentation/mistral/get_vertex_telemetry.js +153 -0
- package/dist/src/instrumentation/openai/get_bedrock_mantle_telemetry.d.ts +10 -0
- package/dist/src/instrumentation/openai/get_bedrock_mantle_telemetry.js +33 -0
- package/dist/src/instrumentation/openai/get_cache_and_reasoning_telemetry.d.ts +13 -6
- package/dist/src/instrumentation/openai/get_cache_and_reasoning_telemetry.js +17 -7
- package/dist/src/instrumentation/openai/get_cache_config_telemetry.d.ts +21 -4
- package/dist/src/instrumentation/openai/get_cache_config_telemetry.js +50 -11
- package/dist/src/instrumentation/openai/get_embeddings_telemetry.js +14 -6
- package/dist/src/instrumentation/openai/get_file_part_mime_type_telemetry.d.ts +26 -0
- package/dist/src/instrumentation/openai/get_file_part_mime_type_telemetry.js +100 -0
- package/dist/src/instrumentation/openai/get_openai_compatible_provider_telemetry.d.ts +48 -0
- package/dist/src/instrumentation/openai/get_openai_compatible_provider_telemetry.js +123 -0
- package/dist/src/instrumentation/openai/get_reasoning_content_telemetry.d.ts +28 -0
- package/dist/src/instrumentation/openai/get_reasoning_content_telemetry.js +130 -0
- package/dist/src/instrumentation/openai/get_responses_telemetry.d.ts +89 -0
- package/dist/src/instrumentation/openai/get_responses_telemetry.js +587 -0
- package/dist/src/instrumentation/openai/get_service_tier_telemetry.d.ts +20 -0
- package/dist/src/instrumentation/openai/get_service_tier_telemetry.js +84 -0
- package/dist/src/instrumentation/openai/get_structured_output_schema_telemetry.d.ts +7 -5
- package/dist/src/instrumentation/openai/get_structured_output_schema_telemetry.js +15 -8
- package/dist/src/instrumentation/openai/get_vertex_openai_compatible_telemetry.d.ts +23 -0
- package/dist/src/instrumentation/openai/get_vertex_openai_compatible_telemetry.js +69 -0
- package/dist/src/instrumentation/openai/get_web_search_telemetry.d.ts +74 -0
- package/dist/src/instrumentation/openai/get_web_search_telemetry.js +259 -0
- package/dist/src/instrumentation/openai/set_telemetry_attributes.d.ts +13 -1
- package/dist/src/instrumentation/openai/set_telemetry_attributes.js +33 -2
- package/dist/src/instrumentation/shared/get_structured_output_schema_telemetry.d.ts +1 -1
- package/dist/src/instrumentation/shared/get_structured_output_schema_telemetry.js +1 -1
- package/dist/src/instrumentation/shared/is_vertex_base_url.d.ts +17 -0
- package/dist/src/instrumentation/shared/is_vertex_base_url.js +40 -0
- package/dist/src/otel/propagation.d.ts +37 -10
- package/dist/src/otel/propagation.js +62 -22
- package/dist/src/otel/span-filter.d.ts +2 -0
- package/dist/src/otel/span-filter.js +10 -1
- package/dist/src/otel/span-processor.d.ts +1 -2
- package/dist/src/otel/span-processor.js +17 -20
- package/dist/src/otel/strip-media-payloads.d.ts +26 -0
- package/dist/src/otel/strip-media-payloads.js +135 -0
- package/dist/src/propagate-metadata-to-gateway.d.ts +37 -0
- package/dist/src/propagate-metadata-to-gateway.js +213 -0
- package/package.json +3 -3
- package/dist/src/instrumentation/vercel/full_instrumentation.d.ts +0 -3
- package/dist/src/instrumentation/vercel/full_instrumentation.js +0 -54
- package/dist/src/instrumentation/vercel/get_cache_creation_telemetry.d.ts +0 -22
- package/dist/src/instrumentation/vercel/get_cache_creation_telemetry.js +0 -66
- package/dist/src/instrumentation/vercel/get_structured_output_schema_telemetry.d.ts +0 -24
- package/dist/src/instrumentation/vercel/get_structured_output_schema_telemetry.js +0 -84
- package/dist/src/otel/vercel-ai-sdk-span-filter.d.ts +0 -12
- package/dist/src/otel/vercel-ai-sdk-span-filter.js +0 -88
|
@@ -19,6 +19,7 @@ const TRACER_NAME = 'weflayr.instrumentation.cohere_embeddings';
|
|
|
19
19
|
// Matches the gen_ai.system the cohere chat instrumentors set (Python + Node).
|
|
20
20
|
const SYSTEM = 'Cohere';
|
|
21
21
|
const EMBEDDINGS_OPERATION = 'embeddings';
|
|
22
|
+
const EMBEDDINGS_INPUT_ATTR = 'weflayr.embeddings.input';
|
|
22
23
|
function _recordError(span, error) {
|
|
23
24
|
span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error?.message ?? String(error) });
|
|
24
25
|
span.recordException(error);
|
|
@@ -32,13 +33,10 @@ function _wrapEmbed(owner, patched) {
|
|
|
32
33
|
if (request && request.model) {
|
|
33
34
|
span.setAttribute('gen_ai.request.model', request.model);
|
|
34
35
|
}
|
|
35
|
-
// The embedded texts
|
|
36
|
-
//
|
|
36
|
+
// The embedded texts, as passed; the worker shapes them. The span
|
|
37
|
+
// processor strips the attribute when content capture is off.
|
|
37
38
|
if (request && Array.isArray(request.texts) && request.texts.length) {
|
|
38
|
-
span.setAttribute(
|
|
39
|
-
role: 'user',
|
|
40
|
-
parts: [{ type: 'text', content: text }],
|
|
41
|
-
}))));
|
|
39
|
+
span.setAttribute(EMBEDDINGS_INPUT_ATTR, JSON.stringify(request.texts));
|
|
42
40
|
}
|
|
43
41
|
return Promise.resolve(original.call(this, request, ...rest)).then((response) => {
|
|
44
42
|
// Node SDK reports the billed input tokens under camelCase keys.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input message content for Cohere v2 chat spans.
|
|
3
|
+
*
|
|
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.
|
|
8
|
+
*
|
|
9
|
+
* Python wraps the OpenLLMetry instrumentor to stamp this (that instrumentor
|
|
10
|
+
* covers v2 but records its messages in a shape OTel rejects). The Node
|
|
11
|
+
* traceloop instrumentor covers only the v1 client, so on this side the shaping
|
|
12
|
+
* is called by weflayr's own v2 chat instrumentor
|
|
13
|
+
* (`get_v2_chat_telemetry.ts`) instead.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* The v2 request's messages in the role+parts shape.
|
|
17
|
+
*
|
|
18
|
+
* e.g. `messages=[{role: "user", content: [{type: "text", text: "hi"},
|
|
19
|
+
* {type: "image_url", image_url: {url: "data:image/png;base64,AAA"}}]}]`
|
|
20
|
+
* -> `[{role: "user", parts: [{type: "text", content: "hi"},
|
|
21
|
+
* {type: "blob", mime_type: "image/png"}]}]`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function extractInputMessages(request: unknown): Record<string, unknown>[];
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Input message content for Cohere v2 chat spans.
|
|
4
|
+
*
|
|
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.
|
|
9
|
+
*
|
|
10
|
+
* Python wraps the OpenLLMetry instrumentor to stamp this (that instrumentor
|
|
11
|
+
* covers v2 but records its messages in a shape OTel rejects). The Node
|
|
12
|
+
* traceloop instrumentor covers only the v1 client, so on this side the shaping
|
|
13
|
+
* is called by weflayr's own v2 chat instrumentor
|
|
14
|
+
* (`get_v2_chat_telemetry.ts`) instead.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.extractInputMessages = extractInputMessages;
|
|
18
|
+
const get_data_uri_mime_type_1 = require("../../get-data-uri-mime-type");
|
|
19
|
+
function asObject(value) {
|
|
20
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
21
|
+
? value
|
|
22
|
+
: null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
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).
|
|
27
|
+
*/
|
|
28
|
+
function getImagePart(block) {
|
|
29
|
+
// The JS client spells the key imageUrl; the wire (and the Python client)
|
|
30
|
+
// spell it image_url.
|
|
31
|
+
const rawImageUrl = block.imageUrl ?? block.image_url;
|
|
32
|
+
const imageUrl = asObject(rawImageUrl);
|
|
33
|
+
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 };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 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
|
|
42
|
+
* any other block under its own type - naming it is what lets the analyses tell
|
|
43
|
+
* a text-only prompt from one whose content was not stored.
|
|
44
|
+
*/
|
|
45
|
+
function contentToParts(content) {
|
|
46
|
+
if (typeof content === 'string')
|
|
47
|
+
return [{ type: 'text', content }];
|
|
48
|
+
const parts = [];
|
|
49
|
+
for (const rawBlock of Array.isArray(content) ? content : []) {
|
|
50
|
+
const block = asObject(rawBlock);
|
|
51
|
+
if (block === null)
|
|
52
|
+
continue;
|
|
53
|
+
if (block.type === 'text') {
|
|
54
|
+
parts.push({ type: 'text', content: String(block.text ?? '') });
|
|
55
|
+
}
|
|
56
|
+
else if (block.type === 'image_url') {
|
|
57
|
+
parts.push(getImagePart(block));
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
parts.push({ type: String(block.type ?? 'unknown') });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return parts;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* An assistant message's `tool_calls` as `tool_call` parts, so an agentic v2
|
|
67
|
+
* conversation replays with the exchange it was built on.
|
|
68
|
+
*/
|
|
69
|
+
function toolCallsToParts(toolCalls) {
|
|
70
|
+
const parts = [];
|
|
71
|
+
for (const rawToolCall of Array.isArray(toolCalls) ? toolCalls : []) {
|
|
72
|
+
const toolCall = asObject(rawToolCall);
|
|
73
|
+
if (toolCall === null)
|
|
74
|
+
continue;
|
|
75
|
+
const functionCall = asObject(toolCall.function) ?? {};
|
|
76
|
+
parts.push({
|
|
77
|
+
type: 'tool_call',
|
|
78
|
+
id: String(toolCall.id ?? ''),
|
|
79
|
+
name: String(functionCall.name ?? ''),
|
|
80
|
+
arguments: functionCall.arguments ?? {},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return parts;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The v2 request's messages in the role+parts shape.
|
|
87
|
+
*
|
|
88
|
+
* e.g. `messages=[{role: "user", content: [{type: "text", text: "hi"},
|
|
89
|
+
* {type: "image_url", image_url: {url: "data:image/png;base64,AAA"}}]}]`
|
|
90
|
+
* -> `[{role: "user", parts: [{type: "text", content: "hi"},
|
|
91
|
+
* {type: "blob", mime_type: "image/png"}]}]`.
|
|
92
|
+
*/
|
|
93
|
+
function extractInputMessages(request) {
|
|
94
|
+
const messages = asObject(request)?.messages;
|
|
95
|
+
const inputMessages = [];
|
|
96
|
+
for (const rawMessage of Array.isArray(messages) ? messages : []) {
|
|
97
|
+
const message = asObject(rawMessage);
|
|
98
|
+
if (message === null)
|
|
99
|
+
continue;
|
|
100
|
+
const parts = contentToParts(message.content);
|
|
101
|
+
parts.push(...toolCallsToParts(message.tool_calls ?? message.toolCalls));
|
|
102
|
+
inputMessages.push({ role: String(message.role ?? ''), parts });
|
|
103
|
+
}
|
|
104
|
+
return inputMessages;
|
|
105
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-written OTel instrumentor for the Cohere v2 chat endpoint.
|
|
3
|
+
*
|
|
4
|
+
* `@traceloop/instrumentation-cohere` wraps `CohereClient.prototype` only, so a
|
|
5
|
+
* call made through the v2 client (`CohereClientV2`, the only client that
|
|
6
|
+
* accepts images) produces no span at all on Node. OpenLLMetry's Python cohere
|
|
7
|
+
* instrumentor does cover v2, so on that side weflayr only corrects the
|
|
8
|
+
* messages it records; here weflayr owns the whole span. Both end up producing
|
|
9
|
+
* the same normalized row.
|
|
10
|
+
*
|
|
11
|
+
* Only the non-streaming `chat` is covered: `chatStream` has no span on Node
|
|
12
|
+
* yet.
|
|
13
|
+
*/
|
|
14
|
+
export declare class CohereV2ChatInstrumentor {
|
|
15
|
+
private _patched;
|
|
16
|
+
instrument(): void;
|
|
17
|
+
uninstrument(): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Hand-written OTel instrumentor for the Cohere v2 chat endpoint.
|
|
4
|
+
*
|
|
5
|
+
* `@traceloop/instrumentation-cohere` wraps `CohereClient.prototype` only, so a
|
|
6
|
+
* call made through the v2 client (`CohereClientV2`, the only client that
|
|
7
|
+
* accepts images) produces no span at all on Node. OpenLLMetry's Python cohere
|
|
8
|
+
* instrumentor does cover v2, so on that side weflayr only corrects the
|
|
9
|
+
* messages it records; here weflayr owns the whole span. Both end up producing
|
|
10
|
+
* the same normalized row.
|
|
11
|
+
*
|
|
12
|
+
* Only the non-streaming `chat` is covered: `chatStream` has no span on Node
|
|
13
|
+
* yet.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.CohereV2ChatInstrumentor = void 0;
|
|
17
|
+
const api_1 = require("@opentelemetry/api");
|
|
18
|
+
const get_input_message_content_telemetry_1 = require("./get_input_message_content_telemetry");
|
|
19
|
+
const logger_1 = require("../../logger");
|
|
20
|
+
const TRACER_NAME = 'weflayr.instrumentation.cohere_v2_chat';
|
|
21
|
+
// Marks the patched `chat` so a second autoInstrument('cohere') - a new
|
|
22
|
+
// instrumentor instance, and possibly a second copy of this module - does not
|
|
23
|
+
// wrap the wrapper and emit two spans per call.
|
|
24
|
+
const PATCHED_MARKER = Symbol.for('weflayr.cohere.v2.chat.patched');
|
|
25
|
+
// Matches the gen_ai.system the v1 chat instrumentors set (Python + Node).
|
|
26
|
+
const SYSTEM = 'Cohere';
|
|
27
|
+
const CHAT_OPERATION = 'chat';
|
|
28
|
+
function asObject(value) {
|
|
29
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
30
|
+
? value
|
|
31
|
+
: null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The response as one assistant message: its content blocks as text parts, the
|
|
35
|
+
* plan it writes before calling a tool as a reasoning part, and its tool calls
|
|
36
|
+
* as `tool_call` parts. Mirrors the Python complement's `build_output_messages`.
|
|
37
|
+
*/
|
|
38
|
+
function buildOutputMessages(response) {
|
|
39
|
+
const message = asObject(asObject(response)?.message);
|
|
40
|
+
if (message === null)
|
|
41
|
+
return [];
|
|
42
|
+
const parts = [];
|
|
43
|
+
if (message.toolPlan) {
|
|
44
|
+
parts.push({ type: 'reasoning', content: String(message.toolPlan) });
|
|
45
|
+
}
|
|
46
|
+
for (const rawBlock of Array.isArray(message.content) ? message.content : []) {
|
|
47
|
+
const block = asObject(rawBlock);
|
|
48
|
+
if (block?.text)
|
|
49
|
+
parts.push({ type: 'text', content: String(block.text) });
|
|
50
|
+
}
|
|
51
|
+
for (const rawToolCall of Array.isArray(message.toolCalls) ? message.toolCalls : []) {
|
|
52
|
+
const toolCall = asObject(rawToolCall);
|
|
53
|
+
const functionCall = asObject(toolCall?.function);
|
|
54
|
+
if (!functionCall?.name)
|
|
55
|
+
continue;
|
|
56
|
+
// v2 follows the OpenAI shape: the arguments come as a JSON string.
|
|
57
|
+
let parsedArguments = functionCall.arguments;
|
|
58
|
+
if (typeof parsedArguments === 'string') {
|
|
59
|
+
try {
|
|
60
|
+
parsedArguments = JSON.parse(parsedArguments);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// A string the model did not close as valid JSON is kept as is.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const part = {
|
|
67
|
+
type: 'tool_call',
|
|
68
|
+
name: String(functionCall.name),
|
|
69
|
+
arguments: parsedArguments ?? {},
|
|
70
|
+
};
|
|
71
|
+
if (toolCall?.id)
|
|
72
|
+
part.id = String(toolCall.id);
|
|
73
|
+
parts.push(part);
|
|
74
|
+
}
|
|
75
|
+
if (!parts.length)
|
|
76
|
+
return [];
|
|
77
|
+
const outputMessage = { role: 'assistant', parts };
|
|
78
|
+
const finishReason = asObject(response)?.finishReason;
|
|
79
|
+
if (finishReason)
|
|
80
|
+
outputMessage.finish_reason = String(finishReason);
|
|
81
|
+
return [outputMessage];
|
|
82
|
+
}
|
|
83
|
+
function setRequestAttributes(span, request) {
|
|
84
|
+
const chatRequest = asObject(request);
|
|
85
|
+
if (chatRequest === null)
|
|
86
|
+
return;
|
|
87
|
+
if (chatRequest.model)
|
|
88
|
+
span.setAttribute('gen_ai.request.model', String(chatRequest.model));
|
|
89
|
+
// No sampling parameters: OpenLLMetry records none for Cohere either, and a
|
|
90
|
+
// half-populated input_params would differ between the two SDKs.
|
|
91
|
+
const inputMessages = (0, get_input_message_content_telemetry_1.extractInputMessages)(chatRequest);
|
|
92
|
+
if (inputMessages.length) {
|
|
93
|
+
span.setAttribute('gen_ai.input.messages', JSON.stringify(inputMessages));
|
|
94
|
+
}
|
|
95
|
+
if (Array.isArray(chatRequest.tools) && chatRequest.tools.length) {
|
|
96
|
+
span.setAttribute('gen_ai.tool.definitions', JSON.stringify(chatRequest.tools));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function setResponseAttributes(span, response) {
|
|
100
|
+
const chatResponse = asObject(response);
|
|
101
|
+
if (chatResponse === null)
|
|
102
|
+
return;
|
|
103
|
+
if (chatResponse.id)
|
|
104
|
+
span.setAttribute('gen_ai.response.id', String(chatResponse.id));
|
|
105
|
+
if (chatResponse.finishReason) {
|
|
106
|
+
span.setAttribute('gen_ai.response.finish_reasons', [String(chatResponse.finishReason)]);
|
|
107
|
+
}
|
|
108
|
+
const outputMessages = buildOutputMessages(chatResponse);
|
|
109
|
+
if (outputMessages.length) {
|
|
110
|
+
span.setAttribute('gen_ai.output.messages', JSON.stringify(outputMessages));
|
|
111
|
+
}
|
|
112
|
+
// Cohere reports what it bills under billedUnits; tokens holds the raw counts.
|
|
113
|
+
const usage = asObject(chatResponse.usage);
|
|
114
|
+
const billedUnits = asObject(usage?.billedUnits) ?? asObject(usage?.tokens);
|
|
115
|
+
if (typeof billedUnits?.inputTokens === 'number') {
|
|
116
|
+
span.setAttribute('gen_ai.usage.input_tokens', billedUnits.inputTokens);
|
|
117
|
+
}
|
|
118
|
+
if (typeof billedUnits?.outputTokens === 'number') {
|
|
119
|
+
span.setAttribute('gen_ai.usage.output_tokens', billedUnits.outputTokens);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
class CohereV2ChatInstrumentor {
|
|
123
|
+
// [ownerPrototype, methodName, originalFunction] for uninstrument().
|
|
124
|
+
_patched = [];
|
|
125
|
+
instrument() {
|
|
126
|
+
let V2Client;
|
|
127
|
+
try {
|
|
128
|
+
// CohereClientV2 binds `chat` as an instance property in its constructor,
|
|
129
|
+
// so the underlying V2Client class is what has to be patched - and it has
|
|
130
|
+
// to be patched before the caller builds their client.
|
|
131
|
+
({ V2Client } = require('cohere-ai/api/resources/v2/client/Client'));
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
// A private path inside cohere-ai, so a release can move it. v2 calls
|
|
135
|
+
// then go uninstrumented, which is what they were before this file: the
|
|
136
|
+
// v1 client the traceloop instrumentor covers must keep working.
|
|
137
|
+
(0, logger_1.debug)('cohere: v2 chat not instrumented (%s)', err.message);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const original = V2Client.prototype.chat;
|
|
141
|
+
// Not a function means the release moved or renamed the method: leave v2
|
|
142
|
+
// uninstrumented rather than install a wrapper whose original.call() would
|
|
143
|
+
// throw on the caller's first chat.
|
|
144
|
+
if (typeof original !== 'function' || original[PATCHED_MARKER])
|
|
145
|
+
return;
|
|
146
|
+
function wrapper(request, ...rest) {
|
|
147
|
+
const span = api_1.trace.getTracer(TRACER_NAME).startSpan(CHAT_OPERATION);
|
|
148
|
+
span.setAttribute('gen_ai.system', SYSTEM);
|
|
149
|
+
span.setAttribute('gen_ai.operation.name', CHAT_OPERATION);
|
|
150
|
+
try {
|
|
151
|
+
setRequestAttributes(span, request);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// Never break the user's Cohere call over an enrichment failure.
|
|
155
|
+
}
|
|
156
|
+
return Promise.resolve(original.call(this, request, ...rest)).then((response) => {
|
|
157
|
+
try {
|
|
158
|
+
setResponseAttributes(span, response);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// As above: the answer still reaches the caller.
|
|
162
|
+
}
|
|
163
|
+
span.end();
|
|
164
|
+
return response;
|
|
165
|
+
}, (error) => {
|
|
166
|
+
span.setStatus({
|
|
167
|
+
code: api_1.SpanStatusCode.ERROR,
|
|
168
|
+
message: error?.message ?? String(error),
|
|
169
|
+
});
|
|
170
|
+
span.recordException(error);
|
|
171
|
+
span.end();
|
|
172
|
+
throw error;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
wrapper[PATCHED_MARKER] = true;
|
|
176
|
+
V2Client.prototype.chat = wrapper;
|
|
177
|
+
this._patched.push([V2Client.prototype, 'chat', original]);
|
|
178
|
+
}
|
|
179
|
+
uninstrument() {
|
|
180
|
+
for (const [owner, methodName, original] of this._patched) {
|
|
181
|
+
owner[methodName] = original;
|
|
182
|
+
}
|
|
183
|
+
this._patched = [];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
exports.CohereV2ChatInstrumentor = CohereV2ChatInstrumentor;
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* - the embeddings endpoint it doesn't cover,
|
|
8
8
|
* - the request's structured-output schema, and
|
|
9
9
|
* - function calling: it records neither the request's tools nor the
|
|
10
|
-
* response's tool calls
|
|
10
|
+
* response's tool calls, and
|
|
11
|
+
* - the v2 chat endpoint, which it does not wrap at all (so a v2 call, the
|
|
12
|
+
* only way to send Cohere an image, produced no span).
|
|
11
13
|
*/
|
|
12
14
|
export declare class CohereTelemetryInstrumentor {
|
|
13
15
|
private _subInstrumentors;
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* - the embeddings endpoint it doesn't cover,
|
|
9
9
|
* - the request's structured-output schema, and
|
|
10
10
|
* - function calling: it records neither the request's tools nor the
|
|
11
|
-
* response's tool calls
|
|
11
|
+
* response's tool calls, and
|
|
12
|
+
* - the v2 chat endpoint, which it does not wrap at all (so a v2 call, the
|
|
13
|
+
* only way to send Cohere an image, produced no span).
|
|
12
14
|
*/
|
|
13
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
16
|
exports.CohereTelemetryInstrumentor = void 0;
|
|
@@ -18,6 +20,7 @@ const get_embeddings_telemetry_1 = require("./get_embeddings_telemetry");
|
|
|
18
20
|
const get_structured_output_schema_telemetry_2 = require("./get_structured_output_schema_telemetry");
|
|
19
21
|
const get_tool_call_output_telemetry_1 = require("./get_tool_call_output_telemetry");
|
|
20
22
|
const get_tool_definitions_telemetry_1 = require("./get_tool_definitions_telemetry");
|
|
23
|
+
const get_v2_chat_telemetry_1 = require("./get_v2_chat_telemetry");
|
|
21
24
|
class CohereTelemetryInstrumentor {
|
|
22
25
|
_subInstrumentors;
|
|
23
26
|
constructor() {
|
|
@@ -27,6 +30,7 @@ class CohereTelemetryInstrumentor {
|
|
|
27
30
|
new get_structured_output_schema_telemetry_1.TraceloopStructuredOutputSchemaInstrumentor(get_structured_output_schema_telemetry_2.readStructuredOutputSchema),
|
|
28
31
|
new get_tool_definitions_telemetry_1.CohereToolDefinitionsInstrumentor(),
|
|
29
32
|
new get_tool_call_output_telemetry_1.CohereToolCallOutputInstrumentor(),
|
|
33
|
+
new get_v2_chat_telemetry_1.CohereV2ChatInstrumentor(),
|
|
30
34
|
];
|
|
31
35
|
}
|
|
32
36
|
instrument(instrumentation) {
|
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
* `@traceloop/instrumentation-google-generativeai` wraps generate_content but not
|
|
5
5
|
* embeddings, so this covers it by starting its own span on the private
|
|
6
6
|
* `Models.prototype.embedContentInternal` (which the public `embedContent`
|
|
7
|
-
* delegates to).
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* delegates to).
|
|
8
|
+
*
|
|
9
|
+
* Both surfaces bill embeddings per input token, and only Vertex reports them:
|
|
10
|
+
* it returns `tokenCount` in each embedding's `statistics`, which the span
|
|
11
|
+
* records as `gen_ai.usage.input_tokens`. The Gemini Developer API returns no
|
|
12
|
+
* usage at all, on any API version, so its calls carry no token attribute and
|
|
13
|
+
* weflayr cannot price them, which `_warnDeveloperApiUnpriced` says once per
|
|
14
|
+
* process.
|
|
11
15
|
*/
|
|
12
16
|
export declare function instrumentEmbeddings(Models: any, patched: [any, string, any][]): void;
|
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
* `@traceloop/instrumentation-google-generativeai` wraps generate_content but not
|
|
6
6
|
* embeddings, so this covers it by starting its own span on the private
|
|
7
7
|
* `Models.prototype.embedContentInternal` (which the public `embedContent`
|
|
8
|
-
* delegates to).
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* delegates to).
|
|
9
|
+
*
|
|
10
|
+
* Both surfaces bill embeddings per input token, and only Vertex reports them:
|
|
11
|
+
* it returns `tokenCount` in each embedding's `statistics`, which the span
|
|
12
|
+
* records as `gen_ai.usage.input_tokens`. The Gemini Developer API returns no
|
|
13
|
+
* usage at all, on any API version, so its calls carry no token attribute and
|
|
14
|
+
* weflayr cannot price them, which `_warnDeveloperApiUnpriced` says once per
|
|
15
|
+
* process.
|
|
12
16
|
*/
|
|
13
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
18
|
exports.instrumentEmbeddings = instrumentEmbeddings;
|
|
@@ -16,34 +20,59 @@ const api_1 = require("@opentelemetry/api");
|
|
|
16
20
|
const get_provider_name_1 = require("./get_provider_name");
|
|
17
21
|
const TRACER_NAME = 'weflayr.instrumentation.google.embeddings';
|
|
18
22
|
const EMBEDDINGS_OPERATION = 'embeddings';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
const EMBEDDINGS_INPUT_ATTR = 'weflayr.embeddings.input';
|
|
24
|
+
const GEMINI_DEVELOPER_API_SYSTEM = 'gemini';
|
|
25
|
+
// The Gemini Developer API reports no embeddings usage, so those calls are
|
|
26
|
+
// stored with a cost of zero; say so once rather than let that pass silently.
|
|
27
|
+
let _developerApiWarningSaid = false;
|
|
28
|
+
// One embedded content is a string, a Content/Part object, or an array of
|
|
29
|
+
// those (a Content's parts); its text is the concatenation across them.
|
|
30
|
+
function _contentText(content) {
|
|
31
|
+
if (content == null)
|
|
32
|
+
return '';
|
|
33
|
+
if (typeof content === 'string')
|
|
34
|
+
return content;
|
|
35
|
+
if (Array.isArray(content))
|
|
36
|
+
return content.map(_contentText).join('');
|
|
37
|
+
if (typeof content.text === 'string')
|
|
38
|
+
return content.text;
|
|
39
|
+
return _contentText(content.parts);
|
|
40
|
+
}
|
|
41
|
+
// The text of each embedded content, e.g. ["ab", "cde"] for
|
|
42
|
+
// contents: ["ab", { parts: [{ text: "cde" }] }].
|
|
43
|
+
function _embeddedTexts(contents) {
|
|
22
44
|
if (contents == null)
|
|
23
|
-
return
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (Array.isArray(contents)) {
|
|
27
|
-
return contents.reduce((total, item) => total + _contentCharacters(item), 0);
|
|
28
|
-
}
|
|
29
|
-
if (typeof contents === 'object') {
|
|
30
|
-
if (typeof contents.text === 'string')
|
|
31
|
-
return contents.text.length;
|
|
32
|
-
if (Array.isArray(contents.parts))
|
|
33
|
-
return _contentCharacters(contents.parts);
|
|
34
|
-
}
|
|
35
|
-
return 0;
|
|
45
|
+
return [];
|
|
46
|
+
const contentsList = Array.isArray(contents) ? contents : [contents];
|
|
47
|
+
return contentsList.map(_contentText);
|
|
36
48
|
}
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
function
|
|
40
|
-
if (
|
|
41
|
-
return
|
|
49
|
+
// Says once per process that Gemini Developer API embeddings calls cannot be
|
|
50
|
+
// priced.
|
|
51
|
+
function _warnDeveloperApiUnpriced() {
|
|
52
|
+
if (_developerApiWarningSaid)
|
|
53
|
+
return;
|
|
54
|
+
_developerApiWarningSaid = true;
|
|
55
|
+
console.warn('weflayr: Gemini Developer API embeddings calls cannot be priced: the API ' +
|
|
56
|
+
'returns no token usage for them, so they are recorded with a cost of ' +
|
|
57
|
+
'zero. Use Vertex, which reports the token count itself.');
|
|
58
|
+
}
|
|
59
|
+
// Records the input tokens Vertex reports per embedding, e.g. 7 for a call
|
|
60
|
+
// embedding two texts of 2 and 5 tokens. The Gemini Developer API reports none,
|
|
61
|
+
// and then the span carries no token attribute.
|
|
62
|
+
function _recordInputTokens(span, response) {
|
|
63
|
+
try {
|
|
64
|
+
const reported = ((response && response.embeddings) || []).map((embedding) => embedding && embedding.statistics && embedding.statistics.tokenCount);
|
|
65
|
+
// A total is only worth recording when every embedding reports a number:
|
|
66
|
+
// one missing or unreadable count would make the sum too small.
|
|
67
|
+
if (reported.length && reported.every((tokenCount) => Number.isFinite(tokenCount))) {
|
|
68
|
+
span.setAttribute('gen_ai.usage.input_tokens', reported.reduce((total, tokenCount) => total + tokenCount, 0));
|
|
69
|
+
}
|
|
42
70
|
}
|
|
43
|
-
|
|
44
|
-
|
|
71
|
+
catch (error) {
|
|
72
|
+
// Telemetry must never break the app's call, and a swallowed failure still
|
|
73
|
+
// has to be findable.
|
|
74
|
+
console.debug('weflayr: reading Gemini embeddings usage failed', error);
|
|
45
75
|
}
|
|
46
|
-
return { role: 'user', parts: [] };
|
|
47
76
|
}
|
|
48
77
|
function _recordError(span, error) {
|
|
49
78
|
span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error?.message ?? String(error) });
|
|
@@ -58,20 +87,24 @@ function instrumentEmbeddings(Models, patched) {
|
|
|
58
87
|
return;
|
|
59
88
|
function wrapper(params, ...rest) {
|
|
60
89
|
const span = api_1.trace.getTracer(TRACER_NAME).startSpan(EMBEDDINGS_OPERATION);
|
|
61
|
-
|
|
90
|
+
const system = (0, get_provider_name_1.getProviderName)(this);
|
|
91
|
+
span.setAttribute('gen_ai.system', system);
|
|
62
92
|
span.setAttribute('gen_ai.operation.name', EMBEDDINGS_OPERATION);
|
|
63
93
|
if (params && params.model) {
|
|
64
94
|
// The chat path strips the leading "models/" prefix; mirror it.
|
|
65
95
|
span.setAttribute('gen_ai.request.model', params.model.replace(/^models\//, ''));
|
|
66
96
|
}
|
|
67
|
-
|
|
68
|
-
// The embedded
|
|
69
|
-
//
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
97
|
+
const embeddedTexts = _embeddedTexts(params && params.contents);
|
|
98
|
+
// The embedded texts; the worker shapes them. The span processor strips
|
|
99
|
+
// the attribute when message-content capture is disabled.
|
|
100
|
+
if (embeddedTexts.length) {
|
|
101
|
+
span.setAttribute(EMBEDDINGS_INPUT_ATTR, JSON.stringify(embeddedTexts));
|
|
102
|
+
}
|
|
103
|
+
if (system === GEMINI_DEVELOPER_API_SYSTEM) {
|
|
104
|
+
_warnDeveloperApiUnpriced();
|
|
73
105
|
}
|
|
74
106
|
return Promise.resolve(original.call(this, params, ...rest)).then((response) => {
|
|
107
|
+
_recordInputTokens(span, response);
|
|
75
108
|
span.end();
|
|
76
109
|
return response;
|
|
77
110
|
}, (error) => {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Developer API. We read the client's Vertex flag and return the value to set as
|
|
7
7
|
* `gen_ai.system` (which the normalizer prefers): `vertex_ai` / `gemini`,
|
|
8
8
|
* matching what the Python google-genai instrumentor reports natively. The
|
|
9
|
-
* worker normalizes `
|
|
9
|
+
* worker normalizes `gemini` to the canonical `google` and `vertex_ai` to
|
|
10
|
+
* `vertex`. Node-only.
|
|
10
11
|
*/
|
|
11
12
|
export declare function getProviderName(instance: any): string;
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* Developer API. We read the client's Vertex flag and return the value to set as
|
|
8
8
|
* `gen_ai.system` (which the normalizer prefers): `vertex_ai` / `gemini`,
|
|
9
9
|
* matching what the Python google-genai instrumentor reports natively. The
|
|
10
|
-
* worker normalizes `
|
|
10
|
+
* worker normalizes `gemini` to the canonical `google` and `vertex_ai` to
|
|
11
|
+
* `vertex`. Node-only.
|
|
11
12
|
*/
|
|
12
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
14
|
exports.getProviderName = getProviderName;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-run web search (Google Search grounding) capture for Gemini spans.
|
|
3
|
+
*
|
|
4
|
+
* Gemini runs the search itself when the request config carries the
|
|
5
|
+
* `googleSearch` tool, and bills the call once per grounded request whatever
|
|
6
|
+
* number of queries it ran. Neither instrumentor records any of it: the
|
|
7
|
+
* `googleSearch` tool is nowhere in the span, `candidates[].groundingMetadata`
|
|
8
|
+
* is dropped, and so is `usageMetadata.toolUsePromptTokenCount`, input tokens
|
|
9
|
+
* the call is billed for, counted SEPARATELY from `promptTokenCount`, so a
|
|
10
|
+
* 14-token prompt that grounds bills 14 + 65.
|
|
11
|
+
*
|
|
12
|
+
* All three are stamped under `weflayr.web_search.*`; `normalizers/google.py`
|
|
13
|
+
* maps them to the shape every provider's web search is stored in.
|
|
14
|
+
*
|
|
15
|
+
* The `@google/genai` client answers in camelCase where the Python one answers
|
|
16
|
+
* in snake_case, so the stamped JSON is written in snake_case: both SDKs must
|
|
17
|
+
* produce the same event for the same call, and the normalizer reads one
|
|
18
|
+
* spelling.
|
|
19
|
+
*
|
|
20
|
+
* `groundingMetadata.searchEntryPoint` is not stamped: it is several kilobytes
|
|
21
|
+
* of styled HTML for rendering Google's "Search suggestions" widget, which
|
|
22
|
+
* nothing reads back.
|
|
23
|
+
*/
|
|
24
|
+
/** The request's Google Search tools, as configured, under the names the Python
|
|
25
|
+
* SDK stamps. e.g. a config carrying `{ tools: [{ googleSearch: {} }] }` ->
|
|
26
|
+
* `[{ google_search: {} }]`, and one setting a time range ->
|
|
27
|
+
* `[{ google_search: { time_range_filter: {...} } }]`. */
|
|
28
|
+
export declare function extractWebSearchToolDefinitions(config: any): any[];
|
|
29
|
+
export declare function setWebSearchToolTelemetry(span: any, config: any): void;
|
|
30
|
+
/** The queries and pages of a grounded candidate, in the snake_case shape the
|
|
31
|
+
* normalizer reads. Returns an empty array when nothing was grounded. */
|
|
32
|
+
export declare function extractGroundingBlocks(response: any): any[];
|
|
33
|
+
/** Stamp what the grounded answer searched for, that it is billed for one
|
|
34
|
+
* grounded request, and the input tokens the search results added.
|
|
35
|
+
*
|
|
36
|
+
* Over a stream each chunk re-reports the grounding gathered so far, so the
|
|
37
|
+
* attribute is re-stamped rather than accumulated. */
|
|
38
|
+
export declare function setWebSearchResultTelemetry(span: any, response: any): void;
|