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
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ /**
3
+ * Remove media payloads (base64 images, audio, documents) from the message
4
+ * content span attributes, right before export.
5
+ *
6
+ * An inline image, audio clip or document reaches the span in one of five shapes,
7
+ * depending on the instrumentor: a `blob` part carrying the base64 bytes in
8
+ * `content` (OpenAI chat completions, Anthropic), a `uri` part whose uri is a
9
+ * `data:` URI holding the same bytes (OpenAI Responses), Anthropic's own content
10
+ * block with the bytes under `source.data` (Bedrock), an OpenAI `file` block
11
+ * with the bytes under `file.file_data`, an OpenAI Responses `input_audio` block
12
+ * kept whole under `content` with the bytes under `content.input_audio.data`. A single
13
+ * image is hundreds of kilobytes, so keeping it would make every event on the
14
+ * queue that much bigger for nothing: the payload is never read back.
15
+ *
16
+ * Each of those becomes `{"type": "blob", "mime_type": "<mime_type>"}`, the same
17
+ * byte-less stub the google, cohere and bedrock instrumentation complements
18
+ * build themselves: the analyses reading these messages still see that the call
19
+ * carried content the stored text does not. A `uri` part pointing at a hosted
20
+ * image is a reference, not a payload, and is kept as is.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.stripMediaPayloads = stripMediaPayloads;
24
+ const get_data_uri_mime_type_1 = require("../get-data-uri-mime-type");
25
+ // The message content attributes whose JSON holds parts: the first two are
26
+ // `[{role, parts}]`, `gen_ai.system_instructions` is a flat parts array.
27
+ const MESSAGE_PARTS_ATTRIBUTES = [
28
+ 'gen_ai.input.messages',
29
+ 'gen_ai.output.messages',
30
+ 'gen_ai.system_instructions',
31
+ ];
32
+ function isJsonObject(value) {
33
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
34
+ }
35
+ function stripPart(part) {
36
+ if (!isJsonObject(part))
37
+ return part;
38
+ if (part.type === 'blob') {
39
+ return { type: 'blob', mime_type: String(part.mime_type ?? '') };
40
+ }
41
+ if (part.type === 'uri') {
42
+ const mimeType = (0, get_data_uri_mime_type_1.getDataUriMimeType)(String(part.uri ?? ''));
43
+ if (mimeType !== null)
44
+ return { type: 'blob', mime_type: mimeType };
45
+ }
46
+ // Anthropic's own content block, which the Bedrock instrumentor copies onto
47
+ // the span as is: the bytes sit under source.data whatever the block's type
48
+ // says ("image", "document", "video", "audio").
49
+ const source = part.source;
50
+ if (isJsonObject(source) && source.data != null) {
51
+ return { type: 'blob', mime_type: String(source.media_type ?? '') };
52
+ }
53
+ // OpenAI's file block (a PDF sent to chat completions). On Node the
54
+ // instrumentor already maps it to a blob part (see the openai complement that
55
+ // repairs its mime type), so this is the guard for the day it stops, and the
56
+ // mirror of the branch the Python SDK needs.
57
+ const fileBlock = part.file;
58
+ if (isJsonObject(fileBlock)) {
59
+ const mimeType = (0, get_data_uri_mime_type_1.getDataUriMimeType)(String(fileBlock.file_data ?? ''));
60
+ if (mimeType !== null)
61
+ return { type: 'blob', mime_type: mimeType };
62
+ }
63
+ // An input block the OpenAI Responses instrumentor does not recognize is
64
+ // recorded as `{"type": "<block type>", "content": <the block itself>}`, so an
65
+ // `input_audio` block puts its base64 clip under `content.input_audio.data`.
66
+ const unrecognizedBlock = part.content;
67
+ const inlineAudio = isJsonObject(unrecognizedBlock) ? unrecognizedBlock.input_audio : null;
68
+ if (isJsonObject(inlineAudio) && inlineAudio.data != null) {
69
+ const audioFormat = inlineAudio.format;
70
+ return {
71
+ type: 'blob',
72
+ mime_type: audioFormat ? `audio/${audioFormat}` : 'audio/unknown',
73
+ };
74
+ }
75
+ // Mistral's document block. Its own tracing names the block by its type and
76
+ // drops the payload, so what arrives is usually `{"type": "document_url"}`
77
+ // already; this is what keeps the bytes off the queue if it stops dropping
78
+ // them.
79
+ const documentMimeType = (0, get_data_uri_mime_type_1.getDataUriMimeType)(String(part.document_url ?? ''));
80
+ if (documentMimeType !== null)
81
+ return { type: 'blob', mime_type: documentMimeType };
82
+ return part;
83
+ }
84
+ /**
85
+ * `items` with `stripItem` applied to each of them, or `items` itself when
86
+ * every item came back unchanged - an attribute carrying no media then keeps
87
+ * the instrumentor's exact JSON string.
88
+ */
89
+ function stripList(items, stripItem) {
90
+ const stripped = items.map(stripItem);
91
+ return stripped.every((item, index) => item === items[index]) ? items : stripped;
92
+ }
93
+ /**
94
+ * One entry of a parts attribute: a message whose `parts` are stripped, or a
95
+ * bare part when the attribute is a flat parts array.
96
+ */
97
+ function stripPartsEntry(entry) {
98
+ if (isJsonObject(entry) && Array.isArray(entry.parts)) {
99
+ const parts = stripList(entry.parts, stripPart);
100
+ return parts === entry.parts ? entry : { ...entry, parts };
101
+ }
102
+ return stripPart(entry);
103
+ }
104
+ /**
105
+ * The attribute's JSON with the media stripped from each of its entries, or
106
+ * null when there was nothing to strip.
107
+ */
108
+ function getStrippedAttribute(value) {
109
+ if (typeof value !== 'string')
110
+ return null;
111
+ let entries;
112
+ try {
113
+ entries = JSON.parse(value);
114
+ }
115
+ catch {
116
+ return null;
117
+ }
118
+ if (!Array.isArray(entries))
119
+ return null;
120
+ const stripped = stripList(entries, stripPartsEntry);
121
+ return stripped === entries ? null : JSON.stringify(stripped);
122
+ }
123
+ /**
124
+ * Rewrite in place every message content attribute of `attributes` that carries
125
+ * media bytes.
126
+ */
127
+ function stripMediaPayloads(attributes) {
128
+ for (const key of MESSAGE_PARTS_ATTRIBUTES) {
129
+ if (key in attributes) {
130
+ const stripped = getStrippedAttribute(attributes[key]);
131
+ if (stripped !== null)
132
+ attributes[key] = stripped;
133
+ }
134
+ }
135
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Send the metadata of a call to the Weflayr AI gateway in a request header.
3
+ */
4
+ export declare const WEFLAYR_METADATA_HEADER = "x-weflayr-metadata";
5
+ export declare const PUBLIC_GATEWAY_HOST = "gateway.weflayr.com";
6
+ export declare const _state: {
7
+ gatewayHostAndPort: string | undefined;
8
+ openaiClientsPatched: boolean;
9
+ };
10
+ /**
11
+ * The current scope's metadata as the JSON `WEFLAYR_METADATA_HEADER` holds.
12
+ *
13
+ * Returns `undefined` outside any `propagateMetadata` and `setCallName` scope,
14
+ * where there is nothing to send. Inside
15
+ * `propagateMetadata({ featureName: 'onboarding', customerName: 'Acme' })`,
16
+ * returns `'{"feature_name":"onboarding","customer_name":"Acme",
17
+ * "feature_run_uuid":"e3b0c442..."}'`.
18
+ */
19
+ export declare function buildMetadataHeaderValue(): string | undefined;
20
+ /**
21
+ * Send `propagateMetadata` and `setCallName` values to the Weflayr AI gateway.
22
+ *
23
+ * Call it once at startup. Every request an `openai` client then sends to the
24
+ * gateway carries the metadata of the scope it is made in, in the
25
+ * `x-weflayr-metadata` header. Requests to any other host are left alone, and
26
+ * clients built before this call are covered too, since the patch is on the
27
+ * openai client class rather than on an instance.
28
+ *
29
+ * No event is exported for a call sent to the gateway: the gateway reports that
30
+ * call itself, so exporting the SDK's span too would store the call twice.
31
+ *
32
+ * @param gatewayBaseUrl The gateway's base URL when it is not the public
33
+ * `https://gateway.weflayr.com`, for instance a gateway running locally. Only
34
+ * its host and port are compared, and calling this again without it keeps the
35
+ * gateway already named.
36
+ */
37
+ export declare function enablePropagateMetadataToGateway(gatewayBaseUrl?: string): void;
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+ /**
3
+ * Send the metadata of a call to the Weflayr AI gateway in a request header.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports._state = exports.PUBLIC_GATEWAY_HOST = exports.WEFLAYR_METADATA_HEADER = void 0;
7
+ exports.buildMetadataHeaderValue = buildMetadataHeaderValue;
8
+ exports.enablePropagateMetadataToGateway = enablePropagateMetadataToGateway;
9
+ const api_1 = require("@opentelemetry/api");
10
+ const logger_1 = require("./logger");
11
+ const propagation_1 = require("./otel/propagation");
12
+ const span_filter_1 = require("./otel/span-filter");
13
+ // The gateway reads a call's metadata from this header, as a JSON object. A
14
+ // header value has to be ASCII, so a customer name written with an accent
15
+ // travels as JSON's own \uXXXX escape (see escapeNonAscii).
16
+ exports.WEFLAYR_METADATA_HEADER = 'x-weflayr-metadata';
17
+ exports.PUBLIC_GATEWAY_HOST = 'gateway.weflayr.com';
18
+ // The port a base URL points at when it names none, so "https://host/v1" and
19
+ // "https://host:443/v1" are read as the same gateway.
20
+ const DEFAULT_PORT_BY_SCHEME = { 'http:': '80', 'https:': '443' };
21
+ // The instrumentation scopes whose spans cover one openai call: only such a span
22
+ // is the call's own, and only it is stamped as sent to the gateway. Both SDKs'
23
+ // spellings are listed so the two stay identical.
24
+ const OPENAI_INSTRUMENTATION_SCOPES = [
25
+ 'opentelemetry.instrumentation.openai',
26
+ 'opentelemetry.instrumentation.openai_v2',
27
+ '@traceloop/instrumentation-openai',
28
+ 'weflayr.instrumentation.openai',
29
+ ];
30
+ // Span attribute the value is read from -> key the gateway reads it under.
31
+ const HEADER_KEY_BY_SPAN_ATTRIBUTE = {
32
+ [propagation_1.FEATURE_NAME_ATTR]: 'feature_name',
33
+ [propagation_1.CUSTOMER_NAME_ATTR]: 'customer_name',
34
+ [propagation_1.CALL_NAME_ATTR]: 'call_name',
35
+ [propagation_1.UUID_ATTR]: 'feature_run_uuid',
36
+ };
37
+ exports._state = { gatewayHostAndPort: undefined, openaiClientsPatched: false };
38
+ /**
39
+ * Rewrite every character above ASCII as JSON's `\uXXXX` escape.
40
+ *
41
+ * `JSON.stringify` leaves them as they are, and a header value can only hold
42
+ * ASCII. Each UTF-16 code unit is escaped on its own, which is what JSON asks
43
+ * for: an emoji's surrogate pair becomes the two escapes `\ud83d\ude00`.
44
+ *
45
+ * Example: `escapeNonAscii('{"customer_name":"Café"}')` returns
46
+ * `'{"customer_name":"Caf\u00e9"}'`.
47
+ */
48
+ function escapeNonAscii(json) {
49
+ return json.replace(/[^\x00-\x7F]/g, (character) => {
50
+ const codeUnit = character.charCodeAt(0).toString(16).padStart(4, '0');
51
+ return `\\u${codeUnit}`;
52
+ });
53
+ }
54
+ /**
55
+ * The current scope's metadata as the JSON `WEFLAYR_METADATA_HEADER` holds.
56
+ *
57
+ * Returns `undefined` outside any `propagateMetadata` and `setCallName` scope,
58
+ * where there is nothing to send. Inside
59
+ * `propagateMetadata({ featureName: 'onboarding', customerName: 'Acme' })`,
60
+ * returns `'{"feature_name":"onboarding","customer_name":"Acme",
61
+ * "feature_run_uuid":"e3b0c442..."}'`.
62
+ */
63
+ function buildMetadataHeaderValue() {
64
+ const metadata = (0, propagation_1.getMergedMetadataFromContext)();
65
+ if (propagation_1.BILLING_PROVIDER_ATTR in metadata) {
66
+ process.emitWarning('propagateMetadata: `providerNameOverride` is ignored for a call sent to the ' +
67
+ 'Weflayr AI gateway, which reports the provider it called itself');
68
+ }
69
+ const payload = {};
70
+ for (const [spanAttribute, key] of Object.entries(HEADER_KEY_BY_SPAN_ATTRIBUTE)) {
71
+ if (spanAttribute in metadata) {
72
+ payload[key] = String(metadata[spanAttribute]);
73
+ }
74
+ }
75
+ const tags = {};
76
+ for (const [spanAttribute, value] of Object.entries(metadata)) {
77
+ if (spanAttribute.startsWith(propagation_1.EXTRA_TAG_PREFIX)) {
78
+ tags[spanAttribute.slice(propagation_1.EXTRA_TAG_PREFIX.length)] = String(value);
79
+ }
80
+ }
81
+ if (Object.keys(tags).length > 0) {
82
+ payload.tags = tags;
83
+ }
84
+ if (Object.keys(payload).length === 0) {
85
+ return undefined;
86
+ }
87
+ return escapeNonAscii(JSON.stringify(payload));
88
+ }
89
+ /**
90
+ * The host and port a base URL points at, the port filled in from the scheme
91
+ * when the URL names none.
92
+ *
93
+ * Example: `readHostAndPort('https://gateway.weflayr.com/v1')` returns
94
+ * `'gateway.weflayr.com:443'`. Throws on a string that names no host, which
95
+ * includes `'localhost:8787'`: with no scheme, `URL` reads `localhost:` as the
96
+ * scheme and leaves the host empty.
97
+ */
98
+ function readHostAndPort(baseUrl) {
99
+ const parsed = new URL(baseUrl);
100
+ if (!parsed.hostname) {
101
+ throw new Error(`no host in ${baseUrl}`);
102
+ }
103
+ const port = parsed.port || DEFAULT_PORT_BY_SCHEME[parsed.protocol] || '';
104
+ return `${parsed.hostname.toLowerCase()}:${port}`;
105
+ }
106
+ /** Whether a client's base URL is the gateway `enable...` was told about. */
107
+ function isWeflayrGatewayBaseUrl(baseUrl) {
108
+ const hostAndPort = readHostAndPort(baseUrl);
109
+ if (exports._state.gatewayHostAndPort !== undefined) {
110
+ return hostAndPort === exports._state.gatewayHostAndPort;
111
+ }
112
+ const hostname = hostAndPort.slice(0, hostAndPort.lastIndexOf(':'));
113
+ return hostname === exports.PUBLIC_GATEWAY_HOST || hostname.endsWith(`.${exports.PUBLIC_GATEWAY_HOST}`);
114
+ }
115
+ /** Stamp the span of the openai call being sent, when one is being recorded. */
116
+ function markCurrentSpanAsSentToGateway() {
117
+ const span = api_1.trace.getActiveSpan();
118
+ if (!span || !span.isRecording()) {
119
+ return;
120
+ }
121
+ const scopeName = span.instrumentationScope?.name ?? '';
122
+ const isOpenaiCallSpan = OPENAI_INSTRUMENTATION_SCOPES.some((scope) => scopeName === scope || scopeName.startsWith(`${scope}.`));
123
+ if (isOpenaiCallSpan) {
124
+ span.setAttribute(span_filter_1.SENT_TO_WEFLAYR_GATEWAY_ATTR, true);
125
+ }
126
+ }
127
+ /**
128
+ * Add the metadata header to `options` when the client calls the gateway.
129
+ *
130
+ * Swallows its own failures: this runs inside the app's own LLM call, which
131
+ * telemetry must never break. The caller's own headers are kept whichever shape
132
+ * they were passed in (a `Headers`, an array of pairs or a plain object), which
133
+ * is why they go through `Headers` rather than an object spread.
134
+ *
135
+ * The span is marked before the header is built, not after: the request is sent
136
+ * to the gateway either way, so a span left unmarked because building the header
137
+ * threw would be exported on top of the event the gateway reports, storing one
138
+ * call twice and doubling its cost. A call whose header never made it is stored
139
+ * once, with no feature, customer or call name.
140
+ */
141
+ function addMetadataHeader(client, options) {
142
+ try {
143
+ if (!isWeflayrGatewayBaseUrl(String(client?.baseURL ?? ''))) {
144
+ return;
145
+ }
146
+ markCurrentSpanAsSentToGateway();
147
+ const headerValue = buildMetadataHeaderValue();
148
+ if (headerValue !== undefined) {
149
+ const headers = new Headers(options.headers ?? undefined);
150
+ headers.set(exports.WEFLAYR_METADATA_HEADER, headerValue);
151
+ options.headers = headers;
152
+ }
153
+ }
154
+ catch {
155
+ (0, logger_1.debug)('enablePropagateMetadataToGateway: could not add the metadata header');
156
+ }
157
+ }
158
+ /**
159
+ * Send `propagateMetadata` and `setCallName` values to the Weflayr AI gateway.
160
+ *
161
+ * Call it once at startup. Every request an `openai` client then sends to the
162
+ * gateway carries the metadata of the scope it is made in, in the
163
+ * `x-weflayr-metadata` header. Requests to any other host are left alone, and
164
+ * clients built before this call are covered too, since the patch is on the
165
+ * openai client class rather than on an instance.
166
+ *
167
+ * No event is exported for a call sent to the gateway: the gateway reports that
168
+ * call itself, so exporting the SDK's span too would store the call twice.
169
+ *
170
+ * @param gatewayBaseUrl The gateway's base URL when it is not the public
171
+ * `https://gateway.weflayr.com`, for instance a gateway running locally. Only
172
+ * its host and port are compared, and calling this again without it keeps the
173
+ * gateway already named.
174
+ */
175
+ function enablePropagateMetadataToGateway(gatewayBaseUrl) {
176
+ if (gatewayBaseUrl !== undefined) {
177
+ let hostAndPort;
178
+ try {
179
+ hostAndPort = readHostAndPort(gatewayBaseUrl);
180
+ }
181
+ catch {
182
+ throw new Error('weflayr.enablePropagateMetadataToGateway: `gatewayBaseUrl` needs a scheme ' +
183
+ `and a host, e.g. http://localhost:8787/v1, got: ${gatewayBaseUrl}`);
184
+ }
185
+ exports._state.gatewayHostAndPort = hostAndPort;
186
+ }
187
+ if (exports._state.openaiClientsPatched) {
188
+ return;
189
+ }
190
+ let OpenAI;
191
+ try {
192
+ ({ OpenAI } = require('openai'));
193
+ }
194
+ catch (err) {
195
+ throw new Error('weflayr.enablePropagateMetadataToGateway: the openai client is required to ' +
196
+ 'call the Weflayr AI gateway. Install it with: npm install openai\n' +
197
+ `Underlying error: ${err.message}`);
198
+ }
199
+ // `prepareOptions` is the openai client's documented hook for mutating a
200
+ // request's options, and it runs for every endpoint, streaming or not.
201
+ const originalPrepareOptions = OpenAI.prototype.prepareOptions;
202
+ if (typeof originalPrepareOptions !== 'function') {
203
+ throw new Error('weflayr.enablePropagateMetadataToGateway: the openai client has no ' +
204
+ 'prepareOptions hook, so the installed openai version cannot send the ' +
205
+ 'metadata header.');
206
+ }
207
+ OpenAI.prototype.prepareOptions = async function prepareOptions(options) {
208
+ await originalPrepareOptions.call(this, options);
209
+ addMetadataHeader(this, options);
210
+ };
211
+ exports._state.openaiClientsPatched = true;
212
+ (0, logger_1.debug)('enablePropagateMetadataToGateway: patched the openai client gatewayHostAndPort=%s', exports._state.gatewayHostAndPort ?? exports.PUBLIC_GATEWAY_HOST);
213
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weflayr",
3
- "version": "0.26.1",
3
+ "version": "0.28.0",
4
4
  "description": "Weflayr JS/TS SDK — instrument any LLM client",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
@@ -13,7 +13,7 @@
13
13
  "generate-api": "openapi-ts && node scripts/generate_facade.mjs",
14
14
  "pretest": "npm run build",
15
15
  "prepublishOnly": "npm run build",
16
- "test": "node --test",
16
+ "test": "node --test \"dist/tests/**/*.test.js\"",
17
17
  "lint": "eslint src tests",
18
18
  "lint:fix": "eslint src tests --fix",
19
19
  "format": "prettier --write src tests",
@@ -31,19 +31,22 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@opentelemetry/api": "^1.9.1",
34
- "@opentelemetry/exporter-trace-otlp-http": "^0.219.0",
35
- "@opentelemetry/instrumentation": "^0.219.0",
34
+ "@opentelemetry/exporter-trace-otlp-http": "^0.222.0",
35
+ "@opentelemetry/instrumentation": "^0.222.0",
36
36
  "@opentelemetry/resources": "^2.8.0",
37
37
  "@opentelemetry/sdk-trace-base": "^2.8.0",
38
38
  "@opentelemetry/sdk-trace-node": "^2.8.0",
39
39
  "async-retry": "^1.3.3"
40
40
  },
41
+ "overrides": {
42
+ "js-yaml": "^4.3.1"
43
+ },
41
44
  "devDependencies": {
42
45
  "@hey-api/openapi-ts": "^0.99.0",
43
46
  "@opentelemetry/context-async-hooks": "^2.8.0",
44
47
  "@types/async-retry": "^1.4.9",
45
48
  "@types/node": "^26.1.1",
46
- "eslint": "^9.18.0",
49
+ "eslint": "^10.8.0",
47
50
  "prettier": "^3.4.2",
48
51
  "typescript": "^6.0.3",
49
52
  "typescript-eslint": "^8.65.0"
@@ -1,3 +0,0 @@
1
- export declare class VercelAiSdkInstrumentor {
2
- instrument(): void;
3
- }
@@ -1,54 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.VercelAiSdkInstrumentor = void 0;
4
- /**
5
- * Instrumentor for the Vercel AI SDK (`ai` package, v7+).
6
- *
7
- * The AI SDK ships its own OTel integration in `@ai-sdk/otel`; instrumenting
8
- * is registering it globally so every generateText/streamText/embed call emits
9
- * GenAI semconv spans through the tracer provider weflayr's setup() installed.
10
- * `usage: true` adds the `ai.usage.*` token details (reasoning tokens, uncached
11
- * input) that the semconv attributes do not carry; `providerMetadata: true`
12
- * opts into `ai.response.providerMetadata` on the spans that carry it.
13
- *
14
- * The aggregate spans the integration emits around each model call
15
- * (invoke_agent, agent_step, duplicated embeddings spans) are filtered out
16
- * before export by otel/vercel-ai-sdk-span-filter.js.
17
- */
18
- const get_cache_creation_telemetry_1 = require("./get_cache_creation_telemetry");
19
- const get_structured_output_schema_telemetry_1 = require("./get_structured_output_schema_telemetry");
20
- class VercelAiSdkInstrumentor {
21
- instrument() {
22
- let registerTelemetry;
23
- let OpenTelemetry;
24
- try {
25
- ({ registerTelemetry } = require('ai'));
26
- ({ OpenTelemetry } = require('@ai-sdk/otel'));
27
- }
28
- catch (err) {
29
- throw new Error('weflayr.autoInstrument("vercel_ai_gateway"): failed to load "ai" / "@ai-sdk/otel". ' +
30
- `Install them with: npm install ai @ai-sdk/otel\nUnderlying error: ${err.message}`);
31
- }
32
- // registerTelemetry only exists since v7.
33
- if (typeof registerTelemetry !== 'function') {
34
- throw new Error('weflayr.autoInstrument("vercel_ai_gateway") requires version 7+ of the Vercel AI SDK ' +
35
- '(the "ai" package), which added global telemetry registration. ' +
36
- 'Upgrade with: npm install ai@latest @ai-sdk/otel.\n' +
37
- 'On ai <= 6 telemetry can only be enabled per call: remove "vercel_ai_gateway" from ' +
38
- 'aiSdks (pass an empty list if no other sdks need to be instrumented). Instead, make sure ' +
39
- 'experimental_telemetry: { isEnabled: true } is passed on each generateText/streamText call.');
40
- }
41
- const dispatcher = new OpenTelemetry({ usage: true, providerMetadata: true });
42
- // The integration never emits the raw provider usage, the only place the
43
- // written cache tokens per TTL (Anthropic's `cache_creation`) are
44
- // reported; this complement stamps it on the model-call span (see
45
- // get_cache_creation_telemetry).
46
- new get_cache_creation_telemetry_1.VercelCacheCreationInstrumentor().instrument(dispatcher);
47
- // generateObject's schema is only stamped on the root span the weflayr
48
- // filter drops; this complement re-stamps it on the model-call span (see
49
- // get_structured_output_schema_telemetry).
50
- new get_structured_output_schema_telemetry_1.VercelStructuredOutputSchemaInstrumentor().instrument(dispatcher);
51
- registerTelemetry(dispatcher);
52
- }
53
- }
54
- exports.VercelAiSdkInstrumentor = VercelAiSdkInstrumentor;
@@ -1,22 +0,0 @@
1
- /**
2
- * Cache-write capture for Vercel AI SDK (ai >= 7) model-call spans.
3
- *
4
- * Anthropic reports the written cache tokens per TTL in its raw usage
5
- * (`usage.cache_creation`, ephemeral_5m/1h buckets — billed at different
6
- * rates). ai >= 7 carries that verbatim on the model-call telemetry event as
7
- * `event.usage.raw` (a documented field of LanguageModelUsage), but the
8
- * `@ai-sdk/otel` integration never emits it, so the span loses it.
9
- *
10
- * Wrap the telemetry dispatcher's `onLanguageModelCallEnd(event)` (a public
11
- * callback of the telemetry interface) to read `usage.cache_creation` off the
12
- * raw usage and stamp it verbatim as `weflayr.cache.creation` on the
13
- * still-open model-call span (reached through the integration's call state)
14
- * before the original handler ends it; the normalizer owns the mapping to TTL
15
- * tiers.
16
- */
17
- export declare function cacheCreationFromRawUsage(rawUsage: any): Record<string, unknown> | undefined;
18
- export declare class VercelCacheCreationInstrumentor {
19
- private _patched;
20
- instrument(dispatcher: any): void;
21
- uninstrument(): void;
22
- }
@@ -1,66 +0,0 @@
1
- "use strict";
2
- /**
3
- * Cache-write capture for Vercel AI SDK (ai >= 7) model-call spans.
4
- *
5
- * Anthropic reports the written cache tokens per TTL in its raw usage
6
- * (`usage.cache_creation`, ephemeral_5m/1h buckets — billed at different
7
- * rates). ai >= 7 carries that verbatim on the model-call telemetry event as
8
- * `event.usage.raw` (a documented field of LanguageModelUsage), but the
9
- * `@ai-sdk/otel` integration never emits it, so the span loses it.
10
- *
11
- * Wrap the telemetry dispatcher's `onLanguageModelCallEnd(event)` (a public
12
- * callback of the telemetry interface) to read `usage.cache_creation` off the
13
- * raw usage and stamp it verbatim as `weflayr.cache.creation` on the
14
- * still-open model-call span (reached through the integration's call state)
15
- * before the original handler ends it; the normalizer owns the mapping to TTL
16
- * tiers.
17
- */
18
- Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.VercelCacheCreationInstrumentor = void 0;
20
- exports.cacheCreationFromRawUsage = cacheCreationFromRawUsage;
21
- const CACHE_CREATION_ATTR = 'weflayr.cache.creation';
22
- // The raw `usage.cache_creation` of a provider usage, or undefined when the
23
- // provider reports none (or reports only zeros — non-caching calls carry no
24
- // attribute).
25
- function cacheCreationFromRawUsage(rawUsage) {
26
- const cacheCreation = rawUsage && rawUsage.cache_creation;
27
- if (!cacheCreation)
28
- return undefined;
29
- return Object.values(cacheCreation).some(Boolean) ? cacheCreation : undefined;
30
- }
31
- class VercelCacheCreationInstrumentor {
32
- // [owner, methodName, originalFunction] for uninstrument().
33
- _patched = [];
34
- // No-op (and never throws) if the interface is missing — the worst case is
35
- // the cache-write capture staying empty, exactly the pre-complement
36
- // behaviour.
37
- instrument(dispatcher) {
38
- if (!dispatcher || typeof dispatcher.onLanguageModelCallEnd !== 'function')
39
- return;
40
- const original = dispatcher.onLanguageModelCallEnd;
41
- dispatcher.onLanguageModelCallEnd = function onLanguageModelCallEndWithCacheCreation(event) {
42
- try {
43
- const cacheCreation = cacheCreationFromRawUsage(event && event.usage && event.usage.raw);
44
- if (cacheCreation) {
45
- const callState = this.getCallState && this.getCallState(event.callId);
46
- const span = callState && callState.inferenceSpan;
47
- if (span && typeof span.setAttribute === 'function') {
48
- span.setAttribute(CACHE_CREATION_ATTR, JSON.stringify(cacheCreation));
49
- }
50
- }
51
- }
52
- catch {
53
- // Never break span finalization over an enrichment failure.
54
- }
55
- return original.call(this, event);
56
- };
57
- this._patched.push([dispatcher, 'onLanguageModelCallEnd', original]);
58
- }
59
- uninstrument() {
60
- for (const [owner, methodName, original] of this._patched) {
61
- owner[methodName] = original;
62
- }
63
- this._patched = [];
64
- }
65
- }
66
- exports.VercelCacheCreationInstrumentor = VercelCacheCreationInstrumentor;
@@ -1,24 +0,0 @@
1
- /**
2
- * Structured-output schema capture for Vercel AI SDK (ai >= 7) model-call spans.
3
- *
4
- * A `generateObject` call's schema is only ever emitted on the operation's ROOT
5
- * span (`onObjectOperationStart` stamps `ai.schema` there under the
6
- * supplemental-attributes opt-in) — and weflayr's span filter drops that
7
- * aggregate span, keeping one span per model call. So the schema never reaches
8
- * the exported span, even though the gateway bills it as input.
9
- *
10
- * The operation and its nested model call share one `callId` in the
11
- * dispatcher's call state. Wrap `onObjectOperationStart` to remember the
12
- * event's schema by callId, and `onObjectStepEnd` — the object operation's own
13
- * model-call hook, where the call state's `inferenceSpan` exists (object
14
- * operations never fire `onLanguageModelCallEnd`; verified live) — to stamp it
15
- * as `ai.schema` on that span, the attribute the worker already reads.
16
- * `cleanupCallState` is wrapped so an operation that errors before its model
17
- * call does not leak its entry.
18
- */
19
- export declare class VercelStructuredOutputSchemaInstrumentor {
20
- private _patched;
21
- private _schemasByCallId;
22
- instrument(dispatcher: any): void;
23
- uninstrument(): void;
24
- }