weflayr 0.20.6 → 0.22.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 (49) hide show
  1. package/index.d.ts +1 -1
  2. package/package.json +2 -2
  3. package/src/auto-instrument.js +64 -31
  4. package/src/instrumentation/anthropic/get_cache_and_reasoning_telemetry.js +107 -0
  5. package/src/instrumentation/anthropic/get_streaming_stop_reason_telemetry.js +121 -0
  6. package/src/instrumentation/anthropic/get_tool_definitions_telemetry.js +50 -0
  7. package/src/instrumentation/anthropic/set_telemetry_attributes.js +37 -0
  8. package/src/instrumentation/bedrock/get_cache_telemetry.js +104 -0
  9. package/src/instrumentation/bedrock/get_input_message_content_telemetry.js +94 -0
  10. package/src/instrumentation/bedrock/get_streaming_output_message_telemetry.js +87 -0
  11. package/src/instrumentation/bedrock/set_telemetry_attributes.js +36 -0
  12. package/src/instrumentation/cohere/get_chat_telemetry.js +88 -0
  13. package/src/instrumentation/cohere/get_embeddings_telemetry.js +107 -0
  14. package/src/instrumentation/cohere/set_telemetry_attributes.js +32 -0
  15. package/src/instrumentation/google/get_audio_telemetry.js +30 -0
  16. package/src/instrumentation/google/get_cache_telemetry.js +25 -0
  17. package/src/instrumentation/google/get_embeddings_telemetry.js +99 -0
  18. package/src/instrumentation/google/get_provider_name.js +25 -0
  19. package/src/instrumentation/google/get_reasoning_telemetry.js +21 -0
  20. package/src/instrumentation/google/set_telemetry_attributes.js +98 -0
  21. package/src/instrumentation/openai/get_audio_telemetry.js +149 -0
  22. package/src/instrumentation/openai/get_cache_and_reasoning_telemetry.js +71 -0
  23. package/src/instrumentation/openai/get_embeddings_telemetry.js +107 -0
  24. package/src/instrumentation/openai/get_streaming_usage.js +81 -0
  25. package/src/instrumentation/openai/set_telemetry_attributes.js +39 -0
  26. package/src/otel/exporter.js +1 -6
  27. package/src/otel/propagation.js +5 -0
  28. package/src/otel/span-filter.js +0 -5
  29. package/src/otel/span-processor.js +30 -1
  30. package/src/setup.js +1 -2
  31. package/tests/index.test.js +144 -44
  32. package/tests/instrumentation/anthropic/get_cache_and_reasoning_telemetry.test.js +158 -0
  33. package/tests/instrumentation/anthropic/get_streaming_stop_reason_telemetry.test.js +117 -0
  34. package/tests/instrumentation/anthropic/get_tool_definitions_telemetry.test.js +82 -0
  35. package/tests/instrumentation/bedrock/get_cache_telemetry.test.js +102 -0
  36. package/tests/instrumentation/bedrock/get_input_message_content_telemetry.test.js +89 -0
  37. package/tests/instrumentation/bedrock/get_streaming_output_message_telemetry.test.js +87 -0
  38. package/tests/instrumentation/cohere/get_chat_telemetry.test.js +148 -0
  39. package/tests/instrumentation/cohere/get_embeddings_telemetry.test.js +132 -0
  40. package/tests/instrumentation/{elevenlabs.test.js → elevenlabs/full_instrumentation.test.js} +21 -5
  41. package/tests/instrumentation/google/get_embeddings_telemetry.test.js +128 -0
  42. package/tests/instrumentation/google/get_reasoning_telemetry.test.js +41 -0
  43. package/tests/instrumentation/google/set_telemetry_attributes.test.js +219 -0
  44. package/tests/instrumentation/openai/get_audio_telemetry.test.js +170 -0
  45. package/tests/instrumentation/openai/get_cache_and_reasoning_telemetry.test.js +83 -0
  46. package/tests/instrumentation/openai/get_embeddings_telemetry.test.js +145 -0
  47. package/tests/instrumentation/openai/get_streaming_usage.test.js +105 -0
  48. package/src/otel/openai-streaming-usage.js +0 -60
  49. /package/src/instrumentation/{elevenlabs.js → elevenlabs/full_instrumentation.js} +0 -0
package/index.d.ts CHANGED
@@ -62,7 +62,7 @@ export const AIProviderName: {
62
62
  export type AIProviderName = (typeof AIProviderName)[keyof typeof AIProviderName];
63
63
 
64
64
  export interface SetupOptions {
65
- /** Weflayr API key. The server resolves your client_id from this token. */
65
+ /** Weflayr API key. The server resolves your project_uuid from this token. */
66
66
  apiKey: string;
67
67
  /** Override the intake base URL. Defaults to WEFLAYR_BASE_URL or https://api.weflayr.com. */
68
68
  baseUrl?: string;
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "weflayr",
3
- "version": "0.20.6",
3
+ "version": "0.22.0",
4
4
  "description": "Weflayr Node.js SDK — instrument any LLM client via JS Proxy",
5
5
  "main": "src/index.js",
6
6
  "types": "index.d.ts",
7
7
  "scripts": {
8
- "test": "node --test tests/index.test.js tests/instrumentation/elevenlabs.test.js",
8
+ "test": "node --test",
9
9
  "lint": "eslint src tests",
10
10
  "lint:fix": "eslint src tests --fix",
11
11
  "format": "prettier --write src tests",
@@ -4,39 +4,69 @@ const { registerInstrumentations } = require('@opentelemetry/instrumentation');
4
4
 
5
5
  const { debug } = require('./logger');
6
6
  const { setup, _state } = require('./setup');
7
- const { patchOpenAIStreamingUsage } = require('./otel/openai-streaming-usage');
8
7
 
9
8
  const AiSdk = Object.freeze({
10
9
  OPENAI: 'openai',
11
10
  ANTHROPIC: 'anthropic',
12
11
  BEDROCK: 'bedrock',
13
- AZURE: 'azure',
14
12
  COHERE: 'cohere',
15
- TOGETHER: 'together',
16
- GOOGLE_VERTEX: 'google-vertex',
17
13
  GOOGLE_GENAI: 'google-genai',
18
14
  ELEVENLABS: 'elevenlabs',
19
15
  });
20
16
 
21
17
  // Allowed values for `propagateMetadata({ providerName })`: every SDK the
22
18
  // auto-instrumentation registry knows, plus providers reached via the OpenAI
23
- // SDK on the wire but reported as themselves (e.g. Vercel AI Gateway proxies
24
- // to OpenAI/Anthropic but should be reported as the gateway).
19
+ // SDK on the wire but reported as themselves
20
+ // Those are instrumented via `autoInstrument({ aiSdks: 'openai' })` and labelled
21
+ // through `propagateMetadata({ providerName })`, not their own instrumentor.
25
22
  const AIProviderName = Object.freeze({
26
23
  ...AiSdk,
27
24
  VERCEL_AI_GATEWAY: 'vercel_ai_gateway',
28
25
  });
29
26
 
27
+ // The primary instrumentor for each provider SDK. Most are third-party
28
+ // traceloop packages, loaded by `package` name. Providers with no usable
29
+ // third-party instrumentor (e.g. ElevenLabs) instead carry a `requirePath` +
30
+ // `className` pointing at the instrumentor weflayr ships itself; `package` then
31
+ // names the provider SDK to suggest installing if its import fails.
30
32
  const REGISTRY = {
31
- [AiSdk.OPENAI]: '@traceloop/instrumentation-openai',
32
- [AiSdk.ANTHROPIC]: '@traceloop/instrumentation-anthropic',
33
- [AiSdk.BEDROCK]: '@traceloop/instrumentation-bedrock',
34
- [AiSdk.AZURE]: '@traceloop/instrumentation-azure',
35
- [AiSdk.COHERE]: '@traceloop/instrumentation-cohere',
36
- [AiSdk.TOGETHER]: '@traceloop/instrumentation-together',
37
- [AiSdk.GOOGLE_VERTEX]: '@traceloop/instrumentation-vertexai',
38
- [AiSdk.GOOGLE_GENAI]: '@traceloop/instrumentation-google-generativeai',
39
- [AiSdk.ELEVENLABS]: '@elevenlabs/elevenlabs-js',
33
+ [AiSdk.OPENAI]: { package: '@traceloop/instrumentation-openai' },
34
+ [AiSdk.ANTHROPIC]: { package: '@traceloop/instrumentation-anthropic' },
35
+ [AiSdk.BEDROCK]: { package: '@traceloop/instrumentation-bedrock' },
36
+ [AiSdk.COHERE]: { package: '@traceloop/instrumentation-cohere' },
37
+ [AiSdk.GOOGLE_GENAI]: { package: '@traceloop/instrumentation-google-generativeai' },
38
+ [AiSdk.ELEVENLABS]: {
39
+ package: '@elevenlabs/elevenlabs-js',
40
+ requirePath: './instrumentation/elevenlabs/full_instrumentation',
41
+ className: 'ElevenLabsInstrumentor',
42
+ },
43
+ };
44
+
45
+ // One complementary instrumentor per provider: a `set_telemetry_attributes`
46
+ // orchestrator under `instrumentation/<provider>/` that wires every weflayr patch
47
+ // the traceloop instrumentor misses (the orchestrator itself groups the per-patch
48
+ // `get_*` files, so a single entry per provider suffices).
49
+ const COMPLEMENTARY_INSTRUMENTOR = {
50
+ [AiSdk.OPENAI]: {
51
+ requirePath: './instrumentation/openai/set_telemetry_attributes',
52
+ className: 'OpenAITelemetryInstrumentor',
53
+ },
54
+ [AiSdk.ANTHROPIC]: {
55
+ requirePath: './instrumentation/anthropic/set_telemetry_attributes',
56
+ className: 'AnthropicTelemetryInstrumentor',
57
+ },
58
+ [AiSdk.BEDROCK]: {
59
+ requirePath: './instrumentation/bedrock/set_telemetry_attributes',
60
+ className: 'BedrockTelemetryInstrumentor',
61
+ },
62
+ [AiSdk.COHERE]: {
63
+ requirePath: './instrumentation/cohere/set_telemetry_attributes',
64
+ className: 'CohereTelemetryInstrumentor',
65
+ },
66
+ [AiSdk.GOOGLE_GENAI]: {
67
+ requirePath: './instrumentation/google/set_telemetry_attributes',
68
+ className: 'GoogleTelemetryInstrumentor',
69
+ },
40
70
  };
41
71
 
42
72
  const SETUP_OPTION_KEYS = [
@@ -65,34 +95,37 @@ async function _instrumentAiSdk(aiSdk) {
65
95
  `weflayr.autoInstrument: unknown provider "${aiSdk}". Known: ${Object.keys(REGISTRY).join(', ')}`
66
96
  );
67
97
  }
68
- const pkg = REGISTRY[aiSdk];
69
-
70
- if (aiSdk === AiSdk.ELEVENLABS) {
71
- // ElevenLabs has no OTel/traceloop instrumentor, so
72
- // use the hand-written one bundled with weflayr.
73
- const { ElevenLabsInstrumentor } = require('./instrumentation/elevenlabs');
74
- new ElevenLabsInstrumentor().instrument();
75
- debug('autoInstrument: registered aiSdk=%s package=%s', aiSdk, pkg);
98
+ const entry = REGISTRY[aiSdk];
99
+
100
+ if (entry.requirePath) {
101
+ // No usable third-party instrumentor — use the hand-written one bundled
102
+ // with weflayr (it covers every endpoint of the provider).
103
+ const Instrumentor = require(entry.requirePath)[entry.className];
104
+ new Instrumentor().instrument();
105
+ debug('autoInstrument: registered aiSdk=%s package=%s', aiSdk, entry.package);
76
106
  return;
77
107
  }
78
108
 
79
109
  let instrumentationPkg;
80
110
  try {
81
- instrumentationPkg = require(pkg);
111
+ instrumentationPkg = require(entry.package);
82
112
  } catch (err) {
83
113
  throw new Error(
84
- `weflayr.autoInstrument("${aiSdk}"): failed to load "${pkg}". ` +
85
- `Install it with: npm install ${pkg}\nUnderlying error: ${err.message}`
114
+ `weflayr.autoInstrument("${aiSdk}"): failed to load "${entry.package}". ` +
115
+ `Install it with: npm install ${entry.package}\nUnderlying error: ${err.message}`
86
116
  );
87
117
  }
88
118
 
89
119
  const instrumentation = _instantiateInstrumentation(instrumentationPkg);
90
- if (aiSdk === AiSdk.OPENAI) {
91
- // The OpenAI instrumentor drops token usage on streamed calls; backfill it.
92
- patchOpenAIStreamingUsage(instrumentation);
93
- }
94
120
  registerInstrumentations({ instrumentations: [instrumentation] });
95
- debug('autoInstrument: registered aiSdk=%s package=%s', aiSdk, pkg);
121
+
122
+ const complementary = COMPLEMENTARY_INSTRUMENTOR[aiSdk];
123
+ if (complementary) {
124
+ const InstrumentorClass = require(complementary.requirePath)[complementary.className];
125
+ new InstrumentorClass().instrument(instrumentation);
126
+ }
127
+
128
+ debug('autoInstrument: registered aiSdk=%s package=%s', aiSdk, entry.package);
96
129
  }
97
130
 
98
131
  /**
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Cached-prompt token enrichment complement for the non-streamed Anthropic path.
5
+ *
6
+ * `@traceloop/instrumentation-anthropic` reads the response's
7
+ * `usage.cache_creation_input_tokens` / `cache_read_input_tokens` but never writes
8
+ * them to a `gen_ai.usage.cache_*` span attribute (its semantic conventions define
9
+ * no such key), so cache-hit spans carry no cached-token counts. This stamps the
10
+ * underscored attributes the worker reads. (The Python instrumentor emits them
11
+ * natively, under the dotted `cache_read.input_tokens` form, which the worker also
12
+ * accepts.)
13
+ *
14
+ * It wraps the instrumentor instance's `_endSpan({ span, type, result })`, which
15
+ * finalizes the span after the response resolves, letting us read `result.usage`
16
+ * while the span is still open.
17
+ */
18
+
19
+ const CACHE_READ_INPUT_TOKENS_ATTR = 'gen_ai.usage.cache_read_input_tokens';
20
+ const CACHE_CREATION_INPUT_TOKENS_ATTR = 'gen_ai.usage.cache_creation_input_tokens';
21
+ const INPUT_TOKENS_ATTR = 'gen_ai.usage.input_tokens';
22
+ const THINKING_TOKENS_ATTR = 'gen_ai.usage.output_tokens_details.thinking_tokens';
23
+
24
+ // Stamp cache counts (and the raw uncached input) from an Anthropic usage object.
25
+ // The instrumentor emits no `gen_ai.usage.cache_*` attribute; `input_tokens` in
26
+ // the stream events is the raw uncached count (cached tokens are reported
27
+ // separately), so it matches the non-streamed override. The extended-thinking
28
+ // count (`output_tokens_details.thinking_tokens`, a subset of output_tokens) is
29
+ // likewise dropped by the instrumentor, so stamp it for the normalizer.
30
+ function _setUsage(span, usage) {
31
+ if (!usage || !span || !span.isRecording || !span.isRecording()) return;
32
+ if (usage.cache_read_input_tokens) {
33
+ span.setAttribute(CACHE_READ_INPUT_TOKENS_ATTR, usage.cache_read_input_tokens);
34
+ }
35
+ if (usage.cache_creation_input_tokens) {
36
+ span.setAttribute(CACHE_CREATION_INPUT_TOKENS_ATTR, usage.cache_creation_input_tokens);
37
+ }
38
+ if (typeof usage.input_tokens === 'number') {
39
+ span.setAttribute(INPUT_TOKENS_ATTR, usage.input_tokens);
40
+ }
41
+ const thinking = usage.output_tokens_details && usage.output_tokens_details.thinking_tokens;
42
+ if (thinking) {
43
+ span.setAttribute(THINKING_TOKENS_ATTR, thinking);
44
+ }
45
+ }
46
+
47
+ class AnthropicPromptCachingInstrumentor {
48
+ constructor() {
49
+ // [instrumentation, methodName, originalFunction] for uninstrument().
50
+ this._patched = [];
51
+ }
52
+
53
+ // No-op (and never throws) if the instrumentor's internals have changed shape —
54
+ // the worst case is missing cached-token counts, never a crash.
55
+ instrument(instrumentation) {
56
+ if (!instrumentation) return;
57
+
58
+ // Non-streamed: the instrumentor finalizes via `_endSpan({ span, result })`.
59
+ const endSpanOriginal = instrumentation._endSpan;
60
+ if (typeof endSpanOriginal === 'function') {
61
+ instrumentation._endSpan = function endSpanWithCache(args) {
62
+ try {
63
+ _setUsage(args && args.span, args && args.result && args.result.usage);
64
+ } catch {
65
+ // Never break span finalization over an enrichment failure.
66
+ }
67
+ return endSpanOriginal.call(this, args);
68
+ };
69
+ this._patched.push([instrumentation, '_endSpan', endSpanOriginal]);
70
+ }
71
+
72
+ // Streamed: cache + raw input arrive in the `message_start` (and `message_delta`)
73
+ // usage events; observe the chunks as they pass through the streaming handler.
74
+ const streamOriginal = instrumentation._streamingWrapPromise;
75
+ if (typeof streamOriginal === 'function') {
76
+ instrumentation._streamingWrapPromise = function streamWithCache(...streamArgs) {
77
+ const opts = streamArgs[streamArgs.length - 1];
78
+ const span = opts && opts.span;
79
+ const result = streamOriginal.apply(this, streamArgs);
80
+ if (!span || !result || typeof result[Symbol.asyncIterator] !== 'function') return result;
81
+ return (async function* observeCache() {
82
+ for await (const chunk of result) {
83
+ try {
84
+ const usage =
85
+ (chunk && chunk.type === 'message_start' && chunk.message && chunk.message.usage) ||
86
+ (chunk && chunk.usage);
87
+ _setUsage(span, usage);
88
+ } catch {
89
+ // Never break the caller's stream over an enrichment failure.
90
+ }
91
+ yield chunk;
92
+ }
93
+ })();
94
+ };
95
+ this._patched.push([instrumentation, '_streamingWrapPromise', streamOriginal]);
96
+ }
97
+ }
98
+
99
+ uninstrument() {
100
+ for (const [owner, methodName, original] of this._patched) {
101
+ owner[methodName] = original;
102
+ }
103
+ this._patched = [];
104
+ }
105
+ }
106
+
107
+ module.exports = { AnthropicPromptCachingInstrumentor };
@@ -0,0 +1,121 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Streamed stop-reason enrichment for the Anthropic chat span.
5
+ *
6
+ * `@traceloop/instrumentation-anthropic` accumulates a streamed response into a
7
+ * result object but never reads the `message_delta` chunk's `delta.stop_reason`,
8
+ * so streamed spans format their output message with an empty `finish_reason`.
9
+ *
10
+ * `_streamingWrapPromise(client, moduleExports, { span, type, promise })`
11
+ * returns `new moduleExports.APIPromise(...)` whose parse step wraps the real
12
+ * SDK stream with the instrumentor's accumulating iterator. This complement
13
+ * hands it a patched `moduleExports` whose APIPromise interposes on that parse
14
+ * step, observing the chunks (outside the accumulating iterator, so every chunk
15
+ * is seen before the iterator finishes) to capture the stop reason — which is
16
+ * then injected into the accumulated result just before `_endSpan` formats the
17
+ * output message.
18
+ */
19
+
20
+ class AnthropicStreamingStopReasonInstrumentor {
21
+ constructor() {
22
+ // [instrumentation, methodName, originalFunction] for uninstrument().
23
+ this._patched = [];
24
+ // span -> stop_reason captured from the stream's message_delta chunk.
25
+ this._stopReasonBySpan = new WeakMap();
26
+ }
27
+
28
+ // No-op (and never throws) if the instrumentor's internals have changed shape —
29
+ // the worst case is an empty streamed finish_reason, never a crash.
30
+ instrument(instrumentation) {
31
+ if (!instrumentation) return;
32
+
33
+ const stopReasonBySpan = this._stopReasonBySpan;
34
+
35
+ async function* observeStopReason(stream, span) {
36
+ for await (const chunk of stream) {
37
+ try {
38
+ if (
39
+ chunk &&
40
+ chunk.type === 'message_delta' &&
41
+ chunk.delta &&
42
+ chunk.delta.stop_reason != null
43
+ ) {
44
+ stopReasonBySpan.set(span, chunk.delta.stop_reason);
45
+ }
46
+ } catch {
47
+ // Never break the caller's stream over an enrichment failure.
48
+ }
49
+ yield chunk;
50
+ }
51
+ }
52
+
53
+ const streamOriginal = instrumentation._streamingWrapPromise;
54
+ if (typeof streamOriginal === 'function') {
55
+ instrumentation._streamingWrapPromise = function streamWithStopReason(
56
+ client,
57
+ moduleExports,
58
+ opts
59
+ ) {
60
+ const span = opts && opts.span;
61
+ if (!span || !moduleExports || typeof moduleExports.APIPromise !== 'function') {
62
+ return streamOriginal.call(this, client, moduleExports, opts);
63
+ }
64
+ class APIPromiseWithStopReason extends moduleExports.APIPromise {
65
+ constructor(promiseClient, responsePromise, parseResponse) {
66
+ super(promiseClient, responsePromise, async (parseClient, props) => {
67
+ const stream = await parseResponse(parseClient, props);
68
+ try {
69
+ if (
70
+ stream &&
71
+ typeof stream[Symbol.asyncIterator] === 'function' &&
72
+ typeof stream.constructor === 'function'
73
+ ) {
74
+ return new stream.constructor(
75
+ () => observeStopReason(stream, span),
76
+ stream.controller
77
+ );
78
+ }
79
+ } catch {
80
+ // Fall through to the unobserved stream.
81
+ }
82
+ return stream;
83
+ });
84
+ }
85
+ }
86
+ // The SDK's module exports are non-writable, so the override is an own
87
+ // defined property shadowing the inherited one.
88
+ const patchedModuleExports = Object.create(moduleExports, {
89
+ APIPromise: { value: APIPromiseWithStopReason, configurable: true },
90
+ });
91
+ return streamOriginal.call(this, client, patchedModuleExports, opts);
92
+ };
93
+ this._patched.push([instrumentation, '_streamingWrapPromise', streamOriginal]);
94
+ }
95
+
96
+ const endSpanOriginal = instrumentation._endSpan;
97
+ if (typeof endSpanOriginal === 'function') {
98
+ instrumentation._endSpan = function endSpanWithStopReason(args) {
99
+ try {
100
+ const captured = args && args.span && stopReasonBySpan.get(args.span);
101
+ if (captured != null && args.result && args.result.stop_reason == null) {
102
+ args.result.stop_reason = captured;
103
+ }
104
+ } catch {
105
+ // Never break span finalization over an enrichment failure.
106
+ }
107
+ return endSpanOriginal.call(this, args);
108
+ };
109
+ this._patched.push([instrumentation, '_endSpan', endSpanOriginal]);
110
+ }
111
+ }
112
+
113
+ uninstrument() {
114
+ for (const [owner, methodName, original] of this._patched) {
115
+ owner[methodName] = original;
116
+ }
117
+ this._patched = [];
118
+ }
119
+ }
120
+
121
+ module.exports = { AnthropicStreamingStopReasonInstrumentor };
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Tool-definitions enrichment for the Anthropic chat span.
5
+ *
6
+ * `@traceloop/instrumentation-anthropic` records the request model/temperature
7
+ * attributes in `startSpan({ type, params })` but never the request's `tools`,
8
+ * so tool-using calls carry no `gen_ai.tool.definitions`. Wrap `startSpan` to
9
+ * stamp them in the same `{name, description, input_schema}` shape the request
10
+ * uses. The span processor strips the attribute when message-content capture is
11
+ * disabled.
12
+ */
13
+
14
+ class AnthropicToolDefinitionsInstrumentor {
15
+ constructor() {
16
+ // [instrumentation, methodName, originalFunction] for uninstrument().
17
+ this._patched = [];
18
+ }
19
+
20
+ // No-op (and never throws) if the instrumentor's internals have changed shape —
21
+ // the worst case is missing tool definitions, never a crash.
22
+ instrument(instrumentation) {
23
+ if (!instrumentation) return;
24
+ const startSpanOriginal = instrumentation.startSpan;
25
+ if (typeof startSpanOriginal !== 'function') return;
26
+
27
+ instrumentation.startSpan = function startSpanWithToolDefinitions(request) {
28
+ const span = startSpanOriginal.call(this, request);
29
+ try {
30
+ const tools = request && request.params && request.params.tools;
31
+ if (Array.isArray(tools) && tools.length) {
32
+ span.setAttribute('gen_ai.tool.definitions', JSON.stringify(tools));
33
+ }
34
+ } catch {
35
+ // Never break span creation over an enrichment failure.
36
+ }
37
+ return span;
38
+ };
39
+ this._patched.push([instrumentation, 'startSpan', startSpanOriginal]);
40
+ }
41
+
42
+ uninstrument() {
43
+ for (const [owner, methodName, original] of this._patched) {
44
+ owner[methodName] = original;
45
+ }
46
+ this._patched = [];
47
+ }
48
+ }
49
+
50
+ module.exports = { AnthropicToolDefinitionsInstrumentor };
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Anthropic telemetry complement — the registered orchestrator.
5
+ *
6
+ * Wires the weflayr-supplied patches the traceloop Anthropic instrumentor
7
+ * misses: cached-prompt token counts on the chat span (it reads them off the
8
+ * response but writes no `gen_ai.usage.cache_*` attribute), the request's tool
9
+ * definitions, and the streamed stop reason (its stream accumulator drops it,
10
+ * leaving an empty finish_reason on the output message).
11
+ */
12
+
13
+ const { AnthropicPromptCachingInstrumentor } = require('./get_cache_and_reasoning_telemetry');
14
+ const {
15
+ AnthropicStreamingStopReasonInstrumentor,
16
+ } = require('./get_streaming_stop_reason_telemetry');
17
+ const { AnthropicToolDefinitionsInstrumentor } = require('./get_tool_definitions_telemetry');
18
+
19
+ class AnthropicTelemetryInstrumentor {
20
+ constructor() {
21
+ this._subInstrumentors = [
22
+ new AnthropicPromptCachingInstrumentor(),
23
+ new AnthropicStreamingStopReasonInstrumentor(),
24
+ new AnthropicToolDefinitionsInstrumentor(),
25
+ ];
26
+ }
27
+
28
+ instrument(instrumentation) {
29
+ for (const sub of this._subInstrumentors) sub.instrument(instrumentation);
30
+ }
31
+
32
+ uninstrument() {
33
+ for (const sub of this._subInstrumentors) sub.uninstrument();
34
+ }
35
+ }
36
+
37
+ module.exports = { AnthropicTelemetryInstrumentor };
@@ -0,0 +1,104 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Cached-prompt token enrichment complement for the Bedrock invoke_model path.
5
+ *
6
+ * `@traceloop/instrumentation-bedrock` records `gen_ai.usage.{input,output}_tokens`
7
+ * off the InvokeModel response body but never reads its cache-token counts, so
8
+ * spans for prompt-cache hits carry no cached-token figures. Amazon Nova's native
9
+ * InvokeModel response includes `usage.cacheReadInputTokenCount` /
10
+ * `usage.cacheWriteInputTokenCount`; this maps them to the
11
+ * `gen_ai.usage.cache_read_input_tokens` / `cache_creation_input_tokens`
12
+ * attributes the worker reads, matching the Python SDK.
13
+ *
14
+ * It wraps the instrumentor instance's `_wrapPromise(span, promise, modelId)`,
15
+ * which resolves the send promise and enriches the span before ending it,
16
+ * letting us read the decoded body off the same response while the span is open.
17
+ */
18
+
19
+ const CACHE_READ_INPUT_TOKENS_ATTR = 'gen_ai.usage.cache_read_input_tokens';
20
+ const CACHE_CREATION_INPUT_TOKENS_ATTR = 'gen_ai.usage.cache_creation_input_tokens';
21
+
22
+ // Stamp cache counts from a Nova `usage` object onto the span.
23
+ function _setCacheFromUsage(span, usage) {
24
+ 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);
27
+ }
28
+ if (typeof usage.cacheWriteInputTokenCount === 'number' && usage.cacheWriteInputTokenCount) {
29
+ span.setAttribute(CACHE_CREATION_INPUT_TOKENS_ATTR, usage.cacheWriteInputTokenCount);
30
+ }
31
+ }
32
+
33
+ function _setCacheTokens(span, response) {
34
+ if (!span || !span.isRecording || !span.isRecording()) return;
35
+ const body = response && response.body;
36
+ // Only the non-streaming InvokeModel response buffers its body as bytes; a
37
+ // response stream is an async iterable whose usage arrives in a trailing chunk
38
+ // handled by the streaming wrap below, so skip it here (and never consume the
39
+ // caller's stream).
40
+ if (!body || typeof body[Symbol.asyncIterator] === 'function') return;
41
+ try {
42
+ _setCacheFromUsage(span, JSON.parse(new TextDecoder().decode(body)).usage);
43
+ } catch {
44
+ // Never break span enrichment over a parse failure.
45
+ }
46
+ }
47
+
48
+ class BedrockPromptCachingInstrumentor {
49
+ constructor() {
50
+ // [instrumentation, methodName, originalFunction] for uninstrument().
51
+ this._patched = [];
52
+ }
53
+
54
+ // No-op (and never throws) if the instrumentor's internals have changed shape —
55
+ // the worst case is missing cached-token counts, never a crash.
56
+ instrument(instrumentation) {
57
+ if (!instrumentation) return;
58
+
59
+ // Non-streamed: enrich the buffered InvokeModel response body.
60
+ const wrapPromiseOriginal = instrumentation._wrapPromise;
61
+ if (typeof wrapPromiseOriginal === 'function') {
62
+ instrumentation._wrapPromise = function wrapPromiseWithCache(span, promise, modelId) {
63
+ // Stamp cache tokens off the response, then hand the same response to the
64
+ // original handler (which decodes the still-buffered body for input/output
65
+ // tokens). Resolving to the original response keeps behaviour identical.
66
+ const enriched = Promise.resolve(promise).then((response) => {
67
+ _setCacheTokens(span, response);
68
+ return response;
69
+ });
70
+ return wrapPromiseOriginal.call(this, span, enriched, modelId);
71
+ };
72
+ this._patched.push([instrumentation, '_wrapPromise', wrapPromiseOriginal]);
73
+ }
74
+
75
+ // Streamed: the instrumentor processes the trailing metadata chunk via
76
+ // `_handleNovaStreamingMetadata(span, parsedResponse)` (reading
77
+ // `parsedResponse.metadata.usage` for input/output) but never its cache counts.
78
+ const metadataOriginal = instrumentation._handleNovaStreamingMetadata;
79
+ if (typeof metadataOriginal === 'function') {
80
+ instrumentation._handleNovaStreamingMetadata = function handleMetadataWithCache(
81
+ span,
82
+ parsedResponse
83
+ ) {
84
+ try {
85
+ const usage = parsedResponse && parsedResponse.metadata && parsedResponse.metadata.usage;
86
+ _setCacheFromUsage(span, usage);
87
+ } catch {
88
+ // Never break the caller's stream over an enrichment failure.
89
+ }
90
+ return metadataOriginal.call(this, span, parsedResponse);
91
+ };
92
+ this._patched.push([instrumentation, '_handleNovaStreamingMetadata', metadataOriginal]);
93
+ }
94
+ }
95
+
96
+ uninstrument() {
97
+ for (const [owner, methodName, original] of this._patched) {
98
+ owner[methodName] = original;
99
+ }
100
+ this._patched = [];
101
+ }
102
+ }
103
+
104
+ module.exports = { BedrockPromptCachingInstrumentor };