weflayr 0.20.5 → 0.21.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.
- package/index.d.ts +2 -0
- package/package.json +2 -2
- package/src/auto-instrument.js +67 -23
- package/src/instrumentation/anthropic/get_cache_and_reasoning_telemetry.js +107 -0
- package/src/instrumentation/anthropic/set_telemetry_attributes.js +27 -0
- package/src/instrumentation/bedrock/get_cache_telemetry.js +104 -0
- package/src/instrumentation/bedrock/set_telemetry_attributes.js +27 -0
- package/src/instrumentation/cohere/get_chat_telemetry.js +88 -0
- package/src/instrumentation/cohere/get_embeddings_telemetry.js +97 -0
- package/src/instrumentation/cohere/set_telemetry_attributes.js +32 -0
- package/src/instrumentation/elevenlabs/full_instrumentation.js +236 -0
- package/src/instrumentation/google/get_audio_telemetry.js +31 -0
- package/src/instrumentation/google/get_cache_telemetry.js +27 -0
- package/src/instrumentation/google/get_embeddings_telemetry.js +78 -0
- package/src/instrumentation/google/get_provider_name.js +25 -0
- package/src/instrumentation/google/get_reasoning_telemetry.js +21 -0
- package/src/instrumentation/google/set_telemetry_attributes.js +98 -0
- package/src/instrumentation/openai/get_audio_telemetry.js +149 -0
- package/src/instrumentation/openai/get_cache_and_reasoning_telemetry.js +71 -0
- package/src/instrumentation/openai/get_embeddings_telemetry.js +95 -0
- package/src/instrumentation/openai/get_streaming_usage.js +81 -0
- package/src/instrumentation/openai/set_telemetry_attributes.js +39 -0
- package/src/otel/propagation.js +5 -0
- package/src/otel/span-filter.js +1 -5
- package/tests/index.test.js +42 -44
- package/tests/instrumentation/anthropic/get_cache_and_reasoning_telemetry.test.js +125 -0
- package/tests/instrumentation/bedrock/get_cache_telemetry.test.js +102 -0
- package/tests/instrumentation/cohere/get_chat_telemetry.test.js +148 -0
- package/tests/instrumentation/cohere/get_embeddings_telemetry.test.js +129 -0
- package/tests/instrumentation/elevenlabs/full_instrumentation.test.js +244 -0
- package/tests/instrumentation/google/get_embeddings_telemetry.test.js +125 -0
- package/tests/instrumentation/google/get_reasoning_telemetry.test.js +41 -0
- package/tests/instrumentation/google/set_telemetry_attributes.test.js +187 -0
- package/tests/instrumentation/openai/get_audio_telemetry.test.js +170 -0
- package/tests/instrumentation/openai/get_cache_and_reasoning_telemetry.test.js +83 -0
- package/tests/instrumentation/openai/get_embeddings_telemetry.test.js +126 -0
- package/tests/instrumentation/openai/get_streaming_usage.test.js +76 -0
- package/src/otel/openai-streaming-usage.js +0 -60
package/index.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ export const AiSdk: {
|
|
|
38
38
|
readonly TOGETHER: 'together';
|
|
39
39
|
readonly GOOGLE_VERTEX: 'google-vertex';
|
|
40
40
|
readonly GOOGLE_GENAI: 'google-genai';
|
|
41
|
+
readonly ELEVENLABS: 'elevenlabs';
|
|
41
42
|
};
|
|
42
43
|
export type AiSdk = (typeof AiSdk)[keyof typeof AiSdk];
|
|
43
44
|
|
|
@@ -55,6 +56,7 @@ export const AIProviderName: {
|
|
|
55
56
|
readonly TOGETHER: 'together';
|
|
56
57
|
readonly GOOGLE_VERTEX: 'google-vertex';
|
|
57
58
|
readonly GOOGLE_GENAI: 'google-genai';
|
|
59
|
+
readonly ELEVENLABS: 'elevenlabs';
|
|
58
60
|
readonly VERCEL_AI_GATEWAY: 'vercel_ai_gateway';
|
|
59
61
|
};
|
|
60
62
|
export type AIProviderName = (typeof AIProviderName)[keyof typeof AIProviderName];
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weflayr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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
|
|
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",
|
package/src/auto-instrument.js
CHANGED
|
@@ -4,37 +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',
|
|
14
|
+
ELEVENLABS: 'elevenlabs',
|
|
18
15
|
});
|
|
19
16
|
|
|
20
17
|
// Allowed values for `propagateMetadata({ providerName })`: every SDK the
|
|
21
18
|
// auto-instrumentation registry knows, plus providers reached via the OpenAI
|
|
22
|
-
// SDK on the wire but reported as themselves
|
|
23
|
-
//
|
|
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.
|
|
24
22
|
const AIProviderName = Object.freeze({
|
|
25
23
|
...AiSdk,
|
|
26
24
|
VERCEL_AI_GATEWAY: 'vercel_ai_gateway',
|
|
27
25
|
});
|
|
28
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.
|
|
29
32
|
const REGISTRY = {
|
|
30
|
-
[AiSdk.OPENAI]: '@traceloop/instrumentation-openai',
|
|
31
|
-
[AiSdk.ANTHROPIC]: '@traceloop/instrumentation-anthropic',
|
|
32
|
-
[AiSdk.BEDROCK]: '@traceloop/instrumentation-bedrock',
|
|
33
|
-
[AiSdk.
|
|
34
|
-
[AiSdk.
|
|
35
|
-
[AiSdk.
|
|
36
|
-
|
|
37
|
-
|
|
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
|
+
},
|
|
38
70
|
};
|
|
39
71
|
|
|
40
72
|
const SETUP_OPTION_KEYS = [
|
|
@@ -63,25 +95,37 @@ async function _instrumentAiSdk(aiSdk) {
|
|
|
63
95
|
`weflayr.autoInstrument: unknown provider "${aiSdk}". Known: ${Object.keys(REGISTRY).join(', ')}`
|
|
64
96
|
);
|
|
65
97
|
}
|
|
66
|
-
const
|
|
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);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
67
108
|
|
|
68
109
|
let instrumentationPkg;
|
|
69
110
|
try {
|
|
70
|
-
instrumentationPkg = require(
|
|
111
|
+
instrumentationPkg = require(entry.package);
|
|
71
112
|
} catch (err) {
|
|
72
113
|
throw new Error(
|
|
73
|
-
`weflayr.autoInstrument("${aiSdk}"): failed to load "${
|
|
74
|
-
`Install it with: npm install ${
|
|
114
|
+
`weflayr.autoInstrument("${aiSdk}"): failed to load "${entry.package}". ` +
|
|
115
|
+
`Install it with: npm install ${entry.package}\nUnderlying error: ${err.message}`
|
|
75
116
|
);
|
|
76
117
|
}
|
|
77
118
|
|
|
78
119
|
const instrumentation = _instantiateInstrumentation(instrumentationPkg);
|
|
79
|
-
if (aiSdk === AiSdk.OPENAI) {
|
|
80
|
-
// The OpenAI instrumentor drops token usage on streamed calls; backfill it.
|
|
81
|
-
patchOpenAIStreamingUsage(instrumentation);
|
|
82
|
-
}
|
|
83
120
|
registerInstrumentations({ instrumentations: [instrumentation] });
|
|
84
|
-
|
|
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);
|
|
85
129
|
}
|
|
86
130
|
|
|
87
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,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Anthropic telemetry complement — the registered orchestrator.
|
|
5
|
+
*
|
|
6
|
+
* Wires the one weflayr-supplied patch 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).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { AnthropicPromptCachingInstrumentor } = require('./get_cache_and_reasoning_telemetry');
|
|
12
|
+
|
|
13
|
+
class AnthropicTelemetryInstrumentor {
|
|
14
|
+
constructor() {
|
|
15
|
+
this._subInstrumentors = [new AnthropicPromptCachingInstrumentor()];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
instrument(instrumentation) {
|
|
19
|
+
for (const sub of this._subInstrumentors) sub.instrument(instrumentation);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
uninstrument() {
|
|
23
|
+
for (const sub of this._subInstrumentors) sub.uninstrument();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
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 };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bedrock telemetry complement — the registered orchestrator.
|
|
5
|
+
*
|
|
6
|
+
* Wires the one weflayr-supplied patch the traceloop Bedrock instrumentor misses:
|
|
7
|
+
* cached-prompt token counts on the invoke_model span (Nova returns them in the
|
|
8
|
+
* response body but traceloop never reads them).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { BedrockPromptCachingInstrumentor } = require('./get_cache_telemetry');
|
|
12
|
+
|
|
13
|
+
class BedrockTelemetryInstrumentor {
|
|
14
|
+
constructor() {
|
|
15
|
+
this._subInstrumentors = [new BedrockPromptCachingInstrumentor()];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
instrument(instrumentation) {
|
|
19
|
+
for (const sub of this._subInstrumentors) sub.instrument(instrumentation);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
uninstrument() {
|
|
23
|
+
for (const sub of this._subInstrumentors) sub.uninstrument();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { BedrockTelemetryInstrumentor };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Chat token-usage enrichment complement for the Cohere v1 chat path.
|
|
5
|
+
*
|
|
6
|
+
* `@traceloop/instrumentation-cohere` creates the `cohere.chat` span but reads
|
|
7
|
+
* token usage from the obsolete `result.token_count.{prompt,response}_tokens`
|
|
8
|
+
* shape; modern cohere-ai returns usage under `meta.billedUnits` instead, so the
|
|
9
|
+
* traceloop span ends up with zero chat tokens. This thin complement backfills
|
|
10
|
+
* `gen_ai.usage.{input,output}_tokens` from `meta.billedUnits`, the same source
|
|
11
|
+
* traceloop already uses for the `generate` endpoint — bringing Node chat tokens
|
|
12
|
+
* to parity with the Python SDK (OpenLLMetry captures them natively, so there is
|
|
13
|
+
* no Python counterpart).
|
|
14
|
+
*
|
|
15
|
+
* It hooks the internal `CohereClient.prototype.__chat` (which the public `chat`
|
|
16
|
+
* delegates to synchronously) rather than the public `chat`: traceloop wraps
|
|
17
|
+
* `chat` and opens the span around it, so `__chat` runs while that span is the
|
|
18
|
+
* active span — letting this complement enrich it regardless of patch order.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const { trace } = require('@opentelemetry/api');
|
|
22
|
+
|
|
23
|
+
function _billedUnits(result) {
|
|
24
|
+
// Public `chat` resolves to the response body, but `__chat` resolves to the
|
|
25
|
+
// Fern wrapper `{ data, rawResponse }`; read billedUnits off either shape.
|
|
26
|
+
const response = result && result.data ? result.data : result;
|
|
27
|
+
return response && response.meta ? response.meta.billedUnits : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function _setChatTokenUsage(span, result) {
|
|
31
|
+
const billedUnits = _billedUnits(result);
|
|
32
|
+
if (!billedUnits) return;
|
|
33
|
+
if (typeof billedUnits.inputTokens === 'number') {
|
|
34
|
+
span.setAttribute('gen_ai.usage.input_tokens', billedUnits.inputTokens);
|
|
35
|
+
}
|
|
36
|
+
if (typeof billedUnits.outputTokens === 'number') {
|
|
37
|
+
span.setAttribute('gen_ai.usage.output_tokens', billedUnits.outputTokens);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class CohereChatTokenUsageInstrumentor {
|
|
42
|
+
constructor() {
|
|
43
|
+
// [ownerPrototype, methodName, originalFunction] for uninstrument().
|
|
44
|
+
this._patched = [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
instrument() {
|
|
48
|
+
let CohereClient;
|
|
49
|
+
try {
|
|
50
|
+
({ CohereClient } = require('cohere-ai/Client'));
|
|
51
|
+
} catch (err) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
'weflayr.autoInstrument(COHERE): the cohere-ai client is not installed. ' +
|
|
54
|
+
'Install it with: npm install cohere-ai\n' +
|
|
55
|
+
`Underlying error: ${err.message}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this._wrap(CohereClient.prototype, '__chat');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
uninstrument() {
|
|
63
|
+
for (const [owner, methodName, original] of this._patched) {
|
|
64
|
+
owner[methodName] = original;
|
|
65
|
+
}
|
|
66
|
+
this._patched = [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
_wrap(owner, methodName) {
|
|
70
|
+
const original = owner[methodName];
|
|
71
|
+
|
|
72
|
+
function wrapper(...args) {
|
|
73
|
+
// Captured synchronously, while traceloop's chat span is still active.
|
|
74
|
+
const span = trace.getActiveSpan();
|
|
75
|
+
const result = original.apply(this, args);
|
|
76
|
+
if (!span || !span.isRecording || !span.isRecording()) return result;
|
|
77
|
+
return Promise.resolve(result).then((response) => {
|
|
78
|
+
_setChatTokenUsage(span, response);
|
|
79
|
+
return response;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
owner[methodName] = wrapper;
|
|
84
|
+
this._patched.push([owner, methodName, original]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { CohereChatTokenUsageInstrumentor };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hand-written OTel instrumentor for the Cohere embeddings endpoint.
|
|
5
|
+
*
|
|
6
|
+
* `@traceloop/instrumentation-cohere` (the package weflayr uses for Cohere on
|
|
7
|
+
* Node) wraps chat/generate/rerank but not `embed`, so this ships inside weflayr
|
|
8
|
+
* to cover it. OpenLLMetry's cohere instrumentor doesn't cover `embed` either, so
|
|
9
|
+
* the Python SDK has a mirror complement (`cohere_embeddings.py`) shaped to
|
|
10
|
+
* normalize identically. It is registered alongside the traceloop instrumentor
|
|
11
|
+
* whenever `autoInstrument({ aiSdks: 'cohere' })` runs.
|
|
12
|
+
*
|
|
13
|
+
* Embeddings are billed per input token, so the span records the input tokens
|
|
14
|
+
* Cohere returns under `gen_ai.usage.input_tokens`; there is no completion.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { trace, SpanStatusCode } = require('@opentelemetry/api');
|
|
18
|
+
|
|
19
|
+
const TRACER_NAME = 'weflayr.instrumentation.cohere_embeddings';
|
|
20
|
+
// Matches the gen_ai.system the cohere chat instrumentors set (Python + Node).
|
|
21
|
+
const SYSTEM = 'Cohere';
|
|
22
|
+
const EMBEDDINGS_OPERATION = 'embeddings';
|
|
23
|
+
|
|
24
|
+
function _recordError(span, error) {
|
|
25
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: error?.message ?? String(error) });
|
|
26
|
+
span.recordException(error);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function _wrapEmbed(owner, patched) {
|
|
30
|
+
const original = owner.embed;
|
|
31
|
+
|
|
32
|
+
function wrapper(request, ...rest) {
|
|
33
|
+
const span = trace.getTracer(TRACER_NAME).startSpan(EMBEDDINGS_OPERATION);
|
|
34
|
+
span.setAttribute('gen_ai.system', SYSTEM);
|
|
35
|
+
span.setAttribute('gen_ai.operation.name', EMBEDDINGS_OPERATION);
|
|
36
|
+
if (request && request.model) {
|
|
37
|
+
span.setAttribute('gen_ai.request.model', request.model);
|
|
38
|
+
}
|
|
39
|
+
return Promise.resolve(original.call(this, request, ...rest)).then(
|
|
40
|
+
(response) => {
|
|
41
|
+
// Node SDK reports the billed input tokens under camelCase keys.
|
|
42
|
+
const inputTokens =
|
|
43
|
+
response?.meta?.billedUnits?.inputTokens ?? response?.meta?.tokens?.inputTokens;
|
|
44
|
+
if (inputTokens != null) {
|
|
45
|
+
span.setAttribute('gen_ai.usage.input_tokens', inputTokens);
|
|
46
|
+
}
|
|
47
|
+
span.end();
|
|
48
|
+
return response;
|
|
49
|
+
},
|
|
50
|
+
(error) => {
|
|
51
|
+
_recordError(span, error);
|
|
52
|
+
span.end();
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
owner.embed = wrapper;
|
|
59
|
+
patched.push([owner, 'embed', original]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class CohereEmbeddingsInstrumentor {
|
|
63
|
+
constructor() {
|
|
64
|
+
// [ownerPrototype, methodName, originalFunction] for uninstrument().
|
|
65
|
+
this._patched = [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
instrument() {
|
|
69
|
+
let CohereClient;
|
|
70
|
+
let V2Client;
|
|
71
|
+
try {
|
|
72
|
+
// v1 `embed` is a prototype method on CohereClient; v2 `embed` lives on
|
|
73
|
+
// V2Client.prototype (CohereClientV2 binds it as an instance property, so
|
|
74
|
+
// it must be patched on the underlying V2Client class instead).
|
|
75
|
+
({ CohereClient } = require('cohere-ai/Client'));
|
|
76
|
+
({ V2Client } = require('cohere-ai/api/resources/v2/client/Client'));
|
|
77
|
+
} catch (err) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
'weflayr.autoInstrument(COHERE): the cohere-ai client is not installed. ' +
|
|
80
|
+
'Install it with: npm install cohere-ai\n' +
|
|
81
|
+
`Underlying error: ${err.message}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_wrapEmbed(CohereClient.prototype, this._patched);
|
|
86
|
+
_wrapEmbed(V2Client.prototype, this._patched);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
uninstrument() {
|
|
90
|
+
for (const [owner, methodName, original] of this._patched) {
|
|
91
|
+
owner[methodName] = original;
|
|
92
|
+
}
|
|
93
|
+
this._patched = [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = { CohereEmbeddingsInstrumentor };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cohere telemetry complement — the registered orchestrator.
|
|
5
|
+
*
|
|
6
|
+
* Wires the weflayr-supplied patches the traceloop Cohere instrumentor misses:
|
|
7
|
+
* - chat token usage (traceloop reads the obsolete usage shape and records
|
|
8
|
+
* zero chat tokens), and
|
|
9
|
+
* - the embeddings endpoint it doesn't cover.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { CohereChatTokenUsageInstrumentor } = require('./get_chat_telemetry');
|
|
13
|
+
const { CohereEmbeddingsInstrumentor } = require('./get_embeddings_telemetry');
|
|
14
|
+
|
|
15
|
+
class CohereTelemetryInstrumentor {
|
|
16
|
+
constructor() {
|
|
17
|
+
this._subInstrumentors = [
|
|
18
|
+
new CohereChatTokenUsageInstrumentor(),
|
|
19
|
+
new CohereEmbeddingsInstrumentor(),
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
instrument(instrumentation) {
|
|
24
|
+
for (const sub of this._subInstrumentors) sub.instrument(instrumentation);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
uninstrument() {
|
|
28
|
+
for (const sub of this._subInstrumentors) sub.uninstrument();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { CohereTelemetryInstrumentor };
|