@ai-sdk/otel 1.0.123 → 1.0.126
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/CHANGELOG.md +24 -0
- package/dist/index.d.ts +13 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +237 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/gen-ai-format-messages.ts +3 -0
- package/src/legacy-open-telemetry.ts +153 -2
- package/src/open-telemetry.ts +217 -2
- package/src/provider-usage-attributes.ts +69 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/otel",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.126",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@ai-sdk/provider": "4.0.
|
|
29
|
+
"@ai-sdk/provider": "4.0.21",
|
|
30
30
|
"@opentelemetry/api": "1.9.1",
|
|
31
|
-
"ai": "7.0.
|
|
31
|
+
"ai": "7.0.126"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
34
|
+
"@ai-sdk/provider-utils": "5.0.53",
|
|
35
35
|
"@opentelemetry/sdk-trace-base": "2.7.1",
|
|
36
36
|
"@types/node": "22.19.19",
|
|
37
37
|
"@vercel/ai-tsconfig": "0.0.0",
|
|
@@ -120,6 +120,9 @@ export function mapOperationName(operationId: string): string {
|
|
|
120
120
|
'ai.embed': 'embeddings',
|
|
121
121
|
'ai.embedMany': 'embeddings',
|
|
122
122
|
'ai.rerank': 'rerank',
|
|
123
|
+
'ai.generateSpeech': 'ai.generateSpeech',
|
|
124
|
+
'ai.transcribe': 'ai.transcribe',
|
|
125
|
+
'ai.streamTranscribe': 'ai.streamTranscribe',
|
|
123
126
|
};
|
|
124
127
|
return mapping[operationId] ?? operationId;
|
|
125
128
|
}
|
|
@@ -24,6 +24,8 @@ import type {
|
|
|
24
24
|
GenerateTextStartEvent,
|
|
25
25
|
GenerateTextStepEndEvent,
|
|
26
26
|
GenerateTextStepStartEvent,
|
|
27
|
+
GenerateSpeechEndEvent,
|
|
28
|
+
GenerateSpeechStartEvent,
|
|
27
29
|
ToolExecutionEndEvent,
|
|
28
30
|
ToolExecutionStartEvent,
|
|
29
31
|
Experimental_EvaluateEndEvent as EvaluateEndEvent,
|
|
@@ -35,6 +37,10 @@ import type {
|
|
|
35
37
|
RerankEndEvent,
|
|
36
38
|
RerankStartEvent,
|
|
37
39
|
RerankingModelCallStartEvent,
|
|
40
|
+
TranscriptionEndEvent,
|
|
41
|
+
TranscriptionStartEvent,
|
|
42
|
+
Experimental_StreamTranscriptionEndEvent as StreamTranscriptionEndEvent,
|
|
43
|
+
Experimental_StreamTranscriptionStartEvent as StreamTranscriptionStartEvent,
|
|
38
44
|
InferTelemetryEvent,
|
|
39
45
|
Telemetry,
|
|
40
46
|
TelemetryOptions,
|
|
@@ -42,6 +48,7 @@ import type {
|
|
|
42
48
|
} from 'ai';
|
|
43
49
|
import { assembleOperationName } from './assemble-operation-name';
|
|
44
50
|
import { getBaseTelemetryAttributes } from './get-base-telemetry-attributes';
|
|
51
|
+
import { getProviderUsageAttributes } from './provider-usage-attributes';
|
|
45
52
|
import { sanitizeAttributeValue } from './sanitize-attribute-value';
|
|
46
53
|
import { stringifyForTelemetry } from './stringify-for-telemetry';
|
|
47
54
|
|
|
@@ -213,8 +220,24 @@ export class LegacyOpenTelemetry implements Telemetry {
|
|
|
213
220
|
| InferTelemetryEvent<GenerateTextStartEvent>
|
|
214
221
|
| InferTelemetryEvent<GenerateObjectStartEvent>
|
|
215
222
|
| InferTelemetryEvent<EmbedStartEvent>
|
|
216
|
-
| InferTelemetryEvent<RerankStartEvent
|
|
223
|
+
| InferTelemetryEvent<RerankStartEvent>
|
|
224
|
+
| InferTelemetryEvent<GenerateSpeechStartEvent>
|
|
225
|
+
| InferTelemetryEvent<TranscriptionStartEvent>,
|
|
217
226
|
): void {
|
|
227
|
+
if (event.operationId === 'ai.generateSpeech') {
|
|
228
|
+
this.onAudioOperationStart(
|
|
229
|
+
event as InferTelemetryEvent<GenerateSpeechStartEvent>,
|
|
230
|
+
);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (event.operationId === 'ai.transcribe') {
|
|
235
|
+
this.onAudioOperationStart(
|
|
236
|
+
event as InferTelemetryEvent<TranscriptionStartEvent>,
|
|
237
|
+
);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
218
241
|
if (
|
|
219
242
|
event.operationId === 'ai.embed' ||
|
|
220
243
|
event.operationId === 'ai.embedMany'
|
|
@@ -243,6 +266,69 @@ export class LegacyOpenTelemetry implements Telemetry {
|
|
|
243
266
|
this.onGenerateStart(event as InferTelemetryEvent<GenerateTextStartEvent>);
|
|
244
267
|
}
|
|
245
268
|
|
|
269
|
+
experimental_onStreamTranscriptionStart(
|
|
270
|
+
event: InferTelemetryEvent<StreamTranscriptionStartEvent>,
|
|
271
|
+
): void {
|
|
272
|
+
this.onAudioOperationStart(event);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private onAudioOperationStart(
|
|
276
|
+
event: InferTelemetryEvent<
|
|
277
|
+
| GenerateSpeechStartEvent
|
|
278
|
+
| TranscriptionStartEvent
|
|
279
|
+
| StreamTranscriptionStartEvent
|
|
280
|
+
>,
|
|
281
|
+
): void {
|
|
282
|
+
const telemetry: TelemetryOptions = {
|
|
283
|
+
recordInputs: event.recordInputs,
|
|
284
|
+
recordOutputs: event.recordOutputs,
|
|
285
|
+
functionId: event.functionId,
|
|
286
|
+
};
|
|
287
|
+
const audio = 'audio' in event ? event.audio : undefined;
|
|
288
|
+
const text = 'text' in event ? event.text : undefined;
|
|
289
|
+
const baseTelemetryAttributes: Attributes = {
|
|
290
|
+
'ai.model.provider': event.provider,
|
|
291
|
+
'ai.model.id': event.modelId,
|
|
292
|
+
...Object.fromEntries(
|
|
293
|
+
Object.entries(event.headers ?? {}).map(([key, value]) => [
|
|
294
|
+
`ai.request.headers.${key}`,
|
|
295
|
+
value,
|
|
296
|
+
]),
|
|
297
|
+
),
|
|
298
|
+
};
|
|
299
|
+
const attributes = selectAttributes(telemetry, {
|
|
300
|
+
...assembleOperationName({
|
|
301
|
+
operationId: event.operationId,
|
|
302
|
+
telemetry,
|
|
303
|
+
}),
|
|
304
|
+
...baseTelemetryAttributes,
|
|
305
|
+
'ai.request.text': text == null ? undefined : { input: () => text },
|
|
306
|
+
'ai.request.audio.size':
|
|
307
|
+
audio?.byteLength == null
|
|
308
|
+
? undefined
|
|
309
|
+
: { input: () => audio.byteLength },
|
|
310
|
+
'ai.request.audio.mediaType':
|
|
311
|
+
audio == null ? undefined : { input: () => audio.mediaType },
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const rootSpan = this.tracer.startSpan(event.operationId, { attributes });
|
|
315
|
+
const rootContext = trace.setSpan(context.active(), rootSpan);
|
|
316
|
+
this.callStates.set(event.callId, {
|
|
317
|
+
operationId: event.operationId,
|
|
318
|
+
telemetry,
|
|
319
|
+
rootSpan,
|
|
320
|
+
rootContext,
|
|
321
|
+
stepSpan: undefined,
|
|
322
|
+
stepContext: undefined,
|
|
323
|
+
embedSpans: new Map(),
|
|
324
|
+
rerankSpan: undefined,
|
|
325
|
+
evaluationSpan: undefined,
|
|
326
|
+
toolSpans: new Map(),
|
|
327
|
+
baseTelemetryAttributes,
|
|
328
|
+
settings: {},
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
246
332
|
private onGenerateStart(
|
|
247
333
|
event: InferTelemetryEvent<GenerateTextStartEvent>,
|
|
248
334
|
): void {
|
|
@@ -793,7 +879,9 @@ export class LegacyOpenTelemetry implements Telemetry {
|
|
|
793
879
|
| GenerateTextEndEvent<ToolSet>
|
|
794
880
|
| GenerateObjectEndEvent<unknown>
|
|
795
881
|
| EmbedEndEvent
|
|
796
|
-
| RerankEndEvent
|
|
882
|
+
| RerankEndEvent
|
|
883
|
+
| GenerateSpeechEndEvent
|
|
884
|
+
| TranscriptionEndEvent,
|
|
797
885
|
): void {
|
|
798
886
|
const state = this.getCallState(event.callId);
|
|
799
887
|
if (!state?.rootSpan) return;
|
|
@@ -811,6 +899,16 @@ export class LegacyOpenTelemetry implements Telemetry {
|
|
|
811
899
|
return;
|
|
812
900
|
}
|
|
813
901
|
|
|
902
|
+
if (
|
|
903
|
+
state.operationId === 'ai.generateSpeech' ||
|
|
904
|
+
state.operationId === 'ai.transcribe'
|
|
905
|
+
) {
|
|
906
|
+
this.onAudioOperationEnd(
|
|
907
|
+
event as GenerateSpeechEndEvent | TranscriptionEndEvent,
|
|
908
|
+
);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
|
|
814
912
|
if (
|
|
815
913
|
state.operationId === 'ai.generateObject' ||
|
|
816
914
|
state.operationId === 'ai.streamObject'
|
|
@@ -822,6 +920,59 @@ export class LegacyOpenTelemetry implements Telemetry {
|
|
|
822
920
|
this.onGenerateEnd(event as GenerateTextEndEvent<ToolSet>);
|
|
823
921
|
}
|
|
824
922
|
|
|
923
|
+
experimental_onStreamTranscriptionEnd(
|
|
924
|
+
event: InferTelemetryEvent<StreamTranscriptionEndEvent>,
|
|
925
|
+
): void {
|
|
926
|
+
this.onAudioOperationEnd(event);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
private onAudioOperationEnd(
|
|
930
|
+
event:
|
|
931
|
+
| GenerateSpeechEndEvent
|
|
932
|
+
| TranscriptionEndEvent
|
|
933
|
+
| StreamTranscriptionEndEvent,
|
|
934
|
+
): void {
|
|
935
|
+
const state = this.getCallState(event.callId);
|
|
936
|
+
if (!state?.rootSpan) return;
|
|
937
|
+
|
|
938
|
+
const isSpeech = event.operationId === 'ai.generateSpeech';
|
|
939
|
+
const audio = event.audio;
|
|
940
|
+
state.rootSpan.setAttributes(
|
|
941
|
+
selectAttributes(state.telemetry, {
|
|
942
|
+
'ai.request.audio.size': isSpeech
|
|
943
|
+
? undefined
|
|
944
|
+
: { input: () => audio.byteLength },
|
|
945
|
+
'ai.request.audio.mediaType': isSpeech
|
|
946
|
+
? undefined
|
|
947
|
+
: { input: () => audio.mediaType },
|
|
948
|
+
'ai.response.text':
|
|
949
|
+
'segments' in event ? { output: () => event.text } : undefined,
|
|
950
|
+
'ai.response.audio.size': isSpeech
|
|
951
|
+
? { output: () => audio.byteLength }
|
|
952
|
+
: undefined,
|
|
953
|
+
'ai.response.audio.mediaType': isSpeech
|
|
954
|
+
? { output: () => audio.mediaType }
|
|
955
|
+
: undefined,
|
|
956
|
+
'ai.response.audio.format':
|
|
957
|
+
isSpeech && 'format' in audio
|
|
958
|
+
? { output: () => audio.format }
|
|
959
|
+
: undefined,
|
|
960
|
+
...getProviderUsageAttributes({
|
|
961
|
+
usage: event.usage,
|
|
962
|
+
prefix: 'ai.usage',
|
|
963
|
+
}),
|
|
964
|
+
'ai.response.usage':
|
|
965
|
+
event.usage == null ? undefined : JSON.stringify(event.usage),
|
|
966
|
+
'ai.response.providerMetadata':
|
|
967
|
+
event.providerMetadata == null
|
|
968
|
+
? undefined
|
|
969
|
+
: JSON.stringify(event.providerMetadata),
|
|
970
|
+
}),
|
|
971
|
+
);
|
|
972
|
+
state.rootSpan.end();
|
|
973
|
+
this.cleanupCallState(event.callId);
|
|
974
|
+
}
|
|
975
|
+
|
|
825
976
|
private onGenerateEnd(event: GenerateTextEndEvent<ToolSet>): void {
|
|
826
977
|
const state = this.getCallState(event.callId);
|
|
827
978
|
if (!state?.rootSpan) return;
|
package/src/open-telemetry.ts
CHANGED
|
@@ -23,6 +23,8 @@ import type {
|
|
|
23
23
|
GenerateTextStartEvent,
|
|
24
24
|
GenerateTextStepEndEvent,
|
|
25
25
|
GenerateTextStepStartEvent,
|
|
26
|
+
GenerateSpeechEndEvent,
|
|
27
|
+
GenerateSpeechStartEvent,
|
|
26
28
|
ToolExecutionEndEvent,
|
|
27
29
|
ToolExecutionStartEvent,
|
|
28
30
|
Experimental_EvaluateEndEvent as EvaluateEndEvent,
|
|
@@ -33,6 +35,10 @@ import type {
|
|
|
33
35
|
RerankEndEvent,
|
|
34
36
|
RerankStartEvent,
|
|
35
37
|
RerankingModelCallStartEvent,
|
|
38
|
+
TranscriptionEndEvent,
|
|
39
|
+
TranscriptionStartEvent,
|
|
40
|
+
Experimental_StreamTranscriptionEndEvent as StreamTranscriptionEndEvent,
|
|
41
|
+
Experimental_StreamTranscriptionStartEvent as StreamTranscriptionStartEvent,
|
|
36
42
|
InferTelemetryEvent,
|
|
37
43
|
Telemetry,
|
|
38
44
|
TelemetryOptions,
|
|
@@ -48,6 +54,7 @@ import {
|
|
|
48
54
|
mapProviderName,
|
|
49
55
|
} from './gen-ai-format-messages';
|
|
50
56
|
import { recordErrorOnSpan } from './record-span';
|
|
57
|
+
import { getProviderUsageAttributes } from './provider-usage-attributes';
|
|
51
58
|
import { sanitizeAttributes } from './sanitize-attribute-value';
|
|
52
59
|
import { selectAttributes } from './select-attributes';
|
|
53
60
|
import {
|
|
@@ -209,8 +216,24 @@ export class OpenTelemetry implements Telemetry {
|
|
|
209
216
|
| InferTelemetryEvent<GenerateTextStartEvent>
|
|
210
217
|
| InferTelemetryEvent<GenerateObjectStartEvent>
|
|
211
218
|
| InferTelemetryEvent<EmbedStartEvent>
|
|
212
|
-
| InferTelemetryEvent<RerankStartEvent
|
|
219
|
+
| InferTelemetryEvent<RerankStartEvent>
|
|
220
|
+
| InferTelemetryEvent<GenerateSpeechStartEvent>
|
|
221
|
+
| InferTelemetryEvent<TranscriptionStartEvent>,
|
|
213
222
|
): void {
|
|
223
|
+
if (event.operationId === 'ai.generateSpeech') {
|
|
224
|
+
this.onAudioOperationStart(
|
|
225
|
+
event as InferTelemetryEvent<GenerateSpeechStartEvent>,
|
|
226
|
+
);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (event.operationId === 'ai.transcribe') {
|
|
231
|
+
this.onAudioOperationStart(
|
|
232
|
+
event as InferTelemetryEvent<TranscriptionStartEvent>,
|
|
233
|
+
);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
214
237
|
if (
|
|
215
238
|
event.operationId === 'ai.embed' ||
|
|
216
239
|
event.operationId === 'ai.embedMany'
|
|
@@ -239,6 +262,102 @@ export class OpenTelemetry implements Telemetry {
|
|
|
239
262
|
this.onGenerateStart(event as InferTelemetryEvent<GenerateTextStartEvent>);
|
|
240
263
|
}
|
|
241
264
|
|
|
265
|
+
experimental_onStreamTranscriptionStart(
|
|
266
|
+
event: InferTelemetryEvent<StreamTranscriptionStartEvent>,
|
|
267
|
+
): void {
|
|
268
|
+
this.onAudioOperationStart(event);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private onAudioOperationStart(
|
|
272
|
+
event: InferTelemetryEvent<
|
|
273
|
+
| GenerateSpeechStartEvent
|
|
274
|
+
| TranscriptionStartEvent
|
|
275
|
+
| StreamTranscriptionStartEvent
|
|
276
|
+
>,
|
|
277
|
+
): void {
|
|
278
|
+
const telemetry: TelemetryOptions = {
|
|
279
|
+
recordInputs: event.recordInputs,
|
|
280
|
+
recordOutputs: event.recordOutputs,
|
|
281
|
+
functionId: event.functionId,
|
|
282
|
+
};
|
|
283
|
+
const providerName = mapProviderName(event.provider);
|
|
284
|
+
const operationName = mapOperationName(event.operationId);
|
|
285
|
+
const audio = 'audio' in event ? event.audio : undefined;
|
|
286
|
+
const text = 'text' in event ? event.text : undefined;
|
|
287
|
+
const baseSupplementalAttributes = selectSupplementalAttributes(
|
|
288
|
+
telemetry,
|
|
289
|
+
this.supplementalAttributes,
|
|
290
|
+
{
|
|
291
|
+
headers: getHeaderAttributes(event.headers),
|
|
292
|
+
},
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const attributes = selectAttributes(telemetry, {
|
|
296
|
+
'gen_ai.operation.name': operationName,
|
|
297
|
+
'gen_ai.provider.name': providerName,
|
|
298
|
+
'gen_ai.request.model': event.modelId,
|
|
299
|
+
'gen_ai.agent.name': telemetry.functionId,
|
|
300
|
+
'gen_ai.output.type':
|
|
301
|
+
event.operationId === 'ai.generateSpeech' ? 'speech' : 'text',
|
|
302
|
+
'gen_ai.request.stream': event.operationId === 'ai.streamTranscribe',
|
|
303
|
+
'gen_ai.input.messages':
|
|
304
|
+
text == null
|
|
305
|
+
? undefined
|
|
306
|
+
: {
|
|
307
|
+
input: () =>
|
|
308
|
+
JSON.stringify([
|
|
309
|
+
{
|
|
310
|
+
role: 'user',
|
|
311
|
+
parts: [{ type: 'text', content: text }],
|
|
312
|
+
},
|
|
313
|
+
]),
|
|
314
|
+
},
|
|
315
|
+
'ai.request.text': text == null ? undefined : { input: () => text },
|
|
316
|
+
'ai.request.audio.size':
|
|
317
|
+
audio?.byteLength == null
|
|
318
|
+
? undefined
|
|
319
|
+
: { input: () => audio.byteLength },
|
|
320
|
+
'ai.request.audio.media_type':
|
|
321
|
+
audio == null ? undefined : { input: () => audio.mediaType },
|
|
322
|
+
...baseSupplementalAttributes,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const rootSpan = this.tracer.startSpan(
|
|
326
|
+
`${operationName} ${event.modelId}`,
|
|
327
|
+
{
|
|
328
|
+
attributes: this.getSpanAttributes({
|
|
329
|
+
attributes,
|
|
330
|
+
spanType: 'operation',
|
|
331
|
+
operationId: event.operationId,
|
|
332
|
+
callId: event.callId,
|
|
333
|
+
runtimeContext: undefined,
|
|
334
|
+
}),
|
|
335
|
+
kind: SpanKind.CLIENT,
|
|
336
|
+
},
|
|
337
|
+
);
|
|
338
|
+
const rootContext = trace.setSpan(context.active(), rootSpan);
|
|
339
|
+
|
|
340
|
+
this.callStates.set(event.callId, {
|
|
341
|
+
operationId: event.operationId,
|
|
342
|
+
telemetry,
|
|
343
|
+
rootSpan,
|
|
344
|
+
rootContext,
|
|
345
|
+
stepSpan: undefined,
|
|
346
|
+
stepContext: undefined,
|
|
347
|
+
inferenceSpan: undefined,
|
|
348
|
+
inferenceContext: undefined,
|
|
349
|
+
embedSpans: new Map(),
|
|
350
|
+
rerankSpan: undefined,
|
|
351
|
+
evaluationSpan: undefined,
|
|
352
|
+
toolSpans: new Map(),
|
|
353
|
+
settings: {},
|
|
354
|
+
provider: event.provider,
|
|
355
|
+
modelId: event.modelId,
|
|
356
|
+
runtimeContext: undefined,
|
|
357
|
+
baseSupplementalAttributes,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
242
361
|
private onGenerateStart(
|
|
243
362
|
event: InferTelemetryEvent<GenerateTextStartEvent>,
|
|
244
363
|
): void {
|
|
@@ -1032,7 +1151,9 @@ export class OpenTelemetry implements Telemetry {
|
|
|
1032
1151
|
| GenerateTextEndEvent<ToolSet>
|
|
1033
1152
|
| GenerateObjectEndEvent<unknown>
|
|
1034
1153
|
| EmbedEndEvent
|
|
1035
|
-
| RerankEndEvent
|
|
1154
|
+
| RerankEndEvent
|
|
1155
|
+
| GenerateSpeechEndEvent
|
|
1156
|
+
| TranscriptionEndEvent,
|
|
1036
1157
|
): void {
|
|
1037
1158
|
const state = this.getCallState(event.callId);
|
|
1038
1159
|
if (!state?.rootSpan) return;
|
|
@@ -1050,6 +1171,16 @@ export class OpenTelemetry implements Telemetry {
|
|
|
1050
1171
|
return;
|
|
1051
1172
|
}
|
|
1052
1173
|
|
|
1174
|
+
if (
|
|
1175
|
+
state.operationId === 'ai.generateSpeech' ||
|
|
1176
|
+
state.operationId === 'ai.transcribe'
|
|
1177
|
+
) {
|
|
1178
|
+
this.onAudioOperationEnd(
|
|
1179
|
+
event as GenerateSpeechEndEvent | TranscriptionEndEvent,
|
|
1180
|
+
);
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1053
1184
|
if (
|
|
1054
1185
|
state.operationId === 'ai.generateObject' ||
|
|
1055
1186
|
state.operationId === 'ai.streamObject'
|
|
@@ -1061,6 +1192,90 @@ export class OpenTelemetry implements Telemetry {
|
|
|
1061
1192
|
this.onGenerateEnd(event as GenerateTextEndEvent<ToolSet>);
|
|
1062
1193
|
}
|
|
1063
1194
|
|
|
1195
|
+
experimental_onStreamTranscriptionEnd(
|
|
1196
|
+
event: InferTelemetryEvent<StreamTranscriptionEndEvent>,
|
|
1197
|
+
): void {
|
|
1198
|
+
this.onAudioOperationEnd(event);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
private onAudioOperationEnd(
|
|
1202
|
+
event:
|
|
1203
|
+
| GenerateSpeechEndEvent
|
|
1204
|
+
| TranscriptionEndEvent
|
|
1205
|
+
| StreamTranscriptionEndEvent,
|
|
1206
|
+
): void {
|
|
1207
|
+
const state = this.getCallState(event.callId);
|
|
1208
|
+
if (!state?.rootSpan) return;
|
|
1209
|
+
|
|
1210
|
+
const outputAudio =
|
|
1211
|
+
event.operationId === 'ai.generateSpeech' ? event.audio : undefined;
|
|
1212
|
+
const inputAudio =
|
|
1213
|
+
event.operationId === 'ai.generateSpeech' ? undefined : event.audio;
|
|
1214
|
+
const transcript = 'segments' in event ? event.text : undefined;
|
|
1215
|
+
|
|
1216
|
+
state.rootSpan.setAttributes(
|
|
1217
|
+
selectAttributes(state.telemetry, {
|
|
1218
|
+
'ai.request.audio.size':
|
|
1219
|
+
inputAudio == null
|
|
1220
|
+
? undefined
|
|
1221
|
+
: { input: () => inputAudio.byteLength },
|
|
1222
|
+
'ai.request.audio.media_type':
|
|
1223
|
+
inputAudio == null
|
|
1224
|
+
? undefined
|
|
1225
|
+
: { input: () => inputAudio.mediaType },
|
|
1226
|
+
'ai.response.text':
|
|
1227
|
+
transcript == null ? undefined : { output: () => transcript },
|
|
1228
|
+
'gen_ai.output.messages':
|
|
1229
|
+
transcript == null
|
|
1230
|
+
? undefined
|
|
1231
|
+
: {
|
|
1232
|
+
output: () =>
|
|
1233
|
+
JSON.stringify([
|
|
1234
|
+
{
|
|
1235
|
+
role: 'assistant',
|
|
1236
|
+
parts: [{ type: 'text', content: transcript }],
|
|
1237
|
+
},
|
|
1238
|
+
]),
|
|
1239
|
+
},
|
|
1240
|
+
'ai.response.audio.size':
|
|
1241
|
+
outputAudio == null
|
|
1242
|
+
? undefined
|
|
1243
|
+
: { output: () => outputAudio.byteLength },
|
|
1244
|
+
'ai.response.audio.media_type':
|
|
1245
|
+
outputAudio == null
|
|
1246
|
+
? undefined
|
|
1247
|
+
: { output: () => outputAudio.mediaType },
|
|
1248
|
+
'ai.response.audio.format':
|
|
1249
|
+
outputAudio == null || !('format' in outputAudio)
|
|
1250
|
+
? undefined
|
|
1251
|
+
: { output: () => String(outputAudio.format) },
|
|
1252
|
+
...getProviderUsageAttributes({
|
|
1253
|
+
usage: event.usage,
|
|
1254
|
+
prefix: 'gen_ai.usage',
|
|
1255
|
+
}),
|
|
1256
|
+
...selectSupplementalAttributes(
|
|
1257
|
+
state.telemetry,
|
|
1258
|
+
this.supplementalAttributes,
|
|
1259
|
+
{
|
|
1260
|
+
usage: {
|
|
1261
|
+
'ai.response.usage':
|
|
1262
|
+
event.usage == null ? undefined : JSON.stringify(event.usage),
|
|
1263
|
+
},
|
|
1264
|
+
providerMetadata: {
|
|
1265
|
+
'ai.response.providerMetadata':
|
|
1266
|
+
event.providerMetadata == null
|
|
1267
|
+
? undefined
|
|
1268
|
+
: JSON.stringify(event.providerMetadata),
|
|
1269
|
+
},
|
|
1270
|
+
},
|
|
1271
|
+
),
|
|
1272
|
+
}),
|
|
1273
|
+
);
|
|
1274
|
+
|
|
1275
|
+
state.rootSpan.end();
|
|
1276
|
+
this.cleanupCallState(event.callId);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1064
1279
|
private onGenerateEnd(event: GenerateTextEndEvent<ToolSet>): void {
|
|
1065
1280
|
const state = this.getCallState(event.callId);
|
|
1066
1281
|
if (!state?.rootSpan) return;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Attributes } from '@opentelemetry/api';
|
|
2
|
+
import type { JSONObject, JSONValue } from '@ai-sdk/provider';
|
|
3
|
+
|
|
4
|
+
const usageKeyAliases: Record<string, string> = {
|
|
5
|
+
inputtokens: 'input_tokens',
|
|
6
|
+
prompttokens: 'input_tokens',
|
|
7
|
+
prompttokencount: 'input_tokens',
|
|
8
|
+
totalinputtokens: 'input_tokens',
|
|
9
|
+
outputtokens: 'output_tokens',
|
|
10
|
+
completiontokens: 'output_tokens',
|
|
11
|
+
completiontokencount: 'output_tokens',
|
|
12
|
+
candidatestokencount: 'output_tokens',
|
|
13
|
+
totaltokens: 'total_tokens',
|
|
14
|
+
totaltokencount: 'total_tokens',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function normalizeUsageKey(key: string): string {
|
|
18
|
+
const aliasKey = key.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
|
|
19
|
+
const alias = usageKeyAliases[aliasKey];
|
|
20
|
+
|
|
21
|
+
if (alias != null) {
|
|
22
|
+
return alias;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return key
|
|
26
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
27
|
+
.replace(/[^a-zA-Z0-9]+/g, '_')
|
|
28
|
+
.replace(/^_+|_+$/g, '')
|
|
29
|
+
.toLowerCase();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Converts provider-native usage objects into numeric span attributes.
|
|
34
|
+
*
|
|
35
|
+
* Usage units such as tokens, characters, and seconds remain distinct so
|
|
36
|
+
* observability backends can apply the provider/model-specific price unit.
|
|
37
|
+
*/
|
|
38
|
+
export function getProviderUsageAttributes({
|
|
39
|
+
usage,
|
|
40
|
+
prefix,
|
|
41
|
+
}: {
|
|
42
|
+
usage: JSONObject | undefined;
|
|
43
|
+
prefix: string;
|
|
44
|
+
}): Attributes {
|
|
45
|
+
const attributes: Attributes = {};
|
|
46
|
+
|
|
47
|
+
const addValue = (value: JSONValue | undefined, path: string[]) => {
|
|
48
|
+
if (typeof value === 'number') {
|
|
49
|
+
if (Number.isFinite(value) && path.length > 0) {
|
|
50
|
+
attributes[`${prefix}.${path.join('.')}`] = value;
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
60
|
+
const normalizedKey = normalizeUsageKey(key);
|
|
61
|
+
if (normalizedKey !== '') {
|
|
62
|
+
addValue(nestedValue, [...path, normalizedKey]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
addValue(usage, []);
|
|
68
|
+
return attributes;
|
|
69
|
+
}
|