@ai-sdk/otel 1.0.0-canary.98 → 1.0.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/CHANGELOG.md +313 -0
- package/dist/index.d.ts +26 -4
- package/dist/index.js +209 -42
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
- package/src/get-base-telemetry-attributes.ts +2 -6
- package/src/legacy-open-telemetry.ts +87 -25
- package/src/open-telemetry.ts +101 -11
- package/src/sanitize-attribute-value.ts +53 -0
- package/src/select-attributes.ts +11 -3
- package/src/supplemental-attributes.ts +35 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/otel",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -27,15 +27,16 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@opentelemetry/api": "1.9.1",
|
|
30
|
-
"
|
|
31
|
-
"ai": "
|
|
30
|
+
"ai": "7.0.0",
|
|
31
|
+
"@ai-sdk/provider": "4.0.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
+
"@opentelemetry/sdk-trace-base": "2.7.1",
|
|
34
35
|
"@types/node": "22.19.19",
|
|
35
36
|
"tsup": "^8.5.1",
|
|
36
37
|
"typescript": "5.8.3",
|
|
37
38
|
"zod": "3.25.76",
|
|
38
|
-
"@ai-sdk/provider-utils": "5.0.0
|
|
39
|
+
"@ai-sdk/provider-utils": "5.0.0",
|
|
39
40
|
"@vercel/ai-tsconfig": "0.0.0"
|
|
40
41
|
},
|
|
41
42
|
"engines": {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Attributes, AttributeValue } from '@opentelemetry/api';
|
|
2
2
|
import type { LanguageModelCallOptions } from 'ai';
|
|
3
|
+
import { getRuntimeContextAttributes } from './supplemental-attributes';
|
|
3
4
|
|
|
4
5
|
export function getBaseTelemetryAttributes({
|
|
5
6
|
model,
|
|
@@ -23,12 +24,7 @@ export function getBaseTelemetryAttributes({
|
|
|
23
24
|
}, {} as Attributes),
|
|
24
25
|
|
|
25
26
|
// add context as attributes:
|
|
26
|
-
...
|
|
27
|
-
if (value != undefined) {
|
|
28
|
-
attributes[`ai.settings.context.${key}`] = value as AttributeValue;
|
|
29
|
-
}
|
|
30
|
-
return attributes;
|
|
31
|
-
}, {} as Attributes),
|
|
27
|
+
...(getRuntimeContextAttributes(context) as Attributes),
|
|
32
28
|
|
|
33
29
|
// request headers
|
|
34
30
|
...Object.entries(headers ?? {}).reduce((attributes, [key, value]) => {
|
|
@@ -19,13 +19,14 @@ import type {
|
|
|
19
19
|
GenerateObjectStartEvent,
|
|
20
20
|
GenerateObjectStepEndEvent,
|
|
21
21
|
GenerateObjectStepStartEvent,
|
|
22
|
+
GenerateTextAbortEvent,
|
|
22
23
|
GenerateTextEndEvent,
|
|
23
24
|
GenerateTextStartEvent,
|
|
24
25
|
GenerateTextStepEndEvent,
|
|
25
26
|
GenerateTextStepStartEvent,
|
|
26
27
|
ToolExecutionEndEvent,
|
|
27
28
|
ToolExecutionStartEvent,
|
|
28
|
-
|
|
29
|
+
Output,
|
|
29
30
|
RerankingModelCallEndEvent,
|
|
30
31
|
RerankEndEvent,
|
|
31
32
|
RerankStartEvent,
|
|
@@ -37,6 +38,7 @@ import type {
|
|
|
37
38
|
} from 'ai';
|
|
38
39
|
import { assembleOperationName } from './assemble-operation-name';
|
|
39
40
|
import { getBaseTelemetryAttributes } from './get-base-telemetry-attributes';
|
|
41
|
+
import { sanitizeAttributeValue } from './sanitize-attribute-value';
|
|
40
42
|
import { stringifyForTelemetry } from './stringify-for-telemetry';
|
|
41
43
|
|
|
42
44
|
function recordSpanError(span: Span, error: unknown): void {
|
|
@@ -87,7 +89,10 @@ function selectAttributes(
|
|
|
87
89
|
) {
|
|
88
90
|
if (telemetry?.recordInputs === false) continue;
|
|
89
91
|
const resolved = value.input();
|
|
90
|
-
if (resolved != null)
|
|
92
|
+
if (resolved != null) {
|
|
93
|
+
const sanitized = sanitizeAttributeValue(resolved);
|
|
94
|
+
if (sanitized != null) result[key] = sanitized;
|
|
95
|
+
}
|
|
91
96
|
continue;
|
|
92
97
|
}
|
|
93
98
|
|
|
@@ -98,11 +103,15 @@ function selectAttributes(
|
|
|
98
103
|
) {
|
|
99
104
|
if (telemetry?.recordOutputs === false) continue;
|
|
100
105
|
const resolved = value.output();
|
|
101
|
-
if (resolved != null)
|
|
106
|
+
if (resolved != null) {
|
|
107
|
+
const sanitized = sanitizeAttributeValue(resolved);
|
|
108
|
+
if (sanitized != null) result[key] = sanitized;
|
|
109
|
+
}
|
|
102
110
|
continue;
|
|
103
111
|
}
|
|
104
112
|
|
|
105
|
-
|
|
113
|
+
const sanitized = sanitizeAttributeValue(value as AttributeValue);
|
|
114
|
+
if (sanitized != null) result[key] = sanitized;
|
|
106
115
|
}
|
|
107
116
|
|
|
108
117
|
return result;
|
|
@@ -111,7 +120,7 @@ function selectAttributes(
|
|
|
111
120
|
interface OtelStepStartEvent<
|
|
112
121
|
TOOLS extends ToolSet = ToolSet,
|
|
113
122
|
RUNTIME_CONTEXT extends AISDKContext = AISDKContext,
|
|
114
|
-
OUTPUT extends Output = Output,
|
|
123
|
+
OUTPUT extends Output.Output = Output.Output,
|
|
115
124
|
> extends GenerateTextStepStartEvent<TOOLS, RUNTIME_CONTEXT, OUTPUT> {
|
|
116
125
|
readonly promptMessages?: LanguageModelV4Prompt;
|
|
117
126
|
readonly stepTools?: ReadonlyArray<Record<string, unknown>>;
|
|
@@ -174,6 +183,26 @@ export class LegacyOpenTelemetry implements Telemetry {
|
|
|
174
183
|
return context.with(toolSpanEntry.context, execute);
|
|
175
184
|
}
|
|
176
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Runs the provider `doGenerate`/`doStream` call with the active legacy
|
|
188
|
+
* model-call context.
|
|
189
|
+
*/
|
|
190
|
+
executeLanguageModelCall<T>({
|
|
191
|
+
callId,
|
|
192
|
+
execute,
|
|
193
|
+
}: {
|
|
194
|
+
callId: string;
|
|
195
|
+
execute: () => PromiseLike<T>;
|
|
196
|
+
}): PromiseLike<T> {
|
|
197
|
+
const stepContext = this.getCallState(callId)?.stepContext;
|
|
198
|
+
|
|
199
|
+
if (stepContext == null) {
|
|
200
|
+
return execute();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return context.with(stepContext, execute);
|
|
204
|
+
}
|
|
205
|
+
|
|
177
206
|
onStart(
|
|
178
207
|
event:
|
|
179
208
|
| InferTelemetryEvent<GenerateTextStartEvent>
|
|
@@ -391,7 +420,7 @@ export class LegacyOpenTelemetry implements Telemetry {
|
|
|
391
420
|
}
|
|
392
421
|
|
|
393
422
|
/** @deprecated */
|
|
394
|
-
|
|
423
|
+
onObjectStepEnd(event: GenerateObjectStepEndEvent): void {
|
|
395
424
|
const state = this.getCallState(event.callId);
|
|
396
425
|
if (!state?.stepSpan) return;
|
|
397
426
|
|
|
@@ -634,7 +663,7 @@ export class LegacyOpenTelemetry implements Telemetry {
|
|
|
634
663
|
state.toolSpans.delete(event.toolCall.toolCallId);
|
|
635
664
|
}
|
|
636
665
|
|
|
637
|
-
|
|
666
|
+
onStepEnd(event: GenerateTextStepEndEvent<ToolSet>): void {
|
|
638
667
|
const state = this.getCallState(event.callId);
|
|
639
668
|
if (!state?.stepSpan) return;
|
|
640
669
|
|
|
@@ -687,7 +716,7 @@ export class LegacyOpenTelemetry implements Telemetry {
|
|
|
687
716
|
? JSON.stringify(event.providerMetadata)
|
|
688
717
|
: undefined,
|
|
689
718
|
'ai.response.msToFirstChunk': isStreamText
|
|
690
|
-
? event.performance.
|
|
719
|
+
? event.performance.timeToFirstOutputMs
|
|
691
720
|
: undefined,
|
|
692
721
|
'ai.response.msToFinish': isStreamText
|
|
693
722
|
? event.performance.responseTimeMs
|
|
@@ -722,10 +751,9 @@ export class LegacyOpenTelemetry implements Telemetry {
|
|
|
722
751
|
}),
|
|
723
752
|
);
|
|
724
753
|
|
|
725
|
-
if (isStreamText && event.performance.
|
|
754
|
+
if (isStreamText && event.performance.timeToFirstOutputMs != null) {
|
|
726
755
|
state.stepSpan.addEvent('ai.stream.firstChunk', {
|
|
727
|
-
'ai.response.msToFirstChunk':
|
|
728
|
-
event.performance.timeToFirstOutputTokenMs,
|
|
756
|
+
'ai.response.msToFirstChunk': event.performance.timeToFirstOutputMs,
|
|
729
757
|
});
|
|
730
758
|
}
|
|
731
759
|
|
|
@@ -742,6 +770,11 @@ export class LegacyOpenTelemetry implements Telemetry {
|
|
|
742
770
|
state.stepContext = undefined;
|
|
743
771
|
}
|
|
744
772
|
|
|
773
|
+
/** @deprecated Use `onStepEnd` instead. */
|
|
774
|
+
onStepFinish(event: GenerateTextStepEndEvent<ToolSet>): void {
|
|
775
|
+
this.onStepEnd(event);
|
|
776
|
+
}
|
|
777
|
+
|
|
745
778
|
onEnd(
|
|
746
779
|
event:
|
|
747
780
|
| GenerateTextEndEvent<ToolSet>
|
|
@@ -790,8 +823,8 @@ export class LegacyOpenTelemetry implements Telemetry {
|
|
|
790
823
|
},
|
|
791
824
|
'ai.response.reasoning': {
|
|
792
825
|
output: () =>
|
|
793
|
-
event.reasoning.length > 0
|
|
794
|
-
? event.reasoning
|
|
826
|
+
event.finalStep.reasoning.length > 0
|
|
827
|
+
? event.finalStep.reasoning
|
|
795
828
|
.filter(part => 'text' in part)
|
|
796
829
|
.map(part => part.text)
|
|
797
830
|
.join('\n')
|
|
@@ -821,27 +854,27 @@ export class LegacyOpenTelemetry implements Telemetry {
|
|
|
821
854
|
)
|
|
822
855
|
: undefined,
|
|
823
856
|
},
|
|
824
|
-
'ai.response.providerMetadata': event.providerMetadata
|
|
825
|
-
? JSON.stringify(event.providerMetadata)
|
|
857
|
+
'ai.response.providerMetadata': event.finalStep.providerMetadata
|
|
858
|
+
? JSON.stringify(event.finalStep.providerMetadata)
|
|
826
859
|
: undefined,
|
|
827
860
|
|
|
828
|
-
'ai.usage.inputTokens': event.
|
|
829
|
-
'ai.usage.outputTokens': event.
|
|
830
|
-
'ai.usage.totalTokens': event.
|
|
861
|
+
'ai.usage.inputTokens': event.usage.inputTokens,
|
|
862
|
+
'ai.usage.outputTokens': event.usage.outputTokens,
|
|
863
|
+
'ai.usage.totalTokens': event.usage.totalTokens,
|
|
831
864
|
'ai.usage.reasoningTokens':
|
|
832
|
-
event.
|
|
865
|
+
event.usage.outputTokenDetails?.reasoningTokens,
|
|
833
866
|
'ai.usage.cachedInputTokens':
|
|
834
|
-
event.
|
|
867
|
+
event.usage.inputTokenDetails?.cacheReadTokens,
|
|
835
868
|
'ai.usage.inputTokenDetails.noCacheTokens':
|
|
836
|
-
event.
|
|
869
|
+
event.usage.inputTokenDetails?.noCacheTokens,
|
|
837
870
|
'ai.usage.inputTokenDetails.cacheReadTokens':
|
|
838
|
-
event.
|
|
871
|
+
event.usage.inputTokenDetails?.cacheReadTokens,
|
|
839
872
|
'ai.usage.inputTokenDetails.cacheWriteTokens':
|
|
840
|
-
event.
|
|
873
|
+
event.usage.inputTokenDetails?.cacheWriteTokens,
|
|
841
874
|
'ai.usage.outputTokenDetails.textTokens':
|
|
842
|
-
event.
|
|
875
|
+
event.usage.outputTokenDetails?.textTokens,
|
|
843
876
|
'ai.usage.outputTokenDetails.reasoningTokens':
|
|
844
|
-
event.
|
|
877
|
+
event.usage.outputTokenDetails?.reasoningTokens,
|
|
845
878
|
}),
|
|
846
879
|
);
|
|
847
880
|
|
|
@@ -1067,6 +1100,35 @@ export class LegacyOpenTelemetry implements Telemetry {
|
|
|
1067
1100
|
state.rerankSpan = undefined;
|
|
1068
1101
|
}
|
|
1069
1102
|
|
|
1103
|
+
onAbort(event: GenerateTextAbortEvent<ToolSet>): void {
|
|
1104
|
+
const state = this.getCallState(event.callId);
|
|
1105
|
+
if (!state?.rootSpan) return;
|
|
1106
|
+
|
|
1107
|
+
for (const { span: toolSpan } of state.toolSpans.values()) {
|
|
1108
|
+
toolSpan.end();
|
|
1109
|
+
}
|
|
1110
|
+
state.toolSpans.clear();
|
|
1111
|
+
|
|
1112
|
+
if (state.stepSpan) {
|
|
1113
|
+
state.stepSpan.end();
|
|
1114
|
+
state.stepSpan = undefined;
|
|
1115
|
+
state.stepContext = undefined;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
for (const { span: embedSpan } of state.embedSpans.values()) {
|
|
1119
|
+
embedSpan.end();
|
|
1120
|
+
}
|
|
1121
|
+
state.embedSpans.clear();
|
|
1122
|
+
|
|
1123
|
+
if (state.rerankSpan) {
|
|
1124
|
+
state.rerankSpan.span.end();
|
|
1125
|
+
state.rerankSpan = undefined;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
state.rootSpan.end();
|
|
1129
|
+
this.cleanupCallState(event.callId);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1070
1132
|
onError(error: unknown): void {
|
|
1071
1133
|
const event = error as { callId?: string; error?: unknown };
|
|
1072
1134
|
if (!event?.callId) return;
|
package/src/open-telemetry.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
GenerateObjectStartEvent,
|
|
19
19
|
GenerateObjectStepEndEvent,
|
|
20
20
|
GenerateObjectStepStartEvent,
|
|
21
|
+
GenerateTextAbortEvent,
|
|
21
22
|
GenerateTextEndEvent,
|
|
22
23
|
GenerateTextStartEvent,
|
|
23
24
|
GenerateTextStepEndEvent,
|
|
@@ -43,6 +44,7 @@ import {
|
|
|
43
44
|
mapProviderName,
|
|
44
45
|
} from './gen-ai-format-messages';
|
|
45
46
|
import { recordErrorOnSpan } from './record-span';
|
|
47
|
+
import { sanitizeAttributes } from './sanitize-attribute-value';
|
|
46
48
|
import { selectAttributes } from './select-attributes';
|
|
47
49
|
import {
|
|
48
50
|
getDetailedUsageAttributes,
|
|
@@ -85,6 +87,24 @@ interface CallState {
|
|
|
85
87
|
baseSupplementalAttributes: Attributes;
|
|
86
88
|
}
|
|
87
89
|
|
|
90
|
+
function msToSeconds(durationMs: number | undefined): number | undefined {
|
|
91
|
+
return durationMs == null ? undefined : durationMs / 1000;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getGenAIClientPerformanceAttributes(
|
|
95
|
+
performance: LanguageModelCallEndEvent<ToolSet>['performance'],
|
|
96
|
+
): Attributes {
|
|
97
|
+
return {
|
|
98
|
+
'gen_ai.client.operation.duration': msToSeconds(performance.responseTimeMs),
|
|
99
|
+
'gen_ai.client.operation.time_to_first_chunk': msToSeconds(
|
|
100
|
+
performance.timeToFirstOutputMs,
|
|
101
|
+
),
|
|
102
|
+
'gen_ai.client.operation.time_per_output_chunk': msToSeconds(
|
|
103
|
+
performance.timeBetweenOutputChunksMs?.avg,
|
|
104
|
+
),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
88
108
|
export class OpenTelemetry implements Telemetry {
|
|
89
109
|
private readonly callStates = new Map<string, CallState>();
|
|
90
110
|
|
|
@@ -133,7 +153,7 @@ export class OpenTelemetry implements Telemetry {
|
|
|
133
153
|
}
|
|
134
154
|
|
|
135
155
|
return {
|
|
136
|
-
...customAttributes,
|
|
156
|
+
...sanitizeAttributes(customAttributes),
|
|
137
157
|
...attributes,
|
|
138
158
|
};
|
|
139
159
|
}
|
|
@@ -156,6 +176,27 @@ export class OpenTelemetry implements Telemetry {
|
|
|
156
176
|
return context.with(toolSpanEntry.context, execute);
|
|
157
177
|
}
|
|
158
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Runs the provider `doGenerate`/`doStream` call with the active model-call
|
|
181
|
+
* context.
|
|
182
|
+
*/
|
|
183
|
+
executeLanguageModelCall<T>({
|
|
184
|
+
callId,
|
|
185
|
+
execute,
|
|
186
|
+
}: {
|
|
187
|
+
callId: string;
|
|
188
|
+
execute: () => PromiseLike<T>;
|
|
189
|
+
}): PromiseLike<T> {
|
|
190
|
+
const state = this.getCallState(callId);
|
|
191
|
+
const modelCallContext = state?.inferenceContext ?? state?.stepContext;
|
|
192
|
+
|
|
193
|
+
if (modelCallContext == null) {
|
|
194
|
+
return execute();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return context.with(modelCallContext, execute);
|
|
198
|
+
}
|
|
199
|
+
|
|
159
200
|
onStart(
|
|
160
201
|
event:
|
|
161
202
|
| InferTelemetryEvent<GenerateTextStartEvent>
|
|
@@ -456,7 +497,7 @@ export class OpenTelemetry implements Telemetry {
|
|
|
456
497
|
}
|
|
457
498
|
|
|
458
499
|
/** @deprecated */
|
|
459
|
-
|
|
500
|
+
onObjectStepEnd(event: GenerateObjectStepEndEvent): void {
|
|
460
501
|
const state = this.getCallState(event.callId);
|
|
461
502
|
if (!state?.inferenceSpan) return;
|
|
462
503
|
|
|
@@ -691,6 +732,7 @@ export class OpenTelemetry implements Telemetry {
|
|
|
691
732
|
|
|
692
733
|
state.inferenceSpan.setAttributes(
|
|
693
734
|
selectAttributes(telemetry, {
|
|
735
|
+
...getGenAIClientPerformanceAttributes(event.performance),
|
|
694
736
|
'gen_ai.response.finish_reasons': [event.finishReason],
|
|
695
737
|
'gen_ai.response.id': event.responseId,
|
|
696
738
|
'gen_ai.usage.input_tokens': event.usage.inputTokens,
|
|
@@ -783,6 +825,12 @@ export class OpenTelemetry implements Telemetry {
|
|
|
783
825
|
const { telemetry } = state;
|
|
784
826
|
|
|
785
827
|
const { toolOutput } = event;
|
|
828
|
+
span.setAttributes(
|
|
829
|
+
selectAttributes(telemetry, {
|
|
830
|
+
'gen_ai.execute_tool.duration': msToSeconds(event.toolExecutionMs),
|
|
831
|
+
}),
|
|
832
|
+
);
|
|
833
|
+
|
|
786
834
|
if (toolOutput.type === 'tool-result') {
|
|
787
835
|
try {
|
|
788
836
|
span.setAttributes(
|
|
@@ -803,7 +851,7 @@ export class OpenTelemetry implements Telemetry {
|
|
|
803
851
|
state.toolSpans.delete(event.toolCall.toolCallId);
|
|
804
852
|
}
|
|
805
853
|
|
|
806
|
-
|
|
854
|
+
onStepEnd(event: GenerateTextStepEndEvent<ToolSet>): void {
|
|
807
855
|
const state = this.getCallState(event.callId);
|
|
808
856
|
if (!state?.stepSpan) return;
|
|
809
857
|
|
|
@@ -825,6 +873,11 @@ export class OpenTelemetry implements Telemetry {
|
|
|
825
873
|
state.stepContext = undefined;
|
|
826
874
|
}
|
|
827
875
|
|
|
876
|
+
/** @deprecated Use `onStepEnd` instead. */
|
|
877
|
+
onStepFinish(event: GenerateTextStepEndEvent<ToolSet>): void {
|
|
878
|
+
this.onStepEnd(event);
|
|
879
|
+
}
|
|
880
|
+
|
|
828
881
|
onEnd(
|
|
829
882
|
event:
|
|
830
883
|
| GenerateTextEndEvent<ToolSet>
|
|
@@ -868,18 +921,20 @@ export class OpenTelemetry implements Telemetry {
|
|
|
868
921
|
state.rootSpan.setAttributes(
|
|
869
922
|
selectAttributes(telemetry, {
|
|
870
923
|
'gen_ai.response.finish_reasons': [event.finishReason],
|
|
871
|
-
'gen_ai.usage.input_tokens': event.
|
|
872
|
-
'gen_ai.usage.output_tokens': event.
|
|
924
|
+
'gen_ai.usage.input_tokens': event.usage.inputTokens,
|
|
925
|
+
'gen_ai.usage.output_tokens': event.usage.outputTokens,
|
|
873
926
|
'gen_ai.usage.cache_read.input_tokens':
|
|
874
|
-
event.
|
|
927
|
+
event.usage.inputTokenDetails?.cacheReadTokens,
|
|
875
928
|
'gen_ai.usage.cache_creation.input_tokens':
|
|
876
|
-
event.
|
|
929
|
+
event.usage.inputTokenDetails?.cacheWriteTokens,
|
|
877
930
|
'gen_ai.output.messages': {
|
|
878
931
|
output: () =>
|
|
879
932
|
JSON.stringify(
|
|
880
933
|
formatOutputMessages({
|
|
881
934
|
text: event.text ?? undefined,
|
|
882
|
-
reasoning: event.reasoning as ReadonlyArray<{
|
|
935
|
+
reasoning: event.finalStep.reasoning as ReadonlyArray<{
|
|
936
|
+
text?: string;
|
|
937
|
+
}>,
|
|
883
938
|
toolCalls: event.toolCalls,
|
|
884
939
|
files: event.files,
|
|
885
940
|
finishReason: event.finishReason,
|
|
@@ -891,11 +946,11 @@ export class OpenTelemetry implements Telemetry {
|
|
|
891
946
|
this.supplementalAttributes,
|
|
892
947
|
{
|
|
893
948
|
providerMetadata: {
|
|
894
|
-
'ai.response.providerMetadata': event.providerMetadata
|
|
895
|
-
? JSON.stringify(event.providerMetadata)
|
|
949
|
+
'ai.response.providerMetadata': event.finalStep.providerMetadata
|
|
950
|
+
? JSON.stringify(event.finalStep.providerMetadata)
|
|
896
951
|
: undefined,
|
|
897
952
|
},
|
|
898
|
-
usage: getDetailedUsageAttributes(event.
|
|
953
|
+
usage: getDetailedUsageAttributes(event.usage),
|
|
899
954
|
},
|
|
900
955
|
),
|
|
901
956
|
}),
|
|
@@ -1197,6 +1252,41 @@ export class OpenTelemetry implements Telemetry {
|
|
|
1197
1252
|
state.rerankSpan = undefined;
|
|
1198
1253
|
}
|
|
1199
1254
|
|
|
1255
|
+
onAbort(event: GenerateTextAbortEvent<ToolSet>): void {
|
|
1256
|
+
const state = this.getCallState(event.callId);
|
|
1257
|
+
if (!state?.rootSpan) return;
|
|
1258
|
+
|
|
1259
|
+
for (const { span: toolSpan } of state.toolSpans.values()) {
|
|
1260
|
+
toolSpan.end();
|
|
1261
|
+
}
|
|
1262
|
+
state.toolSpans.clear();
|
|
1263
|
+
|
|
1264
|
+
if (state.inferenceSpan) {
|
|
1265
|
+
state.inferenceSpan.end();
|
|
1266
|
+
state.inferenceSpan = undefined;
|
|
1267
|
+
state.inferenceContext = undefined;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
if (state.stepSpan) {
|
|
1271
|
+
state.stepSpan.end();
|
|
1272
|
+
state.stepSpan = undefined;
|
|
1273
|
+
state.stepContext = undefined;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
for (const { span: embedSpan } of state.embedSpans.values()) {
|
|
1277
|
+
embedSpan.end();
|
|
1278
|
+
}
|
|
1279
|
+
state.embedSpans.clear();
|
|
1280
|
+
|
|
1281
|
+
if (state.rerankSpan) {
|
|
1282
|
+
state.rerankSpan.span.end();
|
|
1283
|
+
state.rerankSpan = undefined;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
state.rootSpan.end();
|
|
1287
|
+
this.cleanupCallState(event.callId);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1200
1290
|
onError(error: unknown): void {
|
|
1201
1291
|
const event = error as { callId?: string; error?: unknown };
|
|
1202
1292
|
if (!event?.callId) return;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Attributes, AttributeValue } from '@opentelemetry/api';
|
|
2
|
+
|
|
3
|
+
function isPrimitiveAttributeValue(
|
|
4
|
+
value: unknown,
|
|
5
|
+
): value is string | number | boolean {
|
|
6
|
+
return (
|
|
7
|
+
typeof value === 'string' ||
|
|
8
|
+
typeof value === 'number' ||
|
|
9
|
+
typeof value === 'boolean'
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function sanitizeAttributeValue(
|
|
14
|
+
value: AttributeValue,
|
|
15
|
+
): AttributeValue | undefined {
|
|
16
|
+
if (!Array.isArray(value)) {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const primitiveTypes = new Set(
|
|
21
|
+
value.filter(isPrimitiveAttributeValue).map(item => typeof item),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
if (primitiveTypes.size !== 1) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const [primitiveType] = primitiveTypes;
|
|
29
|
+
|
|
30
|
+
if (primitiveType === 'string') {
|
|
31
|
+
return value.filter((item): item is string => typeof item === 'string');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (primitiveType === 'number') {
|
|
35
|
+
return value.filter((item): item is number => typeof item === 'number');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return value.filter((item): item is boolean => typeof item === 'boolean');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function sanitizeAttributes(
|
|
42
|
+
attributes: Attributes | undefined,
|
|
43
|
+
): Attributes {
|
|
44
|
+
const result: Attributes = {};
|
|
45
|
+
|
|
46
|
+
for (const [key, value] of Object.entries(attributes ?? {})) {
|
|
47
|
+
if (value == null) continue;
|
|
48
|
+
const sanitized = sanitizeAttributeValue(value);
|
|
49
|
+
if (sanitized != null) result[key] = sanitized;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return result;
|
|
53
|
+
}
|
package/src/select-attributes.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Attributes, AttributeValue } from '@opentelemetry/api';
|
|
2
2
|
import type { TelemetryOptions } from 'ai';
|
|
3
|
+
import { sanitizeAttributeValue } from './sanitize-attribute-value';
|
|
3
4
|
|
|
4
5
|
export type AttributeSpec =
|
|
5
6
|
| AttributeValue
|
|
@@ -35,7 +36,10 @@ export function selectAttributes(
|
|
|
35
36
|
) {
|
|
36
37
|
if (telemetry?.recordInputs === false) continue;
|
|
37
38
|
const resolved = value.input();
|
|
38
|
-
if (resolved != null)
|
|
39
|
+
if (resolved != null) {
|
|
40
|
+
const sanitized = sanitizeAttributeValue(resolved);
|
|
41
|
+
if (sanitized != null) result[key] = sanitized;
|
|
42
|
+
}
|
|
39
43
|
continue;
|
|
40
44
|
}
|
|
41
45
|
|
|
@@ -46,11 +50,15 @@ export function selectAttributes(
|
|
|
46
50
|
) {
|
|
47
51
|
if (telemetry?.recordOutputs === false) continue;
|
|
48
52
|
const resolved = value.output();
|
|
49
|
-
if (resolved != null)
|
|
53
|
+
if (resolved != null) {
|
|
54
|
+
const sanitized = sanitizeAttributeValue(resolved);
|
|
55
|
+
if (sanitized != null) result[key] = sanitized;
|
|
56
|
+
}
|
|
50
57
|
continue;
|
|
51
58
|
}
|
|
52
59
|
|
|
53
|
-
|
|
60
|
+
const sanitized = sanitizeAttributeValue(value as AttributeValue);
|
|
61
|
+
if (sanitized != null) result[key] = sanitized;
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
return result;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { Attributes, Tracer } from '@opentelemetry/api';
|
|
2
2
|
import type { TelemetryOptions } from 'ai';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
selectAttributes,
|
|
5
|
+
type AttributeSpec,
|
|
6
|
+
type AttributeSpecMap,
|
|
7
|
+
} from './select-attributes';
|
|
4
8
|
|
|
5
9
|
type SupplementalAttributeOption =
|
|
6
10
|
| 'usage'
|
|
@@ -115,11 +119,36 @@ export function normalizeSupplementalAttributes(
|
|
|
115
119
|
export function getRuntimeContextAttributes(
|
|
116
120
|
context: Record<string, unknown> | undefined,
|
|
117
121
|
): AttributeSpecMap {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
const attributes: AttributeSpecMap = {};
|
|
123
|
+
|
|
124
|
+
for (const [key, value] of Object.entries(context ?? {})) {
|
|
125
|
+
addRuntimeContextAttribute(attributes, `ai.settings.context.${key}`, value);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return attributes;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Flattens nested runtime context objects into OTel-compatible attribute keys.
|
|
133
|
+
* Arrays are preserved because OTel supports primitive array attribute values.
|
|
134
|
+
*/
|
|
135
|
+
function addRuntimeContextAttribute(
|
|
136
|
+
attributes: AttributeSpecMap,
|
|
137
|
+
key: string,
|
|
138
|
+
value: unknown,
|
|
139
|
+
): void {
|
|
140
|
+
if (value == null) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (Array.isArray(value) || typeof value !== 'object') {
|
|
145
|
+
attributes[key] = value as AttributeSpec;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const [nestedKey, nestedValue] of Object.entries(value)) {
|
|
150
|
+
addRuntimeContextAttribute(attributes, `${key}.${nestedKey}`, nestedValue);
|
|
151
|
+
}
|
|
123
152
|
}
|
|
124
153
|
|
|
125
154
|
export function getHeaderAttributes(
|