weflayr 0.26.1 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/package.json +8 -5
  2. package/dist/src/auto-instrument.d.ts +4 -3
  3. package/dist/src/auto-instrument.js +18 -17
  4. package/dist/src/get-data-uri-mime-type.d.ts +17 -0
  5. package/dist/src/get-data-uri-mime-type.js +25 -0
  6. package/dist/src/index.d.ts +3 -2
  7. package/dist/src/index.js +5 -1
  8. package/dist/src/instrumentation/anthropic/get_streaming_accumulation_telemetry.d.ts +3 -1
  9. package/dist/src/instrumentation/anthropic/get_streaming_accumulation_telemetry.js +6 -3
  10. package/dist/src/instrumentation/anthropic/get_vertex_billing_telemetry.d.ts +32 -0
  11. package/dist/src/instrumentation/anthropic/get_vertex_billing_telemetry.js +100 -0
  12. package/dist/src/instrumentation/anthropic/get_web_search_telemetry.d.ts +43 -0
  13. package/dist/src/instrumentation/anthropic/get_web_search_telemetry.js +147 -0
  14. package/dist/src/instrumentation/anthropic/set_telemetry_attributes.d.ts +5 -2
  15. package/dist/src/instrumentation/anthropic/set_telemetry_attributes.js +17 -4
  16. package/dist/src/instrumentation/bedrock/get_cache_config_telemetry.d.ts +13 -1
  17. package/dist/src/instrumentation/bedrock/get_cache_config_telemetry.js +56 -1
  18. package/dist/src/instrumentation/bedrock/get_cache_telemetry.js +8 -5
  19. package/dist/src/instrumentation/bedrock/get_input_message_content_telemetry.js +33 -5
  20. package/dist/src/instrumentation/bedrock/get_model_id_telemetry.d.ts +1 -1
  21. package/dist/src/instrumentation/bedrock/get_model_id_telemetry.js +1 -1
  22. package/dist/src/instrumentation/bedrock/get_openai_chat_format_telemetry.d.ts +45 -0
  23. package/dist/src/instrumentation/bedrock/get_openai_chat_format_telemetry.js +251 -0
  24. package/dist/src/instrumentation/bedrock/get_streaming_output_message_telemetry.d.ts +6 -5
  25. package/dist/src/instrumentation/bedrock/get_streaming_output_message_telemetry.js +15 -12
  26. package/dist/src/instrumentation/bedrock/get_tool_definitions_telemetry.d.ts +2 -2
  27. package/dist/src/instrumentation/bedrock/get_tool_definitions_telemetry.js +12 -4
  28. package/dist/src/instrumentation/bedrock/set_telemetry_attributes.d.ts +4 -2
  29. package/dist/src/instrumentation/bedrock/set_telemetry_attributes.js +10 -3
  30. package/dist/src/instrumentation/cohere/get_embeddings_telemetry.js +4 -6
  31. package/dist/src/instrumentation/cohere/get_input_message_content_telemetry.d.ts +23 -0
  32. package/dist/src/instrumentation/cohere/get_input_message_content_telemetry.js +105 -0
  33. package/dist/src/instrumentation/cohere/get_v2_chat_telemetry.d.ts +18 -0
  34. package/dist/src/instrumentation/cohere/get_v2_chat_telemetry.js +186 -0
  35. package/dist/src/instrumentation/cohere/set_telemetry_attributes.d.ts +3 -1
  36. package/dist/src/instrumentation/cohere/set_telemetry_attributes.js +5 -1
  37. package/dist/src/instrumentation/google/get_embeddings_telemetry.d.ts +8 -4
  38. package/dist/src/instrumentation/google/get_embeddings_telemetry.js +68 -35
  39. package/dist/src/instrumentation/google/get_provider_name.d.ts +2 -1
  40. package/dist/src/instrumentation/google/get_provider_name.js +2 -1
  41. package/dist/src/instrumentation/google/get_web_search_telemetry.d.ts +38 -0
  42. package/dist/src/instrumentation/google/get_web_search_telemetry.js +125 -0
  43. package/dist/src/instrumentation/google/set_telemetry_attributes.d.ts +2 -2
  44. package/dist/src/instrumentation/google/set_telemetry_attributes.js +6 -2
  45. package/dist/src/instrumentation/mistral/full_instrumentation.d.ts +60 -0
  46. package/dist/src/instrumentation/mistral/full_instrumentation.js +364 -0
  47. package/dist/src/instrumentation/mistral/get_input_message_content_telemetry.d.ts +35 -0
  48. package/dist/src/instrumentation/mistral/get_input_message_content_telemetry.js +113 -0
  49. package/dist/src/instrumentation/mistral/get_structured_output_schema_telemetry.d.ts +13 -0
  50. package/dist/src/instrumentation/mistral/get_structured_output_schema_telemetry.js +24 -0
  51. package/dist/src/instrumentation/mistral/get_vertex_telemetry.d.ts +54 -0
  52. package/dist/src/instrumentation/mistral/get_vertex_telemetry.js +153 -0
  53. package/dist/src/instrumentation/openai/get_bedrock_mantle_telemetry.d.ts +10 -0
  54. package/dist/src/instrumentation/openai/get_bedrock_mantle_telemetry.js +33 -0
  55. package/dist/src/instrumentation/openai/get_cache_and_reasoning_telemetry.d.ts +13 -6
  56. package/dist/src/instrumentation/openai/get_cache_and_reasoning_telemetry.js +17 -7
  57. package/dist/src/instrumentation/openai/get_cache_config_telemetry.d.ts +21 -4
  58. package/dist/src/instrumentation/openai/get_cache_config_telemetry.js +50 -11
  59. package/dist/src/instrumentation/openai/get_embeddings_telemetry.js +14 -6
  60. package/dist/src/instrumentation/openai/get_file_part_mime_type_telemetry.d.ts +26 -0
  61. package/dist/src/instrumentation/openai/get_file_part_mime_type_telemetry.js +100 -0
  62. package/dist/src/instrumentation/openai/get_openai_compatible_provider_telemetry.d.ts +48 -0
  63. package/dist/src/instrumentation/openai/get_openai_compatible_provider_telemetry.js +123 -0
  64. package/dist/src/instrumentation/openai/get_reasoning_content_telemetry.d.ts +28 -0
  65. package/dist/src/instrumentation/openai/get_reasoning_content_telemetry.js +130 -0
  66. package/dist/src/instrumentation/openai/get_responses_telemetry.d.ts +89 -0
  67. package/dist/src/instrumentation/openai/get_responses_telemetry.js +587 -0
  68. package/dist/src/instrumentation/openai/get_service_tier_telemetry.d.ts +20 -0
  69. package/dist/src/instrumentation/openai/get_service_tier_telemetry.js +84 -0
  70. package/dist/src/instrumentation/openai/get_structured_output_schema_telemetry.d.ts +7 -5
  71. package/dist/src/instrumentation/openai/get_structured_output_schema_telemetry.js +15 -8
  72. package/dist/src/instrumentation/openai/get_vertex_openai_compatible_telemetry.d.ts +23 -0
  73. package/dist/src/instrumentation/openai/get_vertex_openai_compatible_telemetry.js +69 -0
  74. package/dist/src/instrumentation/openai/get_web_search_telemetry.d.ts +74 -0
  75. package/dist/src/instrumentation/openai/get_web_search_telemetry.js +259 -0
  76. package/dist/src/instrumentation/openai/set_telemetry_attributes.d.ts +13 -1
  77. package/dist/src/instrumentation/openai/set_telemetry_attributes.js +33 -2
  78. package/dist/src/instrumentation/shared/get_structured_output_schema_telemetry.d.ts +1 -1
  79. package/dist/src/instrumentation/shared/get_structured_output_schema_telemetry.js +1 -1
  80. package/dist/src/instrumentation/shared/is_vertex_base_url.d.ts +17 -0
  81. package/dist/src/instrumentation/shared/is_vertex_base_url.js +40 -0
  82. package/dist/src/otel/propagation.d.ts +37 -10
  83. package/dist/src/otel/propagation.js +62 -22
  84. package/dist/src/otel/span-filter.d.ts +2 -0
  85. package/dist/src/otel/span-filter.js +10 -1
  86. package/dist/src/otel/span-processor.d.ts +1 -2
  87. package/dist/src/otel/span-processor.js +17 -20
  88. package/dist/src/otel/strip-media-payloads.d.ts +26 -0
  89. package/dist/src/otel/strip-media-payloads.js +135 -0
  90. package/dist/src/propagate-metadata-to-gateway.d.ts +37 -0
  91. package/dist/src/propagate-metadata-to-gateway.js +213 -0
  92. package/package.json +8 -5
  93. package/dist/src/instrumentation/vercel/full_instrumentation.d.ts +0 -3
  94. package/dist/src/instrumentation/vercel/full_instrumentation.js +0 -54
  95. package/dist/src/instrumentation/vercel/get_cache_creation_telemetry.d.ts +0 -22
  96. package/dist/src/instrumentation/vercel/get_cache_creation_telemetry.js +0 -66
  97. package/dist/src/instrumentation/vercel/get_structured_output_schema_telemetry.d.ts +0 -24
  98. package/dist/src/instrumentation/vercel/get_structured_output_schema_telemetry.js +0 -84
  99. package/dist/src/otel/vercel-ai-sdk-span-filter.d.ts +0 -12
  100. package/dist/src/otel/vercel-ai-sdk-span-filter.js +0 -88
@@ -7,8 +7,10 @@
7
7
  * breakpoints (`cache_control` / `cachePoint` blocks it never records), the
8
8
  * streamed output message content (its stream path formats an empty-parts
9
9
  * message), the request's tool definitions (it never reads the body's
10
- * `toolConfig`) and the tool-call output parts (its block mapper only knows
11
- * Anthropic's block shape, so Nova's `toolUse` blocks come through raw).
10
+ * `toolConfig`), the tool-call output parts (its block mapper only knows
11
+ * Anthropic's block shape, so Nova's `toolUse` blocks come through raw), and
12
+ * everything about a model whose body is OpenAI's chat completions format (it
13
+ * reads only the vendors it knows).
12
14
  */
13
15
  export declare class BedrockTelemetryInstrumentor {
14
16
  private _subInstrumentors;
@@ -8,8 +8,10 @@
8
8
  * breakpoints (`cache_control` / `cachePoint` blocks it never records), the
9
9
  * streamed output message content (its stream path formats an empty-parts
10
10
  * message), the request's tool definitions (it never reads the body's
11
- * `toolConfig`) and the tool-call output parts (its block mapper only knows
12
- * Anthropic's block shape, so Nova's `toolUse` blocks come through raw).
11
+ * `toolConfig`), the tool-call output parts (its block mapper only knows
12
+ * Anthropic's block shape, so Nova's `toolUse` blocks come through raw), and
13
+ * everything about a model whose body is OpenAI's chat completions format (it
14
+ * reads only the vendors it knows).
13
15
  */
14
16
  Object.defineProperty(exports, "__esModule", { value: true });
15
17
  exports.BedrockTelemetryInstrumentor = void 0;
@@ -18,6 +20,7 @@ const get_cache_telemetry_1 = require("./get_cache_telemetry");
18
20
  const get_input_message_content_telemetry_1 = require("./get_input_message_content_telemetry");
19
21
  const get_streaming_output_message_telemetry_1 = require("./get_streaming_output_message_telemetry");
20
22
  const get_model_id_telemetry_1 = require("./get_model_id_telemetry");
23
+ const get_openai_chat_format_telemetry_1 = require("./get_openai_chat_format_telemetry");
21
24
  const get_reasoning_output_telemetry_1 = require("./get_reasoning_output_telemetry");
22
25
  const get_tool_call_output_telemetry_1 = require("./get_tool_call_output_telemetry");
23
26
  const get_tool_definitions_telemetry_1 = require("./get_tool_definitions_telemetry");
@@ -33,14 +36,18 @@ class BedrockTelemetryInstrumentor {
33
36
  new get_tool_call_output_telemetry_1.BedrockToolCallOutputInstrumentor(),
34
37
  new get_model_id_telemetry_1.BedrockModelIdInstrumentor(),
35
38
  new get_reasoning_output_telemetry_1.BedrockReasoningOutputInstrumentor(),
39
+ new get_openai_chat_format_telemetry_1.BedrockOpenaiChatFormatInstrumentor(),
36
40
  ];
37
41
  }
38
42
  instrument(instrumentation) {
39
43
  for (const sub of this._subInstrumentors)
40
44
  sub.instrument(instrumentation);
41
45
  }
46
+ // Reversed: several complements wrap the same instrumentor method
47
+ // (`_startSpan`), so the patches have to be unwound last-patched-first or an
48
+ // earlier wrapper is written back over a later one and stays installed.
42
49
  uninstrument() {
43
- for (const sub of this._subInstrumentors)
50
+ for (const sub of [...this._subInstrumentors].reverse())
44
51
  sub.uninstrument();
45
52
  }
46
53
  }
@@ -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 are the prompt; stamp one user message per text, like
36
- // the chat path. The span processor strips it when content capture is off.
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('gen_ai.input.messages', JSON.stringify(request.texts.map((text) => ({
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). The Gemini Developer API returns NO token usage on embedContent
8
- * (only Vertex does), so the span records the billable unit weflayr can price:
9
- * the input character count under `gen_ai.usage.input_characters` (normalized to
10
- * num_input_chars).
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). The Gemini Developer API returns NO token usage on embedContent
9
- * (only Vertex does), so the span records the billable unit weflayr can price:
10
- * the input character count under `gen_ai.usage.input_characters` (normalized to
11
- * num_input_chars).
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
- // `contents` is a string, a Content/Part object, or an array of those; the
20
- // billable unit is the total number of characters of text across them.
21
- function _contentCharacters(contents) {
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 0;
24
- if (typeof contents === 'string')
25
- return contents.length;
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
- // The embedded text mapped to the role+parts message shape the chat path uses:
38
- // a string (or {text} / {parts} object) becomes one user message per content.
39
- function _contentToMessage(content) {
40
- if (typeof content === 'string') {
41
- return { role: 'user', parts: [{ type: 'text', content }] };
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
- if (content && typeof content.text === 'string') {
44
- return { role: 'user', parts: [{ type: 'text', content: content.text }] };
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
- span.setAttribute('gen_ai.system', (0, get_provider_name_1.getProviderName)(this));
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
- span.setAttribute('gen_ai.usage.input_characters', _contentCharacters(params && params.contents));
68
- // The embedded text is the prompt; stamp it like the chat path does. The
69
- // span processor strips it when message-content capture is disabled.
70
- if (params && params.contents != null) {
71
- const contentsList = Array.isArray(params.contents) ? params.contents : [params.contents];
72
- span.setAttribute('gen_ai.input.messages', JSON.stringify(contentsList.map(_contentToMessage)));
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 `vertex_ai` to the canonical `vertex`. Node-only.
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 `vertex_ai` to the canonical `vertex`. Node-only.
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;