weflayr 0.22.0 → 0.22.1
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/.prettierignore +2 -0
- package/dist/api/caseConversion.d.ts +16 -0
- package/dist/api/caseConversion.js +45 -0
- package/dist/api/generated/client/client.gen.d.ts +2 -0
- package/dist/api/generated/client/client.gen.js +220 -0
- package/dist/api/generated/client/index.d.ts +10 -0
- package/dist/api/generated/client/index.js +17 -0
- package/dist/api/generated/client/types.gen.d.ts +120 -0
- package/dist/api/generated/client/types.gen.js +3 -0
- package/dist/api/generated/client/utils.gen.d.ts +37 -0
- package/dist/api/generated/client/utils.gen.js +239 -0
- package/dist/api/generated/client.gen.d.ts +12 -0
- package/dist/api/generated/client.gen.js +6 -0
- package/dist/api/generated/core/auth.gen.d.ts +25 -0
- package/dist/api/generated/core/auth.gen.js +18 -0
- package/dist/api/generated/core/bodySerializer.gen.d.ts +25 -0
- package/dist/api/generated/core/bodySerializer.gen.js +60 -0
- package/dist/api/generated/core/params.gen.d.ts +43 -0
- package/dist/api/generated/core/params.gen.js +112 -0
- package/dist/api/generated/core/pathSerializer.gen.d.ts +33 -0
- package/dist/api/generated/core/pathSerializer.gen.js +115 -0
- package/dist/api/generated/core/queryKeySerializer.gen.d.ts +18 -0
- package/dist/api/generated/core/queryKeySerializer.gen.js +98 -0
- package/dist/api/generated/core/serverSentEvents.gen.d.ts +71 -0
- package/dist/api/generated/core/serverSentEvents.gen.js +135 -0
- package/dist/api/generated/core/types.gen.d.ts +83 -0
- package/dist/api/generated/core/types.gen.js +3 -0
- package/dist/api/generated/core/utils.gen.d.ts +19 -0
- package/dist/api/generated/core/utils.gen.js +93 -0
- package/dist/api/generated/facade.gen.d.ts +208 -0
- package/dist/api/generated/facade.gen.js +52 -0
- package/dist/api/generated/index.d.ts +2 -0
- package/dist/api/generated/index.js +17 -0
- package/dist/api/generated/sdk.gen.d.ts +63 -0
- package/dist/api/generated/sdk.gen.js +125 -0
- package/dist/api/generated/types.gen.d.ts +711 -0
- package/dist/api/generated/types.gen.js +3 -0
- package/dist/api/index.d.ts +27 -0
- package/dist/api/index.js +48 -0
- package/dist/api/makeEndpoint.d.ts +27 -0
- package/dist/api/makeEndpoint.js +37 -0
- package/index.d.ts +24 -15
- package/openapi-ts.config.mjs +9 -0
- package/package.json +9 -2
- package/scripts/generate_facade.mjs +86 -0
- package/src/api/caseConversion.ts +58 -0
- package/src/api/generated/client/client.gen.ts +277 -0
- package/src/api/generated/client/index.ts +27 -0
- package/src/api/generated/client/types.gen.ts +218 -0
- package/src/api/generated/client/utils.gen.ts +316 -0
- package/src/api/generated/client.gen.ts +16 -0
- package/src/api/generated/core/auth.gen.ts +48 -0
- package/src/api/generated/core/bodySerializer.gen.ts +82 -0
- package/src/api/generated/core/params.gen.ts +178 -0
- package/src/api/generated/core/pathSerializer.gen.ts +171 -0
- package/src/api/generated/core/queryKeySerializer.gen.ts +117 -0
- package/src/api/generated/core/serverSentEvents.gen.ts +242 -0
- package/src/api/generated/core/types.gen.ts +110 -0
- package/src/api/generated/core/utils.gen.ts +140 -0
- package/src/api/generated/facade.gen.ts +80 -0
- package/src/api/generated/index.ts +4 -0
- package/src/api/generated/sdk.gen.ts +139 -0
- package/src/api/generated/types.gen.ts +792 -0
- package/src/api/index.ts +45 -0
- package/src/api/makeEndpoint.ts +54 -0
- package/src/auto-instrument.js +19 -6
- package/src/index.js +5 -0
- package/src/instrumentation/google/get_provider_name.js +2 -1
- package/src/instrumentation/vercel/full_instrumentation.js +44 -0
- package/src/otel/propagation.js +21 -12
- package/src/otel/span-processor.js +11 -1
- package/src/otel/vercel-ai-sdk-span-filter.js +79 -0
- package/tests/api/index.test.js +330 -0
- package/tests/index.test.js +48 -15
- package/tests/instrumentation/vercel/full_instrumentation.test.js +16 -0
- package/tests/otel/vercel-ai-sdk-span-filter.test.js +159 -0
- package/tsconfig.json +18 -0
package/src/api/index.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry point for the Weflayr public API (revenue, key metrics, customer tags).
|
|
3
|
+
*
|
|
4
|
+
* The endpoint functions and types under ./generated are produced from the
|
|
5
|
+
* platform's OpenAPI spec (`make generate-sdk-api-clients`) and speak the
|
|
6
|
+
* wire's snake_case, same as the Python SDK. generated/facade.gen.ts (also
|
|
7
|
+
* produced by that command, via scripts/generate_facade.mjs) wraps every
|
|
8
|
+
* endpoint so JS callers use camelCase instead - see ./caseConversion and
|
|
9
|
+
* ./makeEndpoint. This module is the handwritten front door that configures
|
|
10
|
+
* their client:
|
|
11
|
+
*
|
|
12
|
+
* const weflayr = require('weflayr');
|
|
13
|
+
*
|
|
14
|
+
* const apiClient = weflayr.client({ apiKey: 'wf-...' });
|
|
15
|
+
* const { data } = await weflayr.api.getAllRevenue({ client: apiClient, throwOnError: true });
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { type Client, createClient, createConfig } from './generated/client';
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_BASE_URL = 'https://app.weflayr.com/api';
|
|
21
|
+
|
|
22
|
+
export interface ApiClientOptions {
|
|
23
|
+
/** A project API key, created in the Weflayr dashboard. Defaults to the `WEFLAYR_API_KEY` environment variable. */
|
|
24
|
+
apiKey?: string;
|
|
25
|
+
/** Where the API lives — override for local or self-hosted deployments. */
|
|
26
|
+
baseUrl?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A client for the public API, authenticated with a project API key. */
|
|
30
|
+
export function client(options: ApiClientOptions = {}): Client {
|
|
31
|
+
const apiKey = options.apiKey || process.env.WEFLAYR_API_KEY;
|
|
32
|
+
if (!apiKey) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
'weflayr.client: `apiKey` is required (pass it explicitly or set WEFLAYR_API_KEY)'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return createClient(
|
|
38
|
+
createConfig({
|
|
39
|
+
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
40
|
+
auth: () => apiKey,
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export * from './generated/facade.gen';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps a generated public-API endpoint function so its `body`/`path`/`query`
|
|
3
|
+
* are converted from camelCase to the wire's snake_case on the way out, and
|
|
4
|
+
* its `data`/`error` are converted back to camelCase on the way in.
|
|
5
|
+
*
|
|
6
|
+
* Instantiated once per endpoint in generated/facade.gen.ts, produced by
|
|
7
|
+
* scripts/generate_facade.mjs - see that script for how the per-endpoint
|
|
8
|
+
* type arguments are derived.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { toCamelCaseDeep, toSnakeCaseDeep } from './caseConversion';
|
|
12
|
+
import type { Client } from './generated/client';
|
|
13
|
+
|
|
14
|
+
export interface ClientOption {
|
|
15
|
+
client: Client;
|
|
16
|
+
throwOnError?: boolean;
|
|
17
|
+
headers?: HeadersInit;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CallResult<TData, TError> {
|
|
21
|
+
data?: TData;
|
|
22
|
+
error?: TError;
|
|
23
|
+
request?: Request;
|
|
24
|
+
response?: Response;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type WireResult = { data?: unknown; error?: unknown };
|
|
28
|
+
|
|
29
|
+
export function makeEndpoint<TParams extends object, TData, TError>(
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
|
+
wireFn: (options: any) => Promise<WireResult>
|
|
32
|
+
): (options: ClientOption & TParams) => Promise<CallResult<TData, TError>> {
|
|
33
|
+
return async (options) => {
|
|
34
|
+
const wireOptions = { ...options } as Record<string, unknown>;
|
|
35
|
+
for (const key of ['body', 'path', 'query']) {
|
|
36
|
+
if (wireOptions[key] !== undefined) {
|
|
37
|
+
wireOptions[key] = toSnakeCaseDeep(wireOptions[key]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let result: WireResult;
|
|
42
|
+
try {
|
|
43
|
+
result = await wireFn(wireOptions);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw toCamelCaseDeep(error as object);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
...result,
|
|
50
|
+
data: result.data !== undefined ? (toCamelCaseDeep(result.data) as TData) : undefined,
|
|
51
|
+
error: result.error !== undefined ? (toCamelCaseDeep(result.error) as TError) : undefined,
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
}
|
package/src/auto-instrument.js
CHANGED
|
@@ -12,16 +12,24 @@ const AiSdk = Object.freeze({
|
|
|
12
12
|
COHERE: 'cohere',
|
|
13
13
|
GOOGLE_GENAI: 'google-genai',
|
|
14
14
|
ELEVENLABS: 'elevenlabs',
|
|
15
|
+
VERCEL_AI_GATEWAY: 'vercel_ai_gateway',
|
|
15
16
|
});
|
|
16
17
|
|
|
17
|
-
// Allowed values for `propagateMetadata({ providerName })`:
|
|
18
|
-
//
|
|
19
|
-
// SDK
|
|
20
|
-
//
|
|
21
|
-
//
|
|
18
|
+
// Allowed values for `propagateMetadata({ providerName })`: the actual AI
|
|
19
|
+
// providers a call can be attributed to. Unlike AiSdk this names providers,
|
|
20
|
+
// not SDKs — google-genai is an SDK whose calls are labelled gemini or vertex,
|
|
21
|
+
// and mistral has no Node OTel instrumentor so it is reached through the
|
|
22
|
+
// OpenAI SDK (OpenAI-compatible endpoint) but reported as itself.
|
|
22
23
|
const AIProviderName = Object.freeze({
|
|
23
|
-
|
|
24
|
+
OPENAI: 'openai',
|
|
25
|
+
ANTHROPIC: 'anthropic',
|
|
26
|
+
BEDROCK: 'bedrock',
|
|
27
|
+
COHERE: 'cohere',
|
|
28
|
+
ELEVENLABS: 'elevenlabs',
|
|
29
|
+
MISTRAL: 'mistral',
|
|
24
30
|
VERCEL_AI_GATEWAY: 'vercel_ai_gateway',
|
|
31
|
+
GEMINI: 'gemini',
|
|
32
|
+
VERTEX: 'vertex',
|
|
25
33
|
});
|
|
26
34
|
|
|
27
35
|
// The primary instrumentor for each provider SDK. Most are third-party
|
|
@@ -40,6 +48,11 @@ const REGISTRY = {
|
|
|
40
48
|
requirePath: './instrumentation/elevenlabs/full_instrumentation',
|
|
41
49
|
className: 'ElevenLabsInstrumentor',
|
|
42
50
|
},
|
|
51
|
+
[AiSdk.VERCEL_AI_GATEWAY]: {
|
|
52
|
+
package: 'ai',
|
|
53
|
+
requirePath: './instrumentation/vercel/full_instrumentation',
|
|
54
|
+
className: 'VercelAiSdkInstrumentor',
|
|
55
|
+
},
|
|
43
56
|
};
|
|
44
57
|
|
|
45
58
|
// One complementary instrumentor per provider: a `set_telemetry_attributes`
|
package/src/index.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
const { setup, flush } = require('./setup');
|
|
4
4
|
const { autoInstrument, AiSdk, AIProviderName } = require('./auto-instrument');
|
|
5
5
|
const { propagateMetadata } = require('./otel/propagation');
|
|
6
|
+
// Public-API client (revenue, key metrics, customer tags), compiled from the
|
|
7
|
+
// TypeScript in src/api by `npm run build`.
|
|
8
|
+
const api = require('../dist/api');
|
|
6
9
|
|
|
7
10
|
module.exports = {
|
|
8
11
|
setup,
|
|
@@ -11,4 +14,6 @@ module.exports = {
|
|
|
11
14
|
AiSdk,
|
|
12
15
|
AIProviderName,
|
|
13
16
|
propagateMetadata,
|
|
17
|
+
client: api.client,
|
|
18
|
+
api,
|
|
14
19
|
};
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* `gen_ai.provider.name = "gcp.gen_ai"` and can't tell Vertex from the Gemini
|
|
8
8
|
* Developer API. We read the client's Vertex flag and return the value to set as
|
|
9
9
|
* `gen_ai.system` (which the normalizer prefers): `vertex_ai` / `gemini`,
|
|
10
|
-
* matching what the Python google-genai instrumentor reports natively.
|
|
10
|
+
* matching what the Python google-genai instrumentor reports natively. The
|
|
11
|
+
* worker normalizes `vertex_ai` to the canonical `vertex`. Node-only.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
// Read the Vertex-vs-Dev backend off the Models instance's api client.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Instrumentor for the Vercel AI SDK (`ai` package, v7+).
|
|
5
|
+
*
|
|
6
|
+
* The AI SDK ships its own OTel integration in `@ai-sdk/otel`; instrumenting
|
|
7
|
+
* is registering it globally so every generateText/streamText/embed call emits
|
|
8
|
+
* GenAI semconv spans through the tracer provider weflayr's setup() installed.
|
|
9
|
+
* `usage: true` adds the `ai.usage.*` token details (reasoning tokens, uncached
|
|
10
|
+
* input) that the semconv attributes do not carry.
|
|
11
|
+
*
|
|
12
|
+
* The aggregate spans the integration emits around each model call
|
|
13
|
+
* (invoke_agent, agent_step, duplicated embeddings spans) are filtered out
|
|
14
|
+
* before export by otel/vercel-ai-sdk-span-filter.js.
|
|
15
|
+
*/
|
|
16
|
+
class VercelAiSdkInstrumentor {
|
|
17
|
+
instrument() {
|
|
18
|
+
let registerTelemetry;
|
|
19
|
+
let OpenTelemetry;
|
|
20
|
+
try {
|
|
21
|
+
({ registerTelemetry } = require('ai'));
|
|
22
|
+
({ OpenTelemetry } = require('@ai-sdk/otel'));
|
|
23
|
+
} catch (err) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
'weflayr.autoInstrument("vercel_ai_gateway"): failed to load "ai" / "@ai-sdk/otel". ' +
|
|
26
|
+
`Install them with: npm install ai @ai-sdk/otel\nUnderlying error: ${err.message}`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
// registerTelemetry only exists since v7.
|
|
30
|
+
if (typeof registerTelemetry !== 'function') {
|
|
31
|
+
throw new Error(
|
|
32
|
+
'weflayr.autoInstrument("vercel_ai_gateway") requires version 7+ of the Vercel AI SDK ' +
|
|
33
|
+
'(the "ai" package), which added global telemetry registration. ' +
|
|
34
|
+
'Upgrade with: npm install ai@latest @ai-sdk/otel.\n' +
|
|
35
|
+
'On ai <= 6 telemetry can only be enabled per call: remove "vercel_ai_gateway" from ' +
|
|
36
|
+
'aiSdks (pass an empty list if no other sdks need to be instrumented). Instead, make sure ' +
|
|
37
|
+
'experimental_telemetry: { isEnabled: true } is passed on each generateText/streamText call.'
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
registerTelemetry(new OpenTelemetry({ usage: true }));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { VercelAiSdkInstrumentor };
|
package/src/otel/propagation.js
CHANGED
|
@@ -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,
|
|
9
|
+
// (featureName, customerName, providerName, 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
|
|
@@ -23,7 +23,7 @@ const { context: otelContext, trace, createContextKey } = require('@opentelemetr
|
|
|
23
23
|
const WEFLAYR_METADATA_CONTEXT_KEY = createContextKey('weflayr_metadata');
|
|
24
24
|
|
|
25
25
|
const FEATURE_NAME_ATTR = 'weflayr.feature_name';
|
|
26
|
-
const
|
|
26
|
+
const CUSTOMER_NAME_ATTR = 'weflayr.customer_name';
|
|
27
27
|
const PROVIDER_NAME_ATTR = 'weflayr.provider_name';
|
|
28
28
|
const UUID_ATTR = 'weflayr.uuid';
|
|
29
29
|
// Prefix for user-supplied `extraTags` so downstream normalizers can pick
|
|
@@ -36,10 +36,10 @@ function getMergedMetadataFromContext(parentContext) {
|
|
|
36
36
|
return existing && typeof existing === 'object' ? { ...existing } : {};
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
function _buildAttributes({ featureName,
|
|
39
|
+
function _buildAttributes({ featureName, customerName, providerName, extraTags, spanUuid }) {
|
|
40
40
|
const attributes = {
|
|
41
41
|
[FEATURE_NAME_ATTR]: featureName,
|
|
42
|
-
[
|
|
42
|
+
[CUSTOMER_NAME_ATTR]: customerName,
|
|
43
43
|
[UUID_ATTR]: spanUuid,
|
|
44
44
|
};
|
|
45
45
|
for (const [key, value] of Object.entries(extraTags)) {
|
|
@@ -63,25 +63,34 @@ function _buildAttributes({ featureName, customerId, providerName, extraTags, sp
|
|
|
63
63
|
*/
|
|
64
64
|
function propagateMetadata(options, fn) {
|
|
65
65
|
const { featureName, customerId } = options ?? {};
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
let { customerName } = options ?? {};
|
|
67
|
+
if (customerId != null) {
|
|
68
|
+
process.emitWarning(
|
|
69
|
+
'propagateMetadata: `customerId` is deprecated, use `customerName`',
|
|
70
|
+
'DeprecationWarning'
|
|
71
|
+
);
|
|
72
|
+
customerName = customerName ?? customerId;
|
|
68
73
|
}
|
|
74
|
+
if (featureName == null || customerName == null) {
|
|
75
|
+
throw new Error('propagateMetadata: `featureName` and `customerName` are required');
|
|
76
|
+
}
|
|
77
|
+
const normalizedOptions = { ...options, customerName };
|
|
69
78
|
|
|
70
79
|
if (typeof fn === 'function') {
|
|
71
|
-
return _runWithMetadata(
|
|
80
|
+
return _runWithMetadata(normalizedOptions, fn);
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
return function decorator(target, key, descriptor) {
|
|
75
84
|
if (descriptor && typeof descriptor.value === 'function') {
|
|
76
85
|
const original = descriptor.value;
|
|
77
86
|
descriptor.value = function wrapped(...args) {
|
|
78
|
-
return _runWithMetadata(
|
|
87
|
+
return _runWithMetadata(normalizedOptions, () => original.apply(this, args));
|
|
79
88
|
};
|
|
80
89
|
return descriptor;
|
|
81
90
|
}
|
|
82
91
|
if (typeof target === 'function') {
|
|
83
92
|
return function wrapped(...args) {
|
|
84
|
-
return _runWithMetadata(
|
|
93
|
+
return _runWithMetadata(normalizedOptions, () => target.apply(this, args));
|
|
85
94
|
};
|
|
86
95
|
}
|
|
87
96
|
return target;
|
|
@@ -89,10 +98,10 @@ function propagateMetadata(options, fn) {
|
|
|
89
98
|
}
|
|
90
99
|
|
|
91
100
|
function _runWithMetadata(options, fn) {
|
|
92
|
-
const { featureName,
|
|
101
|
+
const { featureName, customerName, providerName, extraTags } = options;
|
|
93
102
|
const attributes = _buildAttributes({
|
|
94
103
|
featureName,
|
|
95
|
-
|
|
104
|
+
customerName,
|
|
96
105
|
providerName,
|
|
97
106
|
extraTags: extraTags ?? {},
|
|
98
107
|
spanUuid: randomUUID().replace(/-/g, ''),
|
|
@@ -117,7 +126,7 @@ module.exports = {
|
|
|
117
126
|
getMergedMetadataFromContext,
|
|
118
127
|
WEFLAYR_METADATA_CONTEXT_KEY,
|
|
119
128
|
FEATURE_NAME_ATTR,
|
|
120
|
-
|
|
129
|
+
CUSTOMER_NAME_ATTR,
|
|
121
130
|
PROVIDER_NAME_ATTR,
|
|
122
131
|
UUID_ATTR,
|
|
123
132
|
EXTRA_TAG_PREFIX,
|
|
@@ -20,6 +20,10 @@ const { SpanStatusCode } = require('@opentelemetry/api');
|
|
|
20
20
|
const { BatchSpanProcessor, SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base');
|
|
21
21
|
const { EXTRA_TAG_PREFIX, getMergedMetadataFromContext } = require('./propagation');
|
|
22
22
|
const { isDefaultExportSpan, isKnownLLMInstrumentor } = require('./span-filter');
|
|
23
|
+
const {
|
|
24
|
+
VercelAiSdkSpanFilter,
|
|
25
|
+
stampVercelGatewayProviderName,
|
|
26
|
+
} = require('./vercel-ai-sdk-span-filter');
|
|
23
27
|
const { debug } = require('../logger');
|
|
24
28
|
|
|
25
29
|
// setup({captureMessageContent: ...}) records the choice in this env var (the
|
|
@@ -57,6 +61,7 @@ class WeflayrSpanProcessor {
|
|
|
57
61
|
this._defaultTags = defaultTags;
|
|
58
62
|
this._mask = mask;
|
|
59
63
|
this._shouldExportSpan = shouldExportSpan ?? isDefaultExportSpan;
|
|
64
|
+
this._vercelAiSdkSpanFilter = new VercelAiSdkSpanFilter();
|
|
60
65
|
// LLM spans we've started but not yet seen end. On forceFlush we end any
|
|
61
66
|
// leftovers with ERROR status — covers cases like openllmetry's anthropic
|
|
62
67
|
// streaming wrapper, which leaks the span when the underlying call
|
|
@@ -75,6 +80,7 @@ class WeflayrSpanProcessor {
|
|
|
75
80
|
if (isKnownLLMInstrumentor(span)) {
|
|
76
81
|
this._openLlmSpans.add(span);
|
|
77
82
|
}
|
|
83
|
+
this._vercelAiSdkSpanFilter.onStart(span);
|
|
78
84
|
debug(
|
|
79
85
|
'span start: name=%s defaultTags=%d propagatedMetadata=%d',
|
|
80
86
|
span.name,
|
|
@@ -87,7 +93,11 @@ class WeflayrSpanProcessor {
|
|
|
87
93
|
onEnd(span) {
|
|
88
94
|
this._openLlmSpans.delete(span);
|
|
89
95
|
try {
|
|
90
|
-
|
|
96
|
+
const vercelVerdict = this._vercelAiSdkSpanFilter.shouldExport(span);
|
|
97
|
+
if (vercelVerdict === true) {
|
|
98
|
+
stampVercelGatewayProviderName(span);
|
|
99
|
+
}
|
|
100
|
+
if (vercelVerdict === false || (vercelVerdict === null && !this._shouldExportSpan(span))) {
|
|
91
101
|
debug(
|
|
92
102
|
'span end: filtered out name=%s scope=%s',
|
|
93
103
|
span.name,
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The Vercel AI SDK's OTel integrations emit aggregate spans around each model
|
|
4
|
+
// call, all repeating the same usage counts:
|
|
5
|
+
// - `@ai-sdk/otel` (ai >= 7, scope "gen_ai"): per-call "chat" spans are
|
|
6
|
+
// wrapped in "agent_step" + "invoke_agent" spans, and embed/embedMany
|
|
7
|
+
// emits an outer "embeddings" operation span around the model-call
|
|
8
|
+
// "embeddings" span(s).
|
|
9
|
+
// - the legacy integration (ai <= 6, scope "ai"): `ai.<fn>.do<Call>` spans
|
|
10
|
+
// are wrapped in an outer `ai.<fn>` span.
|
|
11
|
+
// Exactly one span per model call may reach the exporter — exporting an
|
|
12
|
+
// aggregate alongside its children would bill every call twice.
|
|
13
|
+
|
|
14
|
+
const VERCEL_SEMCONV_SCOPE = 'gen_ai';
|
|
15
|
+
const VERCEL_LEGACY_SCOPE = 'ai';
|
|
16
|
+
|
|
17
|
+
const LEGACY_MODEL_CALL_SPAN_NAME = /\.do(Generate|Stream|Embed)$/;
|
|
18
|
+
|
|
19
|
+
function _parentSpanId(span) {
|
|
20
|
+
return span.parentSpanContext?.spanId ?? span.parentSpanId;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class VercelAiSdkSpanFilter {
|
|
24
|
+
constructor() {
|
|
25
|
+
// spanId -> gen_ai.operation.name for open "gen_ai"-scope spans, so a
|
|
26
|
+
// span can check what its parent is (children end before their parent).
|
|
27
|
+
this._openSpanOperations = new Map();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
onStart(span) {
|
|
31
|
+
if (span.instrumentationScope?.name !== VERCEL_SEMCONV_SCOPE) return;
|
|
32
|
+
this._openSpanOperations.set(
|
|
33
|
+
span.spanContext().spanId,
|
|
34
|
+
span.attributes['gen_ai.operation.name']
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Export verdict for a Vercel AI SDK span, or null when the span is not
|
|
40
|
+
* one (the caller falls back to the default filter).
|
|
41
|
+
*/
|
|
42
|
+
shouldExport(span) {
|
|
43
|
+
const scope = span.instrumentationScope?.name;
|
|
44
|
+
if (scope === VERCEL_LEGACY_SCOPE) {
|
|
45
|
+
return LEGACY_MODEL_CALL_SPAN_NAME.test(span.name);
|
|
46
|
+
}
|
|
47
|
+
if (scope !== VERCEL_SEMCONV_SCOPE) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const operation = span.attributes['gen_ai.operation.name'];
|
|
51
|
+
this._openSpanOperations.delete(span.spanContext().spanId);
|
|
52
|
+
if (operation === 'chat') {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (operation === 'embeddings') {
|
|
56
|
+
// Keep the outer aggregate (one event per embed/embedMany call, with the
|
|
57
|
+
// total usage) and drop the nested model-call span(s) repeating it.
|
|
58
|
+
return this._openSpanOperations.get(_parentSpanId(span)) !== 'embeddings';
|
|
59
|
+
}
|
|
60
|
+
// invoke_agent / agent_step (usage aggregates) and execute_tool (no usage).
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// `@ai-sdk/otel` reports the Vercel AI Gateway as the bare provider id
|
|
66
|
+
// "gateway" — too generic a name to special-case downstream. Stamp the weflayr
|
|
67
|
+
// provider override (which the normalizer prefers) with the canonical name.
|
|
68
|
+
// A providerName set through propagateMetadata was stamped at span start and
|
|
69
|
+
// wins.
|
|
70
|
+
function stampVercelGatewayProviderName(span) {
|
|
71
|
+
if (
|
|
72
|
+
span.attributes['gen_ai.provider.name'] === 'gateway' &&
|
|
73
|
+
span.attributes['weflayr.provider_name'] === undefined
|
|
74
|
+
) {
|
|
75
|
+
span.attributes['weflayr.provider_name'] = 'vercel_ai_gateway';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { VercelAiSdkSpanFilter, stampVercelGatewayProviderName };
|