weflayr 0.22.2 → 0.24.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 (42) hide show
  1. package/dist/api/generated/facade.gen.d.ts +102 -0
  2. package/dist/api/generated/facade.gen.js +8 -1
  3. package/dist/api/generated/index.d.ts +2 -2
  4. package/dist/api/generated/index.js +8 -1
  5. package/dist/api/generated/sdk.gen.d.ts +29 -1
  6. package/dist/api/generated/sdk.gen.js +64 -1
  7. package/dist/api/generated/types.gen.d.ts +356 -3
  8. package/index.d.ts +5 -2
  9. package/package.json +1 -1
  10. package/src/api/generated/facade.gen.ts +42 -0
  11. package/src/api/generated/index.ts +2 -2
  12. package/src/api/generated/sdk.gen.ts +64 -1
  13. package/src/api/generated/types.gen.ts +400 -3
  14. package/src/auto-instrument.js +1 -0
  15. package/src/instrumentation/anthropic/get_cache_and_reasoning_telemetry.js +11 -0
  16. package/src/instrumentation/anthropic/get_cache_config_telemetry.js +90 -0
  17. package/src/instrumentation/anthropic/get_tool_definitions_telemetry.js +6 -1
  18. package/src/instrumentation/anthropic/set_telemetry_attributes.js +2 -0
  19. package/src/instrumentation/bedrock/get_cache_config_telemetry.js +106 -0
  20. package/src/instrumentation/bedrock/get_cache_telemetry.js +11 -6
  21. package/src/instrumentation/bedrock/set_telemetry_attributes.js +6 -2
  22. package/src/instrumentation/google/get_cache_operations_telemetry.js +171 -0
  23. package/src/instrumentation/google/get_cache_telemetry.js +16 -2
  24. package/src/instrumentation/google/set_telemetry_attributes.js +8 -2
  25. package/src/instrumentation/openai/get_cache_config_telemetry.js +63 -0
  26. package/src/instrumentation/openai/set_telemetry_attributes.js +2 -0
  27. package/src/instrumentation/vercel/full_instrumentation.js +10 -2
  28. package/src/instrumentation/vercel/get_cache_creation_telemetry.js +70 -0
  29. package/src/otel/propagation.js +22 -9
  30. package/src/otel/span-filter.js +12 -12
  31. package/src/otel/span-processor.js +8 -0
  32. package/src/otel/vercel-ai-sdk-span-filter.js +1 -1
  33. package/tests/api/index.test.js +76 -0
  34. package/tests/index.test.js +67 -7
  35. package/tests/instrumentation/anthropic/get_cache_and_reasoning_telemetry.test.js +23 -0
  36. package/tests/instrumentation/anthropic/get_cache_config_telemetry.test.js +92 -0
  37. package/tests/instrumentation/anthropic/get_tool_definitions_telemetry.test.js +15 -0
  38. package/tests/instrumentation/bedrock/get_cache_config_telemetry.test.js +107 -0
  39. package/tests/instrumentation/bedrock/get_cache_telemetry.test.js +16 -0
  40. package/tests/instrumentation/google/get_cache_operations_telemetry.test.js +130 -0
  41. package/tests/instrumentation/openai/get_cache_config_telemetry.test.js +83 -0
  42. package/tests/instrumentation/vercel/get_cache_creation_telemetry.test.js +96 -0
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Request-side prompt-caching config capture for Bedrock invoke_model spans.
5
+ *
6
+ * Bedrock callers opt into caching in the request body, in the model vendor's
7
+ * own schema: Anthropic-on-Bedrock bodies carry `cache_control` blocks (the
8
+ * native Anthropic shape), Amazon Nova bodies carry `cachePoint` blocks (the
9
+ * Converse shape) — on system entries, message content blocks or tool
10
+ * definitions, each with an optional raw `ttl` string ("5m"/"1h"). The
11
+ * traceloop instrumentor never records them.
12
+ *
13
+ * Wrap the instrumentor's `_startSpan({ params })` (params.body is the raw
14
+ * request JSON) to stamp every breakpoint found as `weflayr.cache.breakpoints`.
15
+ * Bedrock responses report no TTL split of the written tokens; the worker
16
+ * derives it from these breakpoints when they all share one TTL.
17
+ */
18
+
19
+ const { extractCacheBreakpoints } = require('../anthropic/get_cache_config_telemetry');
20
+
21
+ const BREAKPOINTS_ATTR = 'weflayr.cache.breakpoints';
22
+
23
+ // A breakpoint as location + the raw `cachePoint` `ttl` string, omitted when
24
+ // the request sets none.
25
+ function _cachePointEntry(location, cachePoint) {
26
+ const ttl = cachePoint && cachePoint.ttl;
27
+ return ttl ? { location, ttl: String(ttl) } : { location };
28
+ }
29
+
30
+ // The first `cachePoint` among a list of blocks (attached to a content block,
31
+ // or standing alone as its own block), or undefined.
32
+ function _cachePointOfBlocks(blocks) {
33
+ if (!Array.isArray(blocks)) return undefined;
34
+ const block = blocks.find((entry) => entry && entry.cachePoint != null);
35
+ return block && block.cachePoint;
36
+ }
37
+
38
+ // Every cache breakpoint in an InvokeModel request body, as location + the
39
+ // raw `ttl` string (omitted when the breakpoint sets none): the
40
+ // Anthropic-on-Bedrock `cache_control` shape and the Nova `cachePoint` shape
41
+ // (which never overlap in one body).
42
+ function extractBedrockCacheBreakpoints(body) {
43
+ const breakpoints = extractCacheBreakpoints(body);
44
+ const systemCachePoint = _cachePointOfBlocks(body.system);
45
+ if (systemCachePoint != null) {
46
+ breakpoints.push(_cachePointEntry('system', systemCachePoint));
47
+ }
48
+ (body.messages || []).forEach((message, index) => {
49
+ const cachePoint = _cachePointOfBlocks(message && message.content);
50
+ if (cachePoint != null) {
51
+ breakpoints.push(_cachePointEntry(`messages[${index}]`, cachePoint));
52
+ }
53
+ });
54
+ const tools = (body.toolConfig && body.toolConfig.tools) || [];
55
+ tools.forEach((tool, index) => {
56
+ if (tool && tool.cachePoint != null) {
57
+ breakpoints.push(_cachePointEntry(`tools[${index}]`, tool.cachePoint));
58
+ }
59
+ });
60
+ return breakpoints;
61
+ }
62
+
63
+ class BedrockCacheConfigInstrumentor {
64
+ constructor() {
65
+ // [instrumentation, methodName, originalFunction] for uninstrument().
66
+ this._patched = [];
67
+ }
68
+
69
+ // No-op (and never throws) if the instrumentor's internals have changed shape —
70
+ // the worst case is missing cache-config capture, never a crash.
71
+ instrument(instrumentation) {
72
+ if (!instrumentation) return;
73
+ const startSpanOriginal = instrumentation._startSpan;
74
+ if (typeof startSpanOriginal !== 'function') return;
75
+
76
+ instrumentation._startSpan = function startSpanWithCacheConfig(request) {
77
+ const span = startSpanOriginal.call(this, request);
78
+ try {
79
+ const rawBody =
80
+ request && request.params && request.params.input && request.params.input.body;
81
+ if (typeof rawBody === 'string' || rawBody instanceof Uint8Array) {
82
+ const body = JSON.parse(
83
+ typeof rawBody === 'string' ? rawBody : new TextDecoder().decode(rawBody)
84
+ );
85
+ const breakpoints = extractBedrockCacheBreakpoints(body || {});
86
+ if (breakpoints.length) {
87
+ span.setAttribute(BREAKPOINTS_ATTR, JSON.stringify(breakpoints));
88
+ }
89
+ }
90
+ } catch {
91
+ // Never break span creation over an enrichment failure.
92
+ }
93
+ return span;
94
+ };
95
+ this._patched.push([instrumentation, '_startSpan', startSpanOriginal]);
96
+ }
97
+
98
+ uninstrument() {
99
+ for (const [owner, methodName, original] of this._patched) {
100
+ owner[methodName] = original;
101
+ }
102
+ this._patched = [];
103
+ }
104
+ }
105
+
106
+ module.exports = { BedrockCacheConfigInstrumentor, extractBedrockCacheBreakpoints };
@@ -7,7 +7,9 @@
7
7
  * off the InvokeModel response body but never reads its cache-token counts, so
8
8
  * spans for prompt-cache hits carry no cached-token figures. Amazon Nova's native
9
9
  * InvokeModel response includes `usage.cacheReadInputTokenCount` /
10
- * `usage.cacheWriteInputTokenCount`; this maps them to the
10
+ * `usage.cacheWriteInputTokenCount` (Anthropic-on-Bedrock responses carry the
11
+ * native `usage.cache_read_input_tokens` / `cache_creation_input_tokens`);
12
+ * this maps them to the
11
13
  * `gen_ai.usage.cache_read_input_tokens` / `cache_creation_input_tokens`
12
14
  * attributes the worker reads, matching the Python SDK.
13
15
  *
@@ -19,14 +21,17 @@
19
21
  const CACHE_READ_INPUT_TOKENS_ATTR = 'gen_ai.usage.cache_read_input_tokens';
20
22
  const CACHE_CREATION_INPUT_TOKENS_ATTR = 'gen_ai.usage.cache_creation_input_tokens';
21
23
 
22
- // Stamp cache counts from a Nova `usage` object onto the span.
24
+ // Stamp cache counts from a `usage` object onto the span: Nova's camelCase
25
+ // count fields, or Anthropic-on-Bedrock's native snake_case ones.
23
26
  function _setCacheFromUsage(span, usage) {
24
27
  if (!usage || !span || !span.isRecording || !span.isRecording()) return;
25
- if (typeof usage.cacheReadInputTokenCount === 'number' && usage.cacheReadInputTokenCount) {
26
- span.setAttribute(CACHE_READ_INPUT_TOKENS_ATTR, usage.cacheReadInputTokenCount);
28
+ const read = usage.cacheReadInputTokenCount ?? usage.cache_read_input_tokens;
29
+ const write = usage.cacheWriteInputTokenCount ?? usage.cache_creation_input_tokens;
30
+ if (typeof read === 'number' && read) {
31
+ span.setAttribute(CACHE_READ_INPUT_TOKENS_ATTR, read);
27
32
  }
28
- if (typeof usage.cacheWriteInputTokenCount === 'number' && usage.cacheWriteInputTokenCount) {
29
- span.setAttribute(CACHE_CREATION_INPUT_TOKENS_ATTR, usage.cacheWriteInputTokenCount);
33
+ if (typeof write === 'number' && write) {
34
+ span.setAttribute(CACHE_CREATION_INPUT_TOKENS_ATTR, write);
30
35
  }
31
36
  }
32
37
 
@@ -5,10 +5,13 @@
5
5
  *
6
6
  * Wires the weflayr-supplied patches the traceloop Bedrock instrumentor misses:
7
7
  * cached-prompt token counts on the invoke_model span (Nova returns them in the
8
- * response body but traceloop never reads them) and the streamed output message
9
- * content (its stream path formats an empty-parts message).
8
+ * response body but traceloop never reads them), the request's cache
9
+ * breakpoints (`cache_control` / `cachePoint` blocks it never records) and the
10
+ * streamed output message content (its stream path formats an empty-parts
11
+ * message).
10
12
  */
11
13
 
14
+ const { BedrockCacheConfigInstrumentor } = require('./get_cache_config_telemetry');
12
15
  const { BedrockPromptCachingInstrumentor } = require('./get_cache_telemetry');
13
16
  const { BedrockInputMessageContentInstrumentor } = require('./get_input_message_content_telemetry');
14
17
  const {
@@ -19,6 +22,7 @@ class BedrockTelemetryInstrumentor {
19
22
  constructor() {
20
23
  this._subInstrumentors = [
21
24
  new BedrockPromptCachingInstrumentor(),
25
+ new BedrockCacheConfigInstrumentor(),
22
26
  new BedrockInputMessageContentInstrumentor(),
23
27
  new BedrockStreamingOutputMessageInstrumentor(),
24
28
  ];
@@ -0,0 +1,171 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Explicit-cache lifecycle telemetry for Google GenAI (Gemini).
5
+ *
6
+ * Explicit caching happens through API calls that are not LLM calls — the
7
+ * caller uploads content once via `client.caches.create` (returning a
8
+ * `cachedContents/{id}` name later referenced by `generateContent`), and may
9
+ * extend its TTL (`caches.update`) or remove it (`caches.delete`). No
10
+ * instrumentor covers these endpoints, so the cached content and its lifetime
11
+ * were invisible to the telemetry.
12
+ *
13
+ * One span per lifecycle call, carrying:
14
+ * - `weflayr.cache.operation` — create / update / delete
15
+ * - `weflayr.cache.cached_content_id` — the cache's server-generated name
16
+ * - `weflayr.cache.expire_time` — absolute expiry (create / update)
17
+ * - `gen_ai.usage.input_tokens` — the cached content's token count
18
+ * (create only; billed at the standard input rate)
19
+ * - the cached content itself (`gen_ai.input.messages` /
20
+ * `gen_ai.system_instructions` / `gen_ai.tool.definitions`) — the content
21
+ * is sent only on the create call and never repeated in the calls that use
22
+ * the cache, so this is the one place it can be captured. The span
23
+ * processor strips these when message-content capture is disabled.
24
+ */
25
+
26
+ const { trace, SpanStatusCode } = require('@opentelemetry/api');
27
+
28
+ const { getProviderName } = require('./get_provider_name');
29
+
30
+ const TRACER_NAME = 'weflayr.instrumentation.google.cache_operations';
31
+
32
+ const OPERATION_ATTR = 'weflayr.cache.operation';
33
+ const CACHED_CONTENT_ID_ATTR = 'weflayr.cache.cached_content_id';
34
+ const EXPIRE_TIME_ATTR = 'weflayr.cache.expire_time';
35
+
36
+ const OPERATIONS = ['create', 'update', 'delete'];
37
+
38
+ // The cached content mapped to the role+parts message shape the chat path uses.
39
+ function _contentToMessage(content) {
40
+ if (typeof content === 'string') {
41
+ return { role: 'user', parts: [{ type: 'text', content }] };
42
+ }
43
+ const role = (content && content.role) || 'user';
44
+ const rawParts = (content && content.parts) || [];
45
+ const parts = [];
46
+ for (const rawPart of rawParts) {
47
+ if (typeof rawPart === 'string') {
48
+ parts.push({ type: 'text', content: rawPart });
49
+ } else if (rawPart && typeof rawPart.text === 'string') {
50
+ parts.push({ type: 'text', content: rawPart.text });
51
+ } else if (rawPart && rawPart.inlineData) {
52
+ // Non-text parts (e.g. audio) become a byte-less blob stub so span
53
+ // attributes never carry raw media payloads.
54
+ parts.push({ type: 'blob', mime_type: rawPart.inlineData.mimeType || '' });
55
+ }
56
+ }
57
+ return { role, parts };
58
+ }
59
+
60
+ function _setSystemInstruction(span, systemInstruction) {
61
+ if (systemInstruction == null) return;
62
+ const texts =
63
+ typeof systemInstruction === 'string'
64
+ ? [systemInstruction]
65
+ : (systemInstruction.parts || [])
66
+ .map((part) => part && part.text)
67
+ .filter((text) => typeof text === 'string');
68
+ if (texts.length) {
69
+ span.setAttribute(
70
+ 'gen_ai.system_instructions',
71
+ JSON.stringify(texts.map((text) => ({ type: 'text', content: text })))
72
+ );
73
+ }
74
+ }
75
+
76
+ function _setToolDefinitions(span, config) {
77
+ const definitions = [];
78
+ for (const tool of (config && config.tools) || []) {
79
+ for (const declaration of (tool && tool.functionDeclarations) || []) {
80
+ definitions.push(declaration);
81
+ }
82
+ }
83
+ if (definitions.length) {
84
+ span.setAttribute('gen_ai.tool.definitions', JSON.stringify(definitions));
85
+ }
86
+ }
87
+
88
+ // Stamp the server-side facts of the cache from a create/update response.
89
+ function _stampCachedContent(span, cachedContent, operation) {
90
+ if (!cachedContent) return;
91
+ if (cachedContent.name) {
92
+ span.setAttribute(CACHED_CONTENT_ID_ATTR, String(cachedContent.name));
93
+ }
94
+ if (cachedContent.expireTime) {
95
+ span.setAttribute(EXPIRE_TIME_ATTR, String(cachedContent.expireTime));
96
+ }
97
+ // Gemini bills the cached tokens once, at creation; an update (TTL extension)
98
+ // returns the same usageMetadata but bills nothing.
99
+ if (operation !== 'create') return;
100
+ const totalTokens = cachedContent.usageMetadata && cachedContent.usageMetadata.totalTokenCount;
101
+ if (totalTokens) {
102
+ span.setAttribute('gen_ai.usage.input_tokens', totalTokens);
103
+ }
104
+ }
105
+
106
+ function _startSpan(operation, params, system) {
107
+ const span = trace.getTracer(TRACER_NAME).startSpan(`cache_${operation}`);
108
+ span.setAttribute('gen_ai.system', system);
109
+ span.setAttribute('gen_ai.operation.name', `cache_${operation}`);
110
+ span.setAttribute(OPERATION_ATTR, operation);
111
+ if (params && params.model) {
112
+ // The chat path strips the leading "models/" prefix; mirror it.
113
+ span.setAttribute('gen_ai.request.model', params.model.replace(/^models\//, ''));
114
+ }
115
+ // update/delete address the cache by name; create gets it from the response.
116
+ if (params && params.name) {
117
+ span.setAttribute(CACHED_CONTENT_ID_ATTR, String(params.name));
118
+ }
119
+ if (operation === 'create') {
120
+ const config = (params && params.config) || {};
121
+ if (config.contents != null) {
122
+ const contentsList = Array.isArray(config.contents) ? config.contents : [config.contents];
123
+ span.setAttribute(
124
+ 'gen_ai.input.messages',
125
+ JSON.stringify(contentsList.map(_contentToMessage))
126
+ );
127
+ }
128
+ _setSystemInstruction(span, config.systemInstruction);
129
+ _setToolDefinitions(span, config);
130
+ }
131
+ return span;
132
+ }
133
+
134
+ function _recordError(span, error) {
135
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error?.message ?? String(error) });
136
+ span.recordException(error);
137
+ }
138
+
139
+ // Wrap `Caches.prototype.create/update/delete`, pushing the restore tuples onto
140
+ // the shared `patched` array the orchestrator owns for uninstrument().
141
+ function instrumentCacheOperations(Caches, patched) {
142
+ const owner = Caches.prototype;
143
+ for (const operation of OPERATIONS) {
144
+ const original = owner[operation];
145
+ if (typeof original !== 'function') continue;
146
+
147
+ const wrapper = function cacheOperationWrapper(params, ...rest) {
148
+ const span = _startSpan(operation, params, getProviderName(this));
149
+ return Promise.resolve(original.call(this, params, ...rest)).then(
150
+ (response) => {
151
+ try {
152
+ _stampCachedContent(span, response, operation);
153
+ } catch {
154
+ // Never break the caller over an enrichment failure.
155
+ }
156
+ span.end();
157
+ return response;
158
+ },
159
+ (error) => {
160
+ _recordError(span, error);
161
+ span.end();
162
+ throw error;
163
+ }
164
+ );
165
+ };
166
+ owner[operation] = wrapper;
167
+ patched.push([owner, operation, original]);
168
+ }
169
+ }
170
+
171
+ module.exports = { instrumentCacheOperations };
@@ -1,16 +1,24 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Context-caching token enrichment for the Google GenAI (Gemini) chat span.
4
+ * Context-caching enrichment for the Google GenAI (Gemini) chat span.
5
5
  *
6
6
  * Neither instrumentor records cached-content tokens, so we read
7
7
  * `usageMetadata.cachedContentTokenCount` (explicit or implicit caching) and
8
8
  * split it by modality: audio cached tokens go to
9
9
  * `gen_ai.usage.input_tokens_details.cached_audio_tokens`and the remainder to `gen_ai.usage.cache_read_input_tokens`
10
+ *
11
+ * Also captures the request's explicit-cache reference: a call using explicit
12
+ * caching passes `config.cachedContent` (the `cachedContents/{id}` name of a
13
+ * cache created via `client.caches.create`), stamped as
14
+ * `weflayr.cache.cached_content_id` so the call can be joined to its cache
15
+ * lifecycle rows.
10
16
  */
11
17
 
12
18
  const { _audioTokens } = require('./get_audio_telemetry');
13
19
 
20
+ const CACHED_CONTENT_ID_ATTR = 'weflayr.cache.cached_content_id';
21
+
14
22
  function setCacheTelemetry(span, response) {
15
23
  const usage = response && response.usageMetadata;
16
24
  const cachedTokens = (usage && usage.cachedContentTokenCount) || 0;
@@ -22,4 +30,10 @@ function setCacheTelemetry(span, response) {
22
30
  if (cacheRead) span.setAttribute('gen_ai.usage.cache_read_input_tokens', cacheRead);
23
31
  }
24
32
 
25
- module.exports = { setCacheTelemetry };
33
+ // Stamp the request's explicit-cache reference (`config.cachedContent`).
34
+ function setCacheConfigTelemetry(span, config) {
35
+ const cachedContent = config && config.cachedContent;
36
+ if (cachedContent) span.setAttribute(CACHED_CONTENT_ID_ATTR, String(cachedContent));
37
+ }
38
+
39
+ module.exports = { setCacheTelemetry, setCacheConfigTelemetry };
@@ -17,9 +17,10 @@ const { trace } = require('@opentelemetry/api');
17
17
 
18
18
  const { getProviderName } = require('./get_provider_name');
19
19
  const { setAudioTelemetry } = require('./get_audio_telemetry');
20
- const { setCacheTelemetry } = require('./get_cache_telemetry');
20
+ const { setCacheTelemetry, setCacheConfigTelemetry } = require('./get_cache_telemetry');
21
21
  const { setReasoningTelemetry } = require('./get_reasoning_telemetry');
22
22
  const { instrumentEmbeddings } = require('./get_embeddings_telemetry');
23
+ const { instrumentCacheOperations } = require('./get_cache_operations_telemetry');
23
24
 
24
25
  class GoogleTelemetryInstrumentor {
25
26
  constructor() {
@@ -29,8 +30,9 @@ class GoogleTelemetryInstrumentor {
29
30
 
30
31
  instrument() {
31
32
  let Models;
33
+ let Caches;
32
34
  try {
33
- ({ Models } = require('@google/genai'));
35
+ ({ Models, Caches } = require('@google/genai'));
34
36
  } catch (err) {
35
37
  throw new Error(
36
38
  'weflayr.autoInstrument(GOOGLE_GENAI): the @google/genai client is not installed. ' +
@@ -45,6 +47,9 @@ class GoogleTelemetryInstrumentor {
45
47
  this._wrapChat(Models.prototype, 'generateContentStreamInternal', { stream: true });
46
48
  // Embeddings: an endpoint the traceloop instrumentor doesn't cover at all.
47
49
  instrumentEmbeddings(Models, this._patched);
50
+ // Explicit-cache lifecycle (caches.create/update/delete): endpoints the
51
+ // traceloop instrumentor doesn't cover at all.
52
+ if (Caches) instrumentCacheOperations(Caches, this._patched);
48
53
  }
49
54
 
50
55
  _wrapChat(owner, methodName, { stream }) {
@@ -56,6 +61,7 @@ class GoogleTelemetryInstrumentor {
56
61
  return original.apply(this, args);
57
62
  }
58
63
  span.setAttribute('gen_ai.system', getProviderName(this));
64
+ setCacheConfigTelemetry(span, args[0] && args[0].config);
59
65
  if (!stream) {
60
66
  return Promise.resolve(original.apply(this, args)).then((response) => {
61
67
  setAudioTelemetry(span, response);
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Request-side prompt-caching config capture for OpenAI chat spans.
5
+ *
6
+ * OpenAI callers influence the (automatic, server-side) prompt cache with
7
+ * request parameters the traceloop instrumentor never records:
8
+ * `prompt_cache_key` (a client-chosen routing key — requests sharing a key are
9
+ * routed to the same cache shard) and the retention tuning —
10
+ * `prompt_cache_retention` ("in_memory" / "24h") on older models, replaced by
11
+ * `prompt_cache_options: { ttl }` ("30m") on GPT-5.6+.
12
+ *
13
+ * Wrap `startSpan({ type, params })` (covering the streamed and non-streamed
14
+ * paths) to stamp the values as `weflayr.cache.key` / `weflayr.cache.retention`
15
+ * (raw retention/ttl string; the normalizer owns the vocabulary).
16
+ */
17
+
18
+ const CACHE_KEY_ATTR = 'weflayr.cache.key';
19
+ const CACHE_RETENTION_ATTR = 'weflayr.cache.retention';
20
+
21
+ class OpenAICacheConfigInstrumentor {
22
+ constructor() {
23
+ // [instrumentation, methodName, originalFunction] for uninstrument().
24
+ this._patched = [];
25
+ }
26
+
27
+ // No-op (and never throws) if the instrumentor's internals have changed shape —
28
+ // the worst case is missing cache-config capture, never a crash.
29
+ instrument(instrumentation) {
30
+ if (!instrumentation) return;
31
+ const startSpanOriginal = instrumentation.startSpan;
32
+ if (typeof startSpanOriginal !== 'function') return;
33
+
34
+ instrumentation.startSpan = function startSpanWithCacheConfig(request) {
35
+ const span = startSpanOriginal.call(this, request);
36
+ try {
37
+ const params = (request && request.params) || {};
38
+ if (params.prompt_cache_key) {
39
+ span.setAttribute(CACHE_KEY_ATTR, String(params.prompt_cache_key));
40
+ }
41
+ const retention =
42
+ params.prompt_cache_retention ||
43
+ (params.prompt_cache_options && params.prompt_cache_options.ttl);
44
+ if (retention) {
45
+ span.setAttribute(CACHE_RETENTION_ATTR, String(retention));
46
+ }
47
+ } catch {
48
+ // Never break span creation over an enrichment failure.
49
+ }
50
+ return span;
51
+ };
52
+ this._patched.push([instrumentation, 'startSpan', startSpanOriginal]);
53
+ }
54
+
55
+ uninstrument() {
56
+ for (const [owner, methodName, original] of this._patched) {
57
+ owner[methodName] = original;
58
+ }
59
+ this._patched = [];
60
+ }
61
+ }
62
+
63
+ module.exports = { OpenAICacheConfigInstrumentor };
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  const { OpenAIAudioInstrumentor } = require('./get_audio_telemetry');
14
+ const { OpenAICacheConfigInstrumentor } = require('./get_cache_config_telemetry');
14
15
  const { OpenAIEmbeddingsInstrumentor } = require('./get_embeddings_telemetry');
15
16
  const { OpenAIStreamingUsageInstrumentor } = require('./get_streaming_usage');
16
17
  const { OpenAIPromptCachingInstrumentor } = require('./get_cache_and_reasoning_telemetry');
@@ -22,6 +23,7 @@ class OpenAITelemetryInstrumentor {
22
23
  new OpenAIEmbeddingsInstrumentor(),
23
24
  new OpenAIStreamingUsageInstrumentor(),
24
25
  new OpenAIPromptCachingInstrumentor(),
26
+ new OpenAICacheConfigInstrumentor(),
25
27
  ];
26
28
  }
27
29
 
@@ -7,7 +7,8 @@
7
7
  * is registering it globally so every generateText/streamText/embed call emits
8
8
  * GenAI semconv spans through the tracer provider weflayr's setup() installed.
9
9
  * `usage: true` adds the `ai.usage.*` token details (reasoning tokens, uncached
10
- * input) that the semconv attributes do not carry.
10
+ * input) that the semconv attributes do not carry; `providerMetadata: true`
11
+ * opts into `ai.response.providerMetadata` on the spans that carry it.
11
12
  *
12
13
  * The aggregate spans the integration emits around each model call
13
14
  * (invoke_agent, agent_step, duplicated embeddings spans) are filtered out
@@ -37,7 +38,14 @@ class VercelAiSdkInstrumentor {
37
38
  'experimental_telemetry: { isEnabled: true } is passed on each generateText/streamText call.'
38
39
  );
39
40
  }
40
- registerTelemetry(new OpenTelemetry({ usage: true }));
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
+ const { VercelCacheCreationInstrumentor } = require('./get_cache_creation_telemetry');
47
+ new VercelCacheCreationInstrumentor().instrument(dispatcher);
48
+ registerTelemetry(dispatcher);
41
49
  }
42
50
  }
43
51
 
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Cache-write capture for Vercel AI SDK (ai >= 7) model-call spans.
5
+ *
6
+ * Anthropic reports the written cache tokens per TTL in its raw usage
7
+ * (`usage.cache_creation`, ephemeral_5m/1h buckets — billed at different
8
+ * rates). ai >= 7 carries that verbatim on the model-call telemetry event as
9
+ * `event.usage.raw` (a documented field of LanguageModelUsage), but the
10
+ * `@ai-sdk/otel` integration never emits it, so the span loses it.
11
+ *
12
+ * Wrap the telemetry dispatcher's `onLanguageModelCallEnd(event)` (a public
13
+ * callback of the telemetry interface) to read `usage.cache_creation` off the
14
+ * raw usage and stamp it verbatim as `weflayr.cache.creation` on the
15
+ * still-open model-call span (reached through the integration's call state)
16
+ * before the original handler ends it; the normalizer owns the mapping to TTL
17
+ * tiers.
18
+ */
19
+
20
+ const CACHE_CREATION_ATTR = 'weflayr.cache.creation';
21
+
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) return undefined;
28
+ return Object.values(cacheCreation).some(Boolean) ? cacheCreation : undefined;
29
+ }
30
+
31
+ class VercelCacheCreationInstrumentor {
32
+ constructor() {
33
+ // [owner, methodName, originalFunction] for uninstrument().
34
+ this._patched = [];
35
+ }
36
+
37
+ // No-op (and never throws) if the interface is missing — the worst case is
38
+ // the cache-write capture staying empty, exactly the pre-complement
39
+ // behaviour.
40
+ instrument(dispatcher) {
41
+ if (!dispatcher || typeof dispatcher.onLanguageModelCallEnd !== 'function') return;
42
+ const original = dispatcher.onLanguageModelCallEnd;
43
+
44
+ dispatcher.onLanguageModelCallEnd = function onLanguageModelCallEndWithCacheCreation(event) {
45
+ try {
46
+ const cacheCreation = cacheCreationFromRawUsage(event && event.usage && event.usage.raw);
47
+ if (cacheCreation) {
48
+ const callState = this.getCallState && this.getCallState(event.callId);
49
+ const span = callState && callState.inferenceSpan;
50
+ if (span && typeof span.setAttribute === 'function') {
51
+ span.setAttribute(CACHE_CREATION_ATTR, JSON.stringify(cacheCreation));
52
+ }
53
+ }
54
+ } catch {
55
+ // Never break span finalization over an enrichment failure.
56
+ }
57
+ return original.call(this, event);
58
+ };
59
+ this._patched.push([dispatcher, 'onLanguageModelCallEnd', original]);
60
+ }
61
+
62
+ uninstrument() {
63
+ for (const [owner, methodName, original] of this._patched) {
64
+ owner[methodName] = original;
65
+ }
66
+ this._patched = [];
67
+ }
68
+ }
69
+
70
+ module.exports = { VercelCacheCreationInstrumentor, cacheCreationFromRawUsage };
@@ -6,7 +6,7 @@
6
6
  // This file has been modified from its original form:
7
7
  // - Translated from TypeScript to JavaScript
8
8
  // - Replaced Langfuse's fixed schema with Weflayr's structured metadata
9
- // (featureName, customerName, providerName, extraTags) plus an
9
+ // (featureName, customerName, providerNameOverride, extraTags) plus an
10
10
  // auto-generated per-call uuid
11
11
  // - Removed asBaggage cross-process propagation (not needed yet)
12
12
  // - Removed experiment framework keys
@@ -36,7 +36,13 @@ function getMergedMetadataFromContext(parentContext) {
36
36
  return existing && typeof existing === 'object' ? { ...existing } : {};
37
37
  }
38
38
 
39
- function _buildAttributes({ featureName, customerName, providerName, extraTags, spanUuid }) {
39
+ function _buildAttributes({
40
+ featureName,
41
+ customerName,
42
+ providerNameOverride,
43
+ extraTags,
44
+ spanUuid,
45
+ }) {
40
46
  const attributes = {
41
47
  [FEATURE_NAME_ATTR]: featureName,
42
48
  [CUSTOMER_NAME_ATTR]: customerName,
@@ -45,8 +51,8 @@ function _buildAttributes({ featureName, customerName, providerName, extraTags,
45
51
  for (const [key, value] of Object.entries(extraTags)) {
46
52
  attributes[`${EXTRA_TAG_PREFIX}${key}`] = value;
47
53
  }
48
- if (providerName !== undefined && providerName !== null) {
49
- attributes[PROVIDER_NAME_ATTR] = providerName;
54
+ if (providerNameOverride !== undefined && providerNameOverride !== null) {
55
+ attributes[PROVIDER_NAME_ATTR] = providerNameOverride;
50
56
  }
51
57
  return attributes;
52
58
  }
@@ -62,8 +68,8 @@ function _buildAttributes({ featureName, customerName, providerName, extraTags,
62
68
  * propagateMetadata call" contract.
63
69
  */
64
70
  function propagateMetadata(options, fn) {
65
- const { featureName, customerId } = options ?? {};
66
- let { customerName } = options ?? {};
71
+ const { featureName, customerId, providerName } = options ?? {};
72
+ let { customerName, providerNameOverride } = options ?? {};
67
73
  if (customerId != null) {
68
74
  process.emitWarning(
69
75
  'propagateMetadata: `customerId` is deprecated, use `customerName`',
@@ -71,10 +77,17 @@ function propagateMetadata(options, fn) {
71
77
  );
72
78
  customerName = customerName ?? customerId;
73
79
  }
80
+ if (providerName != null) {
81
+ process.emitWarning(
82
+ 'propagateMetadata: `providerName` is deprecated, use `providerNameOverride`',
83
+ 'DeprecationWarning'
84
+ );
85
+ providerNameOverride = providerNameOverride ?? providerName;
86
+ }
74
87
  if (featureName == null || customerName == null) {
75
88
  throw new Error('propagateMetadata: `featureName` and `customerName` are required');
76
89
  }
77
- const normalizedOptions = { ...options, customerName };
90
+ const normalizedOptions = { ...options, customerName, providerNameOverride };
78
91
 
79
92
  if (typeof fn === 'function') {
80
93
  return _runWithMetadata(normalizedOptions, fn);
@@ -98,11 +111,11 @@ function propagateMetadata(options, fn) {
98
111
  }
99
112
 
100
113
  function _runWithMetadata(options, fn) {
101
- const { featureName, customerName, providerName, extraTags } = options;
114
+ const { featureName, customerName, providerNameOverride, extraTags } = options;
102
115
  const attributes = _buildAttributes({
103
116
  featureName,
104
117
  customerName,
105
- providerName,
118
+ providerNameOverride,
106
119
  extraTags: extraTags ?? {},
107
120
  spanUuid: randomUUID().replace(/-/g, ''),
108
121
  });