@ai-sdk/otel 1.0.159 → 1.0.160

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/otel",
3
- "version": "1.0.159",
3
+ "version": "1.0.160",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -28,10 +28,11 @@
28
28
  "dependencies": {
29
29
  "@ai-sdk/provider": "4.0.35",
30
30
  "@opentelemetry/api": "1.9.1",
31
- "ai": "7.0.159"
31
+ "ai": "7.0.160"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@ai-sdk/provider-utils": "5.0.72",
35
+ "@opentelemetry/sdk-metrics": "2.8.0",
35
36
  "@opentelemetry/sdk-trace-base": "2.7.1",
36
37
  "@types/node": "22.19.19",
37
38
  "@vercel/ai-tsconfig": "0.0.0",
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  context,
3
+ metrics,
3
4
  SpanKind,
4
5
  SpanStatusCode,
5
6
  trace,
@@ -91,6 +92,11 @@ interface CallState {
91
92
  stepContext: OpenTelemetryContext | undefined;
92
93
  inferenceSpan: Span | undefined;
93
94
  inferenceContext: OpenTelemetryContext | undefined;
95
+ inferenceMetrics?: {
96
+ startTime: number;
97
+ provider: string;
98
+ modelId: string;
99
+ };
94
100
  inferenceToolDefinitions?: ReadonlyArray<Record<string, unknown>>;
95
101
  embedSpans: Map<string, { span: Span; context: OpenTelemetryContext }>;
96
102
  rerankSpan: { span: Span; context: OpenTelemetryContext } | undefined;
@@ -145,6 +151,52 @@ export class OpenTelemetry implements Telemetry {
145
151
  this.callStates.delete(callId);
146
152
  }
147
153
 
154
+ private recordInferenceDuration(
155
+ state: CallState,
156
+ {
157
+ responseTimeMs,
158
+ provider,
159
+ responseModelId,
160
+ errorType,
161
+ }: {
162
+ responseTimeMs?: number;
163
+ provider?: string;
164
+ responseModelId?: string;
165
+ errorType?: string;
166
+ } = {},
167
+ ): void {
168
+ const inference = state.inferenceMetrics;
169
+ if (inference == null) return;
170
+ state.inferenceMetrics = undefined;
171
+
172
+ // Resolve the meter when recording so a provider registered after this
173
+ // integration is constructed can still receive metrics.
174
+ const histogram = metrics
175
+ .getMeter('gen_ai')
176
+ .createHistogram('gen_ai.client.inference.duration', {
177
+ description: 'GenAI client inference operation duration.',
178
+ unit: 's',
179
+ advice: {
180
+ explicitBucketBoundaries: [
181
+ 0.01, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64, 1.28, 2.56, 5.12, 10.24,
182
+ 20.48, 40.96, 81.92,
183
+ ],
184
+ },
185
+ });
186
+
187
+ histogram.record(
188
+ (responseTimeMs ?? performance.now() - inference.startTime) / 1000,
189
+ selectAttributes(state.telemetry, {
190
+ 'gen_ai.operation.name': 'chat',
191
+ 'gen_ai.provider.name': mapProviderName(provider ?? inference.provider),
192
+ 'gen_ai.request.model': inference.modelId,
193
+ 'gen_ai.response.model': responseModelId,
194
+ 'error.type': errorType,
195
+ }),
196
+ state.inferenceContext,
197
+ );
198
+ }
199
+
148
200
  private getAgentSpanName({
149
201
  operationName,
150
202
  modelId,
@@ -859,9 +911,17 @@ export class OpenTelemetry implements Telemetry {
859
911
  const state = this.getCallState(event.callId);
860
912
  if (!state?.stepContext) return;
861
913
 
914
+ // A retried stream may start a new attempt without ending the failed one.
915
+ this.recordInferenceDuration(state, { errorType: '_OTHER' });
916
+
862
917
  const { telemetry } = state;
863
918
  const providerName = mapProviderName(event.provider);
864
919
  state.inferenceToolDefinitions = event.tools;
920
+ state.inferenceMetrics = {
921
+ startTime: performance.now(),
922
+ provider: event.provider,
923
+ modelId: event.modelId,
924
+ };
865
925
 
866
926
  const inferenceAttributes = selectAttributes(telemetry, {
867
927
  'gen_ai.operation.name': 'chat',
@@ -932,6 +992,13 @@ export class OpenTelemetry implements Telemetry {
932
992
  const state = this.getCallState(event.callId);
933
993
  if (!state?.inferenceSpan) return;
934
994
 
995
+ this.recordInferenceDuration(state, {
996
+ responseTimeMs: event.performance.responseTimeMs,
997
+ provider: event.provider,
998
+ responseModelId: event.modelId,
999
+ errorType: event.finishReason === 'error' ? '_OTHER' : undefined,
1000
+ });
1001
+
935
1002
  if (event.finishReason === 'error') {
936
1003
  state.inferenceSpan.setStatus({ code: SpanStatusCode.ERROR });
937
1004
  }
@@ -1175,6 +1242,11 @@ export class OpenTelemetry implements Telemetry {
1175
1242
  const state = this.getCallState(event.callId);
1176
1243
  if (!state?.stepSpan) return;
1177
1244
 
1245
+ // Streams can close after an error chunk without a model-call end event.
1246
+ this.recordInferenceDuration(state, {
1247
+ errorType: event.finishReason === 'error' ? '_OTHER' : undefined,
1248
+ });
1249
+
1178
1250
  if (event.finishReason === 'error') {
1179
1251
  state.stepSpan.setStatus({ code: SpanStatusCode.ERROR });
1180
1252
  }
@@ -1206,6 +1278,10 @@ export class OpenTelemetry implements Telemetry {
1206
1278
  const state = this.getCallState(event.callId);
1207
1279
  if (!state?.stepSpan) return;
1208
1280
 
1281
+ this.recordInferenceDuration(state, {
1282
+ errorType: event.error instanceof Error ? event.error.name : '_OTHER',
1283
+ });
1284
+
1209
1285
  if (state.inferenceSpan) {
1210
1286
  recordErrorOnSpan(state.inferenceSpan, event.error);
1211
1287
  state.inferenceSpan.end();
@@ -1907,6 +1983,8 @@ export class OpenTelemetry implements Telemetry {
1907
1983
  const state = this.getCallState(event.callId);
1908
1984
  if (!state?.rootSpan) return;
1909
1985
 
1986
+ this.recordInferenceDuration(state, { errorType: 'AbortError' });
1987
+
1910
1988
  for (const { span: toolSpan } of state.toolSpans.values()) {
1911
1989
  toolSpan.end();
1912
1990
  }
@@ -1952,6 +2030,10 @@ export class OpenTelemetry implements Telemetry {
1952
2030
 
1953
2031
  const actualError = event.error ?? error;
1954
2032
 
2033
+ this.recordInferenceDuration(state, {
2034
+ errorType: actualError instanceof Error ? actualError.name : '_OTHER',
2035
+ });
2036
+
1955
2037
  for (const { span: toolSpan } of state.toolSpans.values()) {
1956
2038
  recordErrorOnSpan(toolSpan, actualError);
1957
2039
  toolSpan.end();