weflayr 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/package.json +3 -3
  2. package/dist/src/auto-instrument.d.ts +4 -3
  3. package/dist/src/auto-instrument.js +18 -17
  4. package/dist/src/get-data-uri-mime-type.d.ts +17 -0
  5. package/dist/src/get-data-uri-mime-type.js +25 -0
  6. package/dist/src/index.d.ts +3 -2
  7. package/dist/src/index.js +5 -1
  8. package/dist/src/instrumentation/anthropic/get_streaming_accumulation_telemetry.d.ts +3 -1
  9. package/dist/src/instrumentation/anthropic/get_streaming_accumulation_telemetry.js +6 -3
  10. package/dist/src/instrumentation/anthropic/get_vertex_billing_telemetry.d.ts +32 -0
  11. package/dist/src/instrumentation/anthropic/get_vertex_billing_telemetry.js +100 -0
  12. package/dist/src/instrumentation/anthropic/get_web_search_telemetry.d.ts +43 -0
  13. package/dist/src/instrumentation/anthropic/get_web_search_telemetry.js +147 -0
  14. package/dist/src/instrumentation/anthropic/set_telemetry_attributes.d.ts +5 -2
  15. package/dist/src/instrumentation/anthropic/set_telemetry_attributes.js +17 -4
  16. package/dist/src/instrumentation/bedrock/get_cache_config_telemetry.d.ts +13 -1
  17. package/dist/src/instrumentation/bedrock/get_cache_config_telemetry.js +56 -1
  18. package/dist/src/instrumentation/bedrock/get_cache_telemetry.js +8 -5
  19. package/dist/src/instrumentation/bedrock/get_model_id_telemetry.d.ts +1 -1
  20. package/dist/src/instrumentation/bedrock/get_model_id_telemetry.js +1 -1
  21. package/dist/src/instrumentation/bedrock/get_openai_chat_format_telemetry.d.ts +45 -0
  22. package/dist/src/instrumentation/bedrock/get_openai_chat_format_telemetry.js +251 -0
  23. package/dist/src/instrumentation/bedrock/get_streaming_output_message_telemetry.d.ts +6 -5
  24. package/dist/src/instrumentation/bedrock/get_streaming_output_message_telemetry.js +15 -12
  25. package/dist/src/instrumentation/bedrock/get_tool_definitions_telemetry.d.ts +2 -2
  26. package/dist/src/instrumentation/bedrock/get_tool_definitions_telemetry.js +12 -4
  27. package/dist/src/instrumentation/bedrock/set_telemetry_attributes.d.ts +4 -2
  28. package/dist/src/instrumentation/bedrock/set_telemetry_attributes.js +10 -3
  29. package/dist/src/instrumentation/cohere/get_embeddings_telemetry.js +4 -6
  30. package/dist/src/instrumentation/cohere/get_input_message_content_telemetry.d.ts +23 -0
  31. package/dist/src/instrumentation/cohere/get_input_message_content_telemetry.js +105 -0
  32. package/dist/src/instrumentation/cohere/get_v2_chat_telemetry.d.ts +18 -0
  33. package/dist/src/instrumentation/cohere/get_v2_chat_telemetry.js +186 -0
  34. package/dist/src/instrumentation/cohere/set_telemetry_attributes.d.ts +3 -1
  35. package/dist/src/instrumentation/cohere/set_telemetry_attributes.js +5 -1
  36. package/dist/src/instrumentation/google/get_embeddings_telemetry.d.ts +8 -4
  37. package/dist/src/instrumentation/google/get_embeddings_telemetry.js +68 -35
  38. package/dist/src/instrumentation/google/get_provider_name.d.ts +2 -1
  39. package/dist/src/instrumentation/google/get_provider_name.js +2 -1
  40. package/dist/src/instrumentation/google/get_web_search_telemetry.d.ts +38 -0
  41. package/dist/src/instrumentation/google/get_web_search_telemetry.js +125 -0
  42. package/dist/src/instrumentation/google/set_telemetry_attributes.d.ts +2 -2
  43. package/dist/src/instrumentation/google/set_telemetry_attributes.js +6 -2
  44. package/dist/src/instrumentation/mistral/full_instrumentation.d.ts +60 -0
  45. package/dist/src/instrumentation/mistral/full_instrumentation.js +364 -0
  46. package/dist/src/instrumentation/mistral/get_input_message_content_telemetry.d.ts +35 -0
  47. package/dist/src/instrumentation/mistral/get_input_message_content_telemetry.js +113 -0
  48. package/dist/src/instrumentation/mistral/get_structured_output_schema_telemetry.d.ts +13 -0
  49. package/dist/src/instrumentation/mistral/get_structured_output_schema_telemetry.js +24 -0
  50. package/dist/src/instrumentation/mistral/get_vertex_telemetry.d.ts +54 -0
  51. package/dist/src/instrumentation/mistral/get_vertex_telemetry.js +153 -0
  52. package/dist/src/instrumentation/openai/get_bedrock_mantle_telemetry.d.ts +10 -0
  53. package/dist/src/instrumentation/openai/get_bedrock_mantle_telemetry.js +33 -0
  54. package/dist/src/instrumentation/openai/get_cache_and_reasoning_telemetry.d.ts +13 -6
  55. package/dist/src/instrumentation/openai/get_cache_and_reasoning_telemetry.js +17 -7
  56. package/dist/src/instrumentation/openai/get_cache_config_telemetry.d.ts +21 -4
  57. package/dist/src/instrumentation/openai/get_cache_config_telemetry.js +50 -11
  58. package/dist/src/instrumentation/openai/get_embeddings_telemetry.js +14 -6
  59. package/dist/src/instrumentation/openai/get_file_part_mime_type_telemetry.d.ts +26 -0
  60. package/dist/src/instrumentation/openai/get_file_part_mime_type_telemetry.js +100 -0
  61. package/dist/src/instrumentation/openai/get_openai_compatible_provider_telemetry.d.ts +48 -0
  62. package/dist/src/instrumentation/openai/get_openai_compatible_provider_telemetry.js +123 -0
  63. package/dist/src/instrumentation/openai/get_reasoning_content_telemetry.d.ts +28 -0
  64. package/dist/src/instrumentation/openai/get_reasoning_content_telemetry.js +130 -0
  65. package/dist/src/instrumentation/openai/get_responses_telemetry.d.ts +89 -0
  66. package/dist/src/instrumentation/openai/get_responses_telemetry.js +587 -0
  67. package/dist/src/instrumentation/openai/get_service_tier_telemetry.d.ts +20 -0
  68. package/dist/src/instrumentation/openai/get_service_tier_telemetry.js +84 -0
  69. package/dist/src/instrumentation/openai/get_structured_output_schema_telemetry.d.ts +7 -5
  70. package/dist/src/instrumentation/openai/get_structured_output_schema_telemetry.js +15 -8
  71. package/dist/src/instrumentation/openai/get_vertex_openai_compatible_telemetry.d.ts +23 -0
  72. package/dist/src/instrumentation/openai/get_vertex_openai_compatible_telemetry.js +69 -0
  73. package/dist/src/instrumentation/openai/get_web_search_telemetry.d.ts +74 -0
  74. package/dist/src/instrumentation/openai/get_web_search_telemetry.js +259 -0
  75. package/dist/src/instrumentation/openai/set_telemetry_attributes.d.ts +13 -1
  76. package/dist/src/instrumentation/openai/set_telemetry_attributes.js +33 -2
  77. package/dist/src/instrumentation/shared/get_structured_output_schema_telemetry.d.ts +1 -1
  78. package/dist/src/instrumentation/shared/get_structured_output_schema_telemetry.js +1 -1
  79. package/dist/src/instrumentation/shared/is_vertex_base_url.d.ts +17 -0
  80. package/dist/src/instrumentation/shared/is_vertex_base_url.js +40 -0
  81. package/dist/src/otel/propagation.d.ts +37 -10
  82. package/dist/src/otel/propagation.js +62 -22
  83. package/dist/src/otel/span-filter.d.ts +2 -0
  84. package/dist/src/otel/span-filter.js +10 -1
  85. package/dist/src/otel/span-processor.d.ts +1 -2
  86. package/dist/src/otel/span-processor.js +17 -20
  87. package/dist/src/otel/strip-media-payloads.d.ts +26 -0
  88. package/dist/src/otel/strip-media-payloads.js +135 -0
  89. package/dist/src/propagate-metadata-to-gateway.d.ts +37 -0
  90. package/dist/src/propagate-metadata-to-gateway.js +213 -0
  91. package/package.json +3 -3
  92. package/dist/src/instrumentation/vercel/full_instrumentation.d.ts +0 -3
  93. package/dist/src/instrumentation/vercel/full_instrumentation.js +0 -54
  94. package/dist/src/instrumentation/vercel/get_cache_creation_telemetry.d.ts +0 -22
  95. package/dist/src/instrumentation/vercel/get_cache_creation_telemetry.js +0 -66
  96. package/dist/src/instrumentation/vercel/get_structured_output_schema_telemetry.d.ts +0 -24
  97. package/dist/src/instrumentation/vercel/get_structured_output_schema_telemetry.js +0 -84
  98. package/dist/src/otel/vercel-ai-sdk-span-filter.d.ts +0 -12
  99. package/dist/src/otel/vercel-ai-sdk-span-filter.js +0 -88
@@ -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;
@@ -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;