@telemetry-dev/tanstack-ai 0.1.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/LICENSE +21 -0
- package/README.md +95 -0
- package/dist/index.d.mts +40 -0
- package/dist/index.mjs +681 -0
- package/package.json +62 -0
- package/src/config.ts +48 -0
- package/src/index.ts +2 -0
- package/src/middleware.ts +659 -0
- package/src/otel.ts +247 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
import { ROOT_CONTEXT, SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
2
|
+
import { ProtobufMetricsSerializer, ProtobufTraceSerializer } from "@opentelemetry/otlp-transformer";
|
|
3
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
4
|
+
import { AggregationTemporality, MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
5
|
+
import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base";
|
|
6
|
+
//#region src/config.ts
|
|
7
|
+
const DEFAULT_BASE_URL = "https://ingest.telemetry.dev";
|
|
8
|
+
function resolveConfig(options = {}) {
|
|
9
|
+
const env = typeof process !== "undefined" && process.env ? process.env : {};
|
|
10
|
+
const baseUrl = (options.baseUrl ?? env.TELEMETRY_DEV_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
11
|
+
return {
|
|
12
|
+
apiKey: options.apiKey ?? env.TELEMETRY_DEV_API_KEY,
|
|
13
|
+
baseUrl,
|
|
14
|
+
environment: options.environment ?? env.TELEMETRY_DEV_ENVIRONMENT ?? "production",
|
|
15
|
+
serviceName: options.serviceName ?? env.OTEL_SERVICE_NAME ?? "unknown_service",
|
|
16
|
+
fetchImpl: options.fetch ?? globalThis.fetch,
|
|
17
|
+
waitUntil: options.waitUntil,
|
|
18
|
+
onError: options.onError
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/otel.ts
|
|
23
|
+
const DURATION_BUCKETS = [
|
|
24
|
+
.01,
|
|
25
|
+
.02,
|
|
26
|
+
.04,
|
|
27
|
+
.08,
|
|
28
|
+
.16,
|
|
29
|
+
.32,
|
|
30
|
+
.64,
|
|
31
|
+
1.28,
|
|
32
|
+
2.56,
|
|
33
|
+
5.12,
|
|
34
|
+
10.24,
|
|
35
|
+
20.48,
|
|
36
|
+
40.96,
|
|
37
|
+
81.92
|
|
38
|
+
];
|
|
39
|
+
const TOKEN_BUCKETS = [
|
|
40
|
+
1,
|
|
41
|
+
4,
|
|
42
|
+
16,
|
|
43
|
+
64,
|
|
44
|
+
256,
|
|
45
|
+
1024,
|
|
46
|
+
4096,
|
|
47
|
+
16384,
|
|
48
|
+
65536,
|
|
49
|
+
262144,
|
|
50
|
+
1048576,
|
|
51
|
+
4194304,
|
|
52
|
+
16777216,
|
|
53
|
+
67108864
|
|
54
|
+
];
|
|
55
|
+
const SCOPE_NAME = "@telemetry-dev/tanstack-ai";
|
|
56
|
+
const SCOPE_VERSION = "0.0.0";
|
|
57
|
+
const RETRY_DELAYS_MS = [100, 500];
|
|
58
|
+
const RETRYABLE_STATUSES = new Set([
|
|
59
|
+
429,
|
|
60
|
+
502,
|
|
61
|
+
503,
|
|
62
|
+
504
|
|
63
|
+
]);
|
|
64
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
65
|
+
const cancelBody = async (res) => {
|
|
66
|
+
try {
|
|
67
|
+
await res.body?.cancel();
|
|
68
|
+
} catch {}
|
|
69
|
+
};
|
|
70
|
+
const postOtlp = async ({ fetchImpl, url, headers, body }) => {
|
|
71
|
+
for (let attempt = 0;; attempt += 1) {
|
|
72
|
+
try {
|
|
73
|
+
const res = await fetchImpl(url, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers,
|
|
76
|
+
body
|
|
77
|
+
});
|
|
78
|
+
if (res.ok || !RETRYABLE_STATUSES.has(res.status) || attempt === RETRY_DELAYS_MS.length) {
|
|
79
|
+
await cancelBody(res);
|
|
80
|
+
return res;
|
|
81
|
+
}
|
|
82
|
+
await cancelBody(res);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (attempt === RETRY_DELAYS_MS.length) throw error;
|
|
85
|
+
}
|
|
86
|
+
await delay(RETRY_DELAYS_MS[attempt]);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const cache = /* @__PURE__ */ new Map();
|
|
90
|
+
const buildCore = (config) => {
|
|
91
|
+
const { apiKey, baseUrl, environment, serviceName } = config;
|
|
92
|
+
const resource = resourceFromAttributes({
|
|
93
|
+
"service.name": serviceName,
|
|
94
|
+
"deployment.environment.name": environment
|
|
95
|
+
});
|
|
96
|
+
const headers = {
|
|
97
|
+
"content-type": "application/x-protobuf",
|
|
98
|
+
authorization: `Bearer ${apiKey}`
|
|
99
|
+
};
|
|
100
|
+
const tracer = new BasicTracerProvider({ resource }).getTracer(SCOPE_NAME, SCOPE_VERSION);
|
|
101
|
+
const sendSpans = async (spans, transport) => {
|
|
102
|
+
const body = ProtobufTraceSerializer.serializeRequest(spans);
|
|
103
|
+
if (!body || body.byteLength === 0) return;
|
|
104
|
+
const { fetchImpl, onError } = transport;
|
|
105
|
+
const res = await postOtlp({
|
|
106
|
+
fetchImpl,
|
|
107
|
+
url: `${baseUrl}/v1/traces`,
|
|
108
|
+
headers,
|
|
109
|
+
body
|
|
110
|
+
});
|
|
111
|
+
if (!res.ok) onError?.(/* @__PURE__ */ new Error(`telemetry.dev trace ingest failed: ${res.status}`));
|
|
112
|
+
};
|
|
113
|
+
const buildMetrics = (transport) => {
|
|
114
|
+
const { fetchImpl, onError } = transport;
|
|
115
|
+
const meterProvider = new MeterProvider({
|
|
116
|
+
resource,
|
|
117
|
+
readers: [new PeriodicExportingMetricReader({
|
|
118
|
+
exporter: {
|
|
119
|
+
export(resourceMetrics, resultCallback) {
|
|
120
|
+
const body = ProtobufMetricsSerializer.serializeRequest(resourceMetrics);
|
|
121
|
+
if (!body || body.byteLength === 0) {
|
|
122
|
+
resultCallback({ code: 0 });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
fetchImpl(`${baseUrl}/v1/metrics`, {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers,
|
|
128
|
+
body
|
|
129
|
+
}).then((res) => {
|
|
130
|
+
if (!res.ok) {
|
|
131
|
+
const error = /* @__PURE__ */ new Error(`telemetry.dev metric ingest failed: ${res.status}`);
|
|
132
|
+
onError?.(error);
|
|
133
|
+
resultCallback({
|
|
134
|
+
code: 1,
|
|
135
|
+
error
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
resultCallback({ code: 0 });
|
|
140
|
+
}).catch((error) => {
|
|
141
|
+
onError?.(error);
|
|
142
|
+
resultCallback({
|
|
143
|
+
code: 1,
|
|
144
|
+
error: error instanceof Error ? error : void 0
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
selectAggregationTemporality: () => AggregationTemporality.DELTA,
|
|
149
|
+
forceFlush: () => Promise.resolve(),
|
|
150
|
+
shutdown: () => Promise.resolve()
|
|
151
|
+
},
|
|
152
|
+
exportIntervalMillis: 2 ** 31 - 1
|
|
153
|
+
})]
|
|
154
|
+
});
|
|
155
|
+
const meter = meterProvider.getMeter(SCOPE_NAME, SCOPE_VERSION);
|
|
156
|
+
return {
|
|
157
|
+
durationHistogram: meter.createHistogram("gen_ai.client.operation.duration", {
|
|
158
|
+
unit: "s",
|
|
159
|
+
advice: { explicitBucketBoundaries: DURATION_BUCKETS }
|
|
160
|
+
}),
|
|
161
|
+
tokenHistogram: meter.createHistogram("gen_ai.client.token.usage", {
|
|
162
|
+
unit: "{token}",
|
|
163
|
+
advice: { explicitBucketBoundaries: TOKEN_BUCKETS }
|
|
164
|
+
}),
|
|
165
|
+
shutdown: () => meterProvider.shutdown()
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
return {
|
|
169
|
+
tracer,
|
|
170
|
+
sendSpans,
|
|
171
|
+
buildMetrics
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
const coreFor = (config) => {
|
|
175
|
+
const key = `${config.apiKey}|${config.baseUrl}|${config.environment}|${config.serviceName}`;
|
|
176
|
+
const cached = cache.get(key);
|
|
177
|
+
if (cached) return cached;
|
|
178
|
+
const core = buildCore(config);
|
|
179
|
+
cache.set(key, core);
|
|
180
|
+
return core;
|
|
181
|
+
};
|
|
182
|
+
const createEmitter = (config, overrides) => {
|
|
183
|
+
const core = coreFor(config);
|
|
184
|
+
const transport = {
|
|
185
|
+
fetchImpl: config.fetchImpl,
|
|
186
|
+
onError: config.onError
|
|
187
|
+
};
|
|
188
|
+
const onError = config.onError;
|
|
189
|
+
const sendSpans = overrides?.sendSpans ?? ((spans) => core.sendSpans(spans, transport));
|
|
190
|
+
let metrics;
|
|
191
|
+
const ensureMetrics = () => metrics ??= core.buildMetrics(transport);
|
|
192
|
+
const recordDuration = overrides?.recordDuration ?? ((seconds, attributes) => ensureMetrics().durationHistogram.record(seconds, attributes));
|
|
193
|
+
const recordTokens = overrides?.recordTokens ?? ((tokenType, count, attributes) => ensureMetrics().tokenHistogram.record(count, {
|
|
194
|
+
...attributes,
|
|
195
|
+
"gen_ai.token.type": tokenType
|
|
196
|
+
}));
|
|
197
|
+
const flush = (spans) => {
|
|
198
|
+
const pipeline = metrics;
|
|
199
|
+
metrics = void 0;
|
|
200
|
+
const p = (async () => {
|
|
201
|
+
try {
|
|
202
|
+
await sendSpans(spans);
|
|
203
|
+
} catch (e) {
|
|
204
|
+
onError?.(e);
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
if (pipeline) await pipeline.shutdown();
|
|
208
|
+
} catch (e) {
|
|
209
|
+
onError?.(e);
|
|
210
|
+
}
|
|
211
|
+
})();
|
|
212
|
+
if (config.waitUntil) {
|
|
213
|
+
config.waitUntil(p);
|
|
214
|
+
return Promise.resolve();
|
|
215
|
+
}
|
|
216
|
+
return p;
|
|
217
|
+
};
|
|
218
|
+
return {
|
|
219
|
+
tracer: core.tracer,
|
|
220
|
+
recordDuration,
|
|
221
|
+
recordTokens,
|
|
222
|
+
flush
|
|
223
|
+
};
|
|
224
|
+
};
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region src/middleware.ts
|
|
227
|
+
function omitUndefined(attributes) {
|
|
228
|
+
const out = {};
|
|
229
|
+
for (const key of Object.keys(attributes)) {
|
|
230
|
+
const value = attributes[key];
|
|
231
|
+
if (value !== void 0) out[key] = value;
|
|
232
|
+
}
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
function readId(value) {
|
|
236
|
+
if (typeof value === "string") return value.length > 0 ? value : null;
|
|
237
|
+
if (typeof value === "number" || typeof value === "bigint") return value.toString();
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
function jsonAttr(value) {
|
|
241
|
+
if (value === void 0) return void 0;
|
|
242
|
+
if (typeof value === "string") return value;
|
|
243
|
+
try {
|
|
244
|
+
return JSON.stringify(value);
|
|
245
|
+
} catch {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function firstNumber(...candidates) {
|
|
250
|
+
for (const candidate of candidates) if (typeof candidate === "number" && Number.isFinite(candidate)) return candidate;
|
|
251
|
+
}
|
|
252
|
+
function errorTypeName(err) {
|
|
253
|
+
if (err instanceof Error) return err.name || "Error";
|
|
254
|
+
if (err && typeof err === "object" && "name" in err) {
|
|
255
|
+
const n = err.name;
|
|
256
|
+
if (typeof n === "string" && n.length > 0) return n;
|
|
257
|
+
}
|
|
258
|
+
return "Error";
|
|
259
|
+
}
|
|
260
|
+
function errorMessage(err) {
|
|
261
|
+
if (err instanceof Error) return err.message;
|
|
262
|
+
if (typeof err === "string") return err;
|
|
263
|
+
if (err && typeof err === "object" && "message" in err) {
|
|
264
|
+
const m = err.message;
|
|
265
|
+
if (typeof m === "string") return m;
|
|
266
|
+
}
|
|
267
|
+
return String(err);
|
|
268
|
+
}
|
|
269
|
+
const SEVERITY_INFO = 9;
|
|
270
|
+
const SEVERITY_ERROR = 17;
|
|
271
|
+
const MAX_TOKENS_KEYS = [
|
|
272
|
+
"max_output_tokens",
|
|
273
|
+
"max_tokens",
|
|
274
|
+
"max_completion_tokens",
|
|
275
|
+
"maxOutputTokens",
|
|
276
|
+
"maxCompletionTokens",
|
|
277
|
+
"maxTokens"
|
|
278
|
+
];
|
|
279
|
+
function samplingAttributes(modelOptions) {
|
|
280
|
+
const sampling = modelOptions ?? {};
|
|
281
|
+
const nested = sampling["options"] && typeof sampling["options"] === "object" ? sampling["options"] : void 0;
|
|
282
|
+
return omitUndefined({
|
|
283
|
+
"gen_ai.request.temperature": firstNumber(sampling["temperature"], nested?.["temperature"]),
|
|
284
|
+
"gen_ai.request.top_p": firstNumber(sampling["top_p"], sampling["topP"], nested?.["top_p"]),
|
|
285
|
+
"gen_ai.request.max_tokens": firstNumber(...MAX_TOKENS_KEYS.map((key) => sampling[key]), nested?.["num_predict"])
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Build a TanStack AI chat middleware that streams `chat()` runs to telemetry.dev as
|
|
290
|
+
* OpenTelemetry GenAI (`gen_ai.*`) spans + metrics: one root span per `chat()` call, one CLIENT
|
|
291
|
+
* span per agent-loop iteration, and one span per tool execution. Per-run state is keyed by the
|
|
292
|
+
* middleware context in a WeakMap, so a single `telemetryDev()` instance is safe to share across
|
|
293
|
+
* concurrent and overlapping `chat()` calls (e.g. registered once at module scope).
|
|
294
|
+
*/
|
|
295
|
+
function telemetryDev(options, overrides) {
|
|
296
|
+
const config = resolveConfig(options);
|
|
297
|
+
if (!config.apiKey) return { name: "telemetry-dev" };
|
|
298
|
+
const emitter = createEmitter(config, overrides);
|
|
299
|
+
const onError = config.onError;
|
|
300
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
301
|
+
const closeIteration = (state) => {
|
|
302
|
+
const iteration = state.iteration;
|
|
303
|
+
if (!iteration) return;
|
|
304
|
+
const endedAt = /* @__PURE__ */ new Date();
|
|
305
|
+
const usage = iteration.usage;
|
|
306
|
+
iteration.span.setAttributes(omitUndefined({
|
|
307
|
+
"gen_ai.response.model": iteration.responseModel ?? void 0,
|
|
308
|
+
"gen_ai.response.finish_reasons": iteration.finishReason ? [iteration.finishReason] : void 0,
|
|
309
|
+
"gen_ai.usage.input_tokens": usage?.promptTokens,
|
|
310
|
+
"gen_ai.usage.output_tokens": usage?.completionTokens,
|
|
311
|
+
"gen_ai.usage.cache_read.input_tokens": usage?.promptTokensDetails?.cachedTokens,
|
|
312
|
+
"gen_ai.usage.cache_creation.input_tokens": usage?.promptTokensDetails?.cacheWriteTokens,
|
|
313
|
+
"gen_ai.usage.reasoning.output_tokens": usage?.completionTokensDetails?.reasoningTokens,
|
|
314
|
+
"gen_ai.usage.cost": usage?.cost,
|
|
315
|
+
"gen_ai.output.type": iteration.structured ? "json" : "text",
|
|
316
|
+
"gen_ai.output.messages": jsonAttr(iteration.outputText ?? void 0)
|
|
317
|
+
}));
|
|
318
|
+
iteration.span.end(endedAt);
|
|
319
|
+
state.childSpans.push(iteration.span);
|
|
320
|
+
state.iterationMetrics.push({
|
|
321
|
+
durationSec: Math.max(endedAt.getTime() - iteration.startedAt.getTime(), 0) / 1e3,
|
|
322
|
+
inputTokens: usage?.promptTokens ?? null,
|
|
323
|
+
outputTokens: usage?.completionTokens ?? null
|
|
324
|
+
});
|
|
325
|
+
if (iteration.structured && iteration.outputText !== null) state.structuredOutput = iteration.outputText;
|
|
326
|
+
state.iteration = null;
|
|
327
|
+
};
|
|
328
|
+
const setRootBaseAttributes = (state) => {
|
|
329
|
+
state.rootSpan.setAttributes(omitUndefined({
|
|
330
|
+
"gen_ai.operation.name": state.hasToolSpan ? "invoke_agent" : "chat",
|
|
331
|
+
"gen_ai.provider.name": state.provider,
|
|
332
|
+
"gen_ai.request.model": state.requestModel,
|
|
333
|
+
"gen_ai.response.model": state.responseModel ?? void 0,
|
|
334
|
+
"gen_ai.conversation.id": state.sessionId,
|
|
335
|
+
"gen_ai.input.messages": state.rootInput,
|
|
336
|
+
"user.id": state.userId ?? void 0,
|
|
337
|
+
...state.rootSampling
|
|
338
|
+
}));
|
|
339
|
+
if (state.restMetadata) for (const [key, value] of Object.entries(state.restMetadata)) {
|
|
340
|
+
const attr = typeof value === "string" ? value : jsonAttr(value);
|
|
341
|
+
if (attr !== void 0) state.rootSpan.setAttribute(`td.metadata.${key}`, attr);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
const addSummaryEvent = (state, hasError, message) => {
|
|
345
|
+
const inputPresent = state.iterationMetrics.some((m) => m.inputTokens !== null);
|
|
346
|
+
const outputPresent = state.iterationMetrics.some((m) => m.outputTokens !== null);
|
|
347
|
+
const inputTokens = state.iterationMetrics.reduce((sum, m) => sum + (m.inputTokens ?? 0), 0);
|
|
348
|
+
const outputTokens = state.iterationMetrics.reduce((sum, m) => sum + (m.outputTokens ?? 0), 0);
|
|
349
|
+
state.rootSpan.addEvent("generation.summary", omitUndefined({
|
|
350
|
+
"log.severity_number": hasError ? SEVERITY_ERROR : SEVERITY_INFO,
|
|
351
|
+
"log.message": message,
|
|
352
|
+
"gen_ai.usage.input_tokens": inputPresent ? inputTokens : void 0,
|
|
353
|
+
"gen_ai.usage.output_tokens": outputPresent ? outputTokens : void 0
|
|
354
|
+
}));
|
|
355
|
+
};
|
|
356
|
+
const recordRunMetrics = (state) => {
|
|
357
|
+
const metricBase = omitUndefined({
|
|
358
|
+
"gen_ai.provider.name": state.provider,
|
|
359
|
+
"gen_ai.request.model": state.requestModel,
|
|
360
|
+
"gen_ai.response.model": state.responseModel ?? void 0
|
|
361
|
+
});
|
|
362
|
+
for (const m of state.iterationMetrics) {
|
|
363
|
+
const attrs = {
|
|
364
|
+
...metricBase,
|
|
365
|
+
"gen_ai.operation.name": "chat"
|
|
366
|
+
};
|
|
367
|
+
emitter.recordDuration(m.durationSec, attrs);
|
|
368
|
+
if (m.inputTokens !== null) emitter.recordTokens("input", m.inputTokens, attrs);
|
|
369
|
+
if (m.outputTokens !== null) emitter.recordTokens("output", m.outputTokens, attrs);
|
|
370
|
+
}
|
|
371
|
+
for (const t of state.toolMetrics) emitter.recordDuration(t.durationSec, {
|
|
372
|
+
...metricBase,
|
|
373
|
+
"gen_ai.operation.name": "execute_tool"
|
|
374
|
+
});
|
|
375
|
+
};
|
|
376
|
+
const failOpenSpans = (state, errType, message) => {
|
|
377
|
+
for (const [, entry] of state.openTools) {
|
|
378
|
+
entry.span.setStatus({
|
|
379
|
+
code: SpanStatusCode.ERROR,
|
|
380
|
+
message
|
|
381
|
+
});
|
|
382
|
+
entry.span.setAttribute("error.type", errType);
|
|
383
|
+
entry.span.end();
|
|
384
|
+
state.childSpans.push(entry.span);
|
|
385
|
+
}
|
|
386
|
+
state.openTools.clear();
|
|
387
|
+
if (state.iteration) {
|
|
388
|
+
state.iteration.span.setStatus({
|
|
389
|
+
code: SpanStatusCode.ERROR,
|
|
390
|
+
message
|
|
391
|
+
});
|
|
392
|
+
state.iteration.span.setAttribute("error.type", errType);
|
|
393
|
+
closeIteration(state);
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
return {
|
|
397
|
+
name: "telemetry-dev",
|
|
398
|
+
onStart(ctx) {
|
|
399
|
+
try {
|
|
400
|
+
const rawMetadata = ctx.options?.["metadata"];
|
|
401
|
+
const metadata = rawMetadata && typeof rawMetadata === "object" ? rawMetadata : void 0;
|
|
402
|
+
const userId = readId(metadata?.["userId"]);
|
|
403
|
+
const sessionId = readId(metadata?.["sessionId"]) ?? ctx.threadId;
|
|
404
|
+
let restMetadata;
|
|
405
|
+
if (metadata) {
|
|
406
|
+
const rest = {};
|
|
407
|
+
for (const [key, value] of Object.entries(metadata)) if (key !== "userId" && key !== "sessionId") rest[key] = value;
|
|
408
|
+
restMetadata = Object.keys(rest).length > 0 ? rest : void 0;
|
|
409
|
+
}
|
|
410
|
+
const rootSpan = emitter.tracer.startSpan("chat", {
|
|
411
|
+
startTime: /* @__PURE__ */ new Date(),
|
|
412
|
+
kind: SpanKind.INTERNAL
|
|
413
|
+
});
|
|
414
|
+
states.set(ctx, {
|
|
415
|
+
rootSpan,
|
|
416
|
+
rootCtx: trace.setSpan(ROOT_CONTEXT, rootSpan),
|
|
417
|
+
provider: ctx.provider,
|
|
418
|
+
requestModel: ctx.model,
|
|
419
|
+
responseModel: null,
|
|
420
|
+
userId,
|
|
421
|
+
sessionId,
|
|
422
|
+
restMetadata,
|
|
423
|
+
rootInput: void 0,
|
|
424
|
+
rootSampling: {},
|
|
425
|
+
rootCaptured: false,
|
|
426
|
+
iteration: null,
|
|
427
|
+
structuredOutput: null,
|
|
428
|
+
openTools: /* @__PURE__ */ new Map(),
|
|
429
|
+
childSpans: [],
|
|
430
|
+
hasToolSpan: false,
|
|
431
|
+
iterationMetrics: [],
|
|
432
|
+
toolMetrics: []
|
|
433
|
+
});
|
|
434
|
+
} catch (err) {
|
|
435
|
+
onError?.(err);
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
onConfig(ctx, chatConfig) {
|
|
439
|
+
if (ctx.phase !== "beforeModel" && ctx.phase !== "structuredOutput") return void 0;
|
|
440
|
+
try {
|
|
441
|
+
const state = states.get(ctx);
|
|
442
|
+
if (!state) return void 0;
|
|
443
|
+
closeIteration(state);
|
|
444
|
+
const inputMessages = [];
|
|
445
|
+
for (const prompt of chatConfig.systemPrompts) inputMessages.push({
|
|
446
|
+
role: "system",
|
|
447
|
+
content: typeof prompt === "string" ? prompt : prompt.content
|
|
448
|
+
});
|
|
449
|
+
for (const message of chatConfig.messages) inputMessages.push({
|
|
450
|
+
role: message.role,
|
|
451
|
+
content: message.content
|
|
452
|
+
});
|
|
453
|
+
const inputJson = jsonAttr(inputMessages);
|
|
454
|
+
const sampling = samplingAttributes(chatConfig.modelOptions ?? ctx.modelOptions);
|
|
455
|
+
if (!state.rootCaptured) {
|
|
456
|
+
state.rootCaptured = true;
|
|
457
|
+
state.rootInput = inputJson;
|
|
458
|
+
state.rootSampling = sampling;
|
|
459
|
+
}
|
|
460
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
461
|
+
const span = emitter.tracer.startSpan("chat", {
|
|
462
|
+
startTime: startedAt,
|
|
463
|
+
kind: SpanKind.CLIENT,
|
|
464
|
+
attributes: omitUndefined({
|
|
465
|
+
"gen_ai.operation.name": "chat",
|
|
466
|
+
"gen_ai.provider.name": ctx.provider,
|
|
467
|
+
"gen_ai.request.model": ctx.model,
|
|
468
|
+
"gen_ai.conversation.id": state.sessionId,
|
|
469
|
+
"gen_ai.input.messages": inputJson,
|
|
470
|
+
...sampling
|
|
471
|
+
})
|
|
472
|
+
}, state.rootCtx);
|
|
473
|
+
state.iteration = {
|
|
474
|
+
span,
|
|
475
|
+
otelCtx: trace.setSpan(state.rootCtx, span),
|
|
476
|
+
startedAt,
|
|
477
|
+
usage: null,
|
|
478
|
+
finishReason: null,
|
|
479
|
+
responseModel: null,
|
|
480
|
+
outputText: null,
|
|
481
|
+
structured: ctx.phase === "structuredOutput"
|
|
482
|
+
};
|
|
483
|
+
} catch (err) {
|
|
484
|
+
onError?.(err);
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
onChunk(ctx, chunk) {
|
|
488
|
+
if (chunk.type !== "RUN_FINISHED" && chunk.type !== "CUSTOM") return void 0;
|
|
489
|
+
try {
|
|
490
|
+
const state = states.get(ctx);
|
|
491
|
+
const iteration = state?.iteration;
|
|
492
|
+
if (!state || !iteration) return void 0;
|
|
493
|
+
if (chunk.type === "CUSTOM") {
|
|
494
|
+
if (iteration.structured && chunk.name === "structured-output.complete") {
|
|
495
|
+
const raw = chunk.value?.raw;
|
|
496
|
+
if (typeof raw === "string") iteration.outputText = raw;
|
|
497
|
+
}
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
iteration.finishReason = chunk.finishReason ?? null;
|
|
501
|
+
if (chunk.model) {
|
|
502
|
+
iteration.responseModel = chunk.model;
|
|
503
|
+
state.responseModel = chunk.model;
|
|
504
|
+
}
|
|
505
|
+
if (chunk.usage) iteration.usage = chunk.usage;
|
|
506
|
+
if (!iteration.structured) iteration.outputText = ctx.accumulatedContent.length > 0 ? ctx.accumulatedContent : null;
|
|
507
|
+
} catch (err) {
|
|
508
|
+
onError?.(err);
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
onUsage(ctx, usage) {
|
|
512
|
+
try {
|
|
513
|
+
const state = states.get(ctx);
|
|
514
|
+
if (state?.iteration) state.iteration.usage = usage;
|
|
515
|
+
} catch (err) {
|
|
516
|
+
onError?.(err);
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
onBeforeToolCall(ctx, hookCtx) {
|
|
520
|
+
try {
|
|
521
|
+
const state = states.get(ctx);
|
|
522
|
+
if (!state) return void 0;
|
|
523
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
524
|
+
const span = emitter.tracer.startSpan("execute_tool", {
|
|
525
|
+
startTime: startedAt,
|
|
526
|
+
kind: SpanKind.INTERNAL,
|
|
527
|
+
attributes: omitUndefined({
|
|
528
|
+
"gen_ai.operation.name": "execute_tool",
|
|
529
|
+
"gen_ai.tool.name": hookCtx.toolName,
|
|
530
|
+
"gen_ai.tool.call.id": hookCtx.toolCallId,
|
|
531
|
+
"gen_ai.conversation.id": state.sessionId,
|
|
532
|
+
"gen_ai.tool.call.arguments": jsonAttr(hookCtx.args ?? null)
|
|
533
|
+
})
|
|
534
|
+
}, state.iteration?.otelCtx ?? state.rootCtx);
|
|
535
|
+
state.openTools.set(hookCtx.toolCallId, {
|
|
536
|
+
span,
|
|
537
|
+
startedAt
|
|
538
|
+
});
|
|
539
|
+
state.hasToolSpan = true;
|
|
540
|
+
} catch (err) {
|
|
541
|
+
onError?.(err);
|
|
542
|
+
}
|
|
543
|
+
},
|
|
544
|
+
onAfterToolCall(ctx, info) {
|
|
545
|
+
try {
|
|
546
|
+
const state = states.get(ctx);
|
|
547
|
+
const entry = state?.openTools.get(info.toolCallId);
|
|
548
|
+
if (!state || !entry) return;
|
|
549
|
+
state.openTools.delete(info.toolCallId);
|
|
550
|
+
const { span } = entry;
|
|
551
|
+
if (info.ok) {
|
|
552
|
+
const result = jsonAttr(info.result ?? null);
|
|
553
|
+
if (result !== void 0) span.setAttribute("gen_ai.tool.call.result", result);
|
|
554
|
+
} else {
|
|
555
|
+
const message = errorMessage(info.error);
|
|
556
|
+
const errType = info.error instanceof Error ? info.error.name : "tool_error";
|
|
557
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
558
|
+
span.setAttribute("error.type", errType);
|
|
559
|
+
span.addEvent("exception", {
|
|
560
|
+
"exception.type": errType,
|
|
561
|
+
"exception.message": message,
|
|
562
|
+
"log.severity_number": SEVERITY_ERROR
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
span.end();
|
|
566
|
+
state.childSpans.push(span);
|
|
567
|
+
state.toolMetrics.push({ durationSec: Math.max(info.duration, 0) / 1e3 });
|
|
568
|
+
} catch (err) {
|
|
569
|
+
onError?.(err);
|
|
570
|
+
}
|
|
571
|
+
},
|
|
572
|
+
async onToolPhaseComplete(ctx, info) {
|
|
573
|
+
if (info.needsApproval.length === 0 && info.needsClientExecution.length === 0) return;
|
|
574
|
+
try {
|
|
575
|
+
const state = states.get(ctx);
|
|
576
|
+
if (!state) return;
|
|
577
|
+
states.delete(ctx);
|
|
578
|
+
for (const [, entry] of state.openTools) {
|
|
579
|
+
entry.span.end();
|
|
580
|
+
state.childSpans.push(entry.span);
|
|
581
|
+
}
|
|
582
|
+
state.openTools.clear();
|
|
583
|
+
const finishReason = state.iteration?.finishReason ?? "tool_calls";
|
|
584
|
+
closeIteration(state);
|
|
585
|
+
state.hasToolSpan = true;
|
|
586
|
+
setRootBaseAttributes(state);
|
|
587
|
+
state.rootSpan.setAttributes(omitUndefined({
|
|
588
|
+
"gen_ai.output.messages": jsonAttr(ctx.accumulatedContent.length > 0 ? ctx.accumulatedContent : void 0),
|
|
589
|
+
"gen_ai.response.finish_reasons": [finishReason]
|
|
590
|
+
}));
|
|
591
|
+
addSummaryEvent(state, false, `Generation paused awaiting tools (${[...info.needsApproval, ...info.needsClientExecution].map((t) => t.toolName).join(", ")})`);
|
|
592
|
+
state.rootSpan.end();
|
|
593
|
+
recordRunMetrics(state);
|
|
594
|
+
await emitter.flush([state.rootSpan, ...state.childSpans]);
|
|
595
|
+
} catch (err) {
|
|
596
|
+
onError?.(err);
|
|
597
|
+
}
|
|
598
|
+
},
|
|
599
|
+
async onFinish(ctx, info) {
|
|
600
|
+
try {
|
|
601
|
+
const state = states.get(ctx);
|
|
602
|
+
if (!state) return;
|
|
603
|
+
states.delete(ctx);
|
|
604
|
+
for (const [, entry] of state.openTools) {
|
|
605
|
+
entry.span.end();
|
|
606
|
+
state.childSpans.push(entry.span);
|
|
607
|
+
}
|
|
608
|
+
state.openTools.clear();
|
|
609
|
+
closeIteration(state);
|
|
610
|
+
setRootBaseAttributes(state);
|
|
611
|
+
state.rootSpan.setAttributes(omitUndefined({
|
|
612
|
+
"gen_ai.output.messages": jsonAttr(state.structuredOutput ?? info.content),
|
|
613
|
+
"gen_ai.response.finish_reasons": info.finishReason ? [info.finishReason] : void 0
|
|
614
|
+
}));
|
|
615
|
+
const finishReason = info.finishReason ?? "unknown";
|
|
616
|
+
const inputTokens = state.iterationMetrics.reduce((sum, m) => sum + (m.inputTokens ?? 0), 0);
|
|
617
|
+
const outputTokens = state.iterationMetrics.reduce((sum, m) => sum + (m.outputTokens ?? 0), 0);
|
|
618
|
+
const tokenParts = [];
|
|
619
|
+
if (state.iterationMetrics.some((m) => m.inputTokens !== null)) tokenParts.push(`${inputTokens} in`);
|
|
620
|
+
if (state.iterationMetrics.some((m) => m.outputTokens !== null)) tokenParts.push(`${outputTokens} out`);
|
|
621
|
+
addSummaryEvent(state, false, `Generation completed (${finishReason})${tokenParts.length > 0 ? `: ${tokenParts.join(" / ")} tokens` : ""}`);
|
|
622
|
+
state.rootSpan.end();
|
|
623
|
+
recordRunMetrics(state);
|
|
624
|
+
await emitter.flush([state.rootSpan, ...state.childSpans]);
|
|
625
|
+
} catch (err) {
|
|
626
|
+
onError?.(err);
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
async onError(ctx, info) {
|
|
630
|
+
try {
|
|
631
|
+
const state = states.get(ctx);
|
|
632
|
+
if (!state) return;
|
|
633
|
+
states.delete(ctx);
|
|
634
|
+
const errType = errorTypeName(info.error);
|
|
635
|
+
const message = errorMessage(info.error);
|
|
636
|
+
failOpenSpans(state, errType, message);
|
|
637
|
+
setRootBaseAttributes(state);
|
|
638
|
+
state.rootSpan.setStatus({
|
|
639
|
+
code: SpanStatusCode.ERROR,
|
|
640
|
+
message
|
|
641
|
+
});
|
|
642
|
+
state.rootSpan.setAttribute("error.type", errType);
|
|
643
|
+
state.rootSpan.addEvent("exception", {
|
|
644
|
+
"exception.type": errType,
|
|
645
|
+
"exception.message": message,
|
|
646
|
+
"log.severity_number": SEVERITY_ERROR
|
|
647
|
+
});
|
|
648
|
+
addSummaryEvent(state, true, `Generation failed (${errType})`);
|
|
649
|
+
state.rootSpan.end();
|
|
650
|
+
recordRunMetrics(state);
|
|
651
|
+
await emitter.flush([state.rootSpan, ...state.childSpans]);
|
|
652
|
+
} catch (err) {
|
|
653
|
+
onError?.(err);
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
async onAbort(ctx, info) {
|
|
657
|
+
try {
|
|
658
|
+
const state = states.get(ctx);
|
|
659
|
+
if (!state) return;
|
|
660
|
+
states.delete(ctx);
|
|
661
|
+
const message = info.reason ?? "cancelled";
|
|
662
|
+
failOpenSpans(state, "cancelled", message);
|
|
663
|
+
setRootBaseAttributes(state);
|
|
664
|
+
state.rootSpan.setStatus({
|
|
665
|
+
code: SpanStatusCode.ERROR,
|
|
666
|
+
message
|
|
667
|
+
});
|
|
668
|
+
state.rootSpan.setAttribute("error.type", "cancelled");
|
|
669
|
+
state.rootSpan.setAttribute("gen_ai.response.finish_reasons", ["cancelled"]);
|
|
670
|
+
addSummaryEvent(state, true, `Generation cancelled (${message})`);
|
|
671
|
+
state.rootSpan.end();
|
|
672
|
+
recordRunMetrics(state);
|
|
673
|
+
await emitter.flush([state.rootSpan, ...state.childSpans]);
|
|
674
|
+
} catch (err) {
|
|
675
|
+
onError?.(err);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
//#endregion
|
|
681
|
+
export { telemetryDev };
|