@ai-sdk/devtools 1.0.0-beta.1 → 1.0.0-beta.11

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.
@@ -0,0 +1,511 @@
1
+ import type {
2
+ OnStartEvent,
3
+ OnStepStartEvent,
4
+ OnStepFinishEvent,
5
+ OnChunkEvent,
6
+ OnFinishEvent,
7
+ ObjectOnStartEvent,
8
+ ObjectOnStepStartEvent,
9
+ ObjectOnStepFinishEvent,
10
+ ObjectOnFinishEvent,
11
+ TelemetryIntegration,
12
+ ToolSet,
13
+ } from 'ai';
14
+ import {
15
+ createRun,
16
+ createStep,
17
+ updateStepResult,
18
+ notifyServerAsync,
19
+ } from './db.js';
20
+
21
+ type OperationType = 'generate' | 'stream';
22
+
23
+ interface StepState {
24
+ stepId: string;
25
+ startTime: number;
26
+ streamChunks: unknown[];
27
+ rawStreamChunks: unknown[];
28
+ }
29
+
30
+ interface CallState {
31
+ runId: string;
32
+ operationType: OperationType;
33
+ functionId: string | undefined;
34
+ settings: Record<string, unknown>;
35
+ stepStates: Map<number, StepState>;
36
+ }
37
+
38
+ const activeSteps = new Map<string, StepState>();
39
+
40
+ let signalHandlersRegistered = false;
41
+ const registerSignalHandlers = () => {
42
+ if (signalHandlersRegistered) return;
43
+ signalHandlersRegistered = true;
44
+
45
+ const cleanup = async () => {
46
+ if (activeSteps.size === 0) return;
47
+
48
+ const promises = Array.from(activeSteps.entries()).map(
49
+ async ([stepId, data]) => {
50
+ const durationMs = Date.now() - data.startTime;
51
+ await updateStepResult(stepId, {
52
+ duration_ms: durationMs,
53
+ output: null,
54
+ usage: null,
55
+ error: 'Request aborted',
56
+ raw_request: null,
57
+ raw_response: null,
58
+ raw_chunks: null,
59
+ });
60
+ },
61
+ );
62
+ await Promise.all(promises);
63
+ await notifyServerAsync('step-update');
64
+ };
65
+
66
+ process.on('SIGINT', () => {
67
+ cleanup().then(() => process.exit(130));
68
+ });
69
+
70
+ process.on('SIGTERM', () => {
71
+ cleanup().then(() => process.exit(143));
72
+ });
73
+ };
74
+
75
+ const generateRunId = (): string => {
76
+ const now = new Date();
77
+ const timestamp = now
78
+ .toISOString()
79
+ .replace(/[-:T.Z]/g, '')
80
+ .slice(0, 17);
81
+ const uniqueId = crypto.randomUUID().slice(0, 8);
82
+ return `${timestamp}-${uniqueId}`;
83
+ };
84
+
85
+ function getOperationType(operationId: string): OperationType {
86
+ if (operationId === 'ai.streamText' || operationId === 'ai.streamObject') {
87
+ return 'stream';
88
+ }
89
+ return 'generate';
90
+ }
91
+
92
+ /**
93
+ * Creates a devtools telemetry integration that logs all AI SDK operations
94
+ * to the devtools viewer.
95
+ *
96
+ * Usage:
97
+ * ```ts
98
+ * import { registerTelemetryIntegration } from 'ai';
99
+ * import { DevToolsTelemetry } from '@ai-sdk/devtools';
100
+ *
101
+ * registerTelemetryIntegration(DevToolsTelemetry());
102
+ * ```
103
+ *
104
+ * Then enable telemetry on your AI SDK calls:
105
+ * ```ts
106
+ * const result = await generateText({
107
+ * model: openai('gpt-4o'),
108
+ * prompt: 'Hello!',
109
+ * experimental_telemetry: { isEnabled: true },
110
+ * });
111
+ * ```
112
+ */
113
+ export function DevToolsTelemetry(): TelemetryIntegration {
114
+ if (process.env.NODE_ENV === 'production') {
115
+ throw new Error(
116
+ '@ai-sdk/devtools should not be used in production. ' +
117
+ 'Remove DevToolsTelemetry from your telemetry configuration for production builds.',
118
+ );
119
+ }
120
+
121
+ registerSignalHandlers();
122
+
123
+ const callStates = new Map<string, CallState>();
124
+
125
+ // When executeTool runs a tool's execute function, any nested generateText/
126
+ // streamText call inside that tool gets its own callId. We track the nesting
127
+ // so that the inner call's run can be linked to the parent.
128
+ //
129
+ // We use a per-toolCallId Map instead of a single variable so that parallel
130
+ // tool calls
131
+ const toolContextMap = new Map<
132
+ string,
133
+ { parentCallId: string; parentToolCallId: string }
134
+ >();
135
+ let currentToolCallId: string | null = null;
136
+
137
+ function resolveParentInfo(): { runId: string; stepId: string } | undefined {
138
+ if (!currentToolCallId) return undefined;
139
+
140
+ const ctx = toolContextMap.get(currentToolCallId);
141
+ if (!ctx) return undefined;
142
+
143
+ const parentState = callStates.get(ctx.parentCallId);
144
+ if (!parentState) return undefined;
145
+
146
+ // Find the step that is currently executing the tool call.
147
+ // This is the most recent (highest step number) step in the parent call.
148
+ let latestStepId: string | undefined;
149
+ let latestStepNumber = -1;
150
+ for (const [stepNumber, stepState] of parentState.stepStates) {
151
+ if (stepNumber > latestStepNumber) {
152
+ latestStepNumber = stepNumber;
153
+ latestStepId = stepState.stepId;
154
+ }
155
+ }
156
+
157
+ if (!latestStepId) return undefined;
158
+
159
+ return { runId: parentState.runId, stepId: latestStepId };
160
+ }
161
+
162
+ function getOrCreateCallState(
163
+ callId: string,
164
+ operationId: string,
165
+ event: {
166
+ functionId?: string | undefined;
167
+ maxOutputTokens?: number | undefined;
168
+ temperature?: number | undefined;
169
+ topP?: number | undefined;
170
+ topK?: number | undefined;
171
+ presencePenalty?: number | undefined;
172
+ frequencyPenalty?: number | undefined;
173
+ seed?: number | undefined;
174
+ },
175
+ ): CallState {
176
+ let state = callStates.get(callId);
177
+ if (state) return state;
178
+
179
+ state = {
180
+ runId: generateRunId(),
181
+ operationType: getOperationType(operationId),
182
+ functionId: event.functionId,
183
+ settings: {
184
+ maxOutputTokens: event.maxOutputTokens,
185
+ temperature: event.temperature,
186
+ topP: event.topP,
187
+ topK: event.topK,
188
+ presencePenalty: event.presencePenalty,
189
+ frequencyPenalty: event.frequencyPenalty,
190
+ seed: event.seed,
191
+ },
192
+ stepStates: new Map(),
193
+ };
194
+ callStates.set(callId, state);
195
+ return state;
196
+ }
197
+
198
+ const integration: TelemetryIntegration = {
199
+ onStart: async event => {
200
+ const operationId = (event as { operationId: string }).operationId;
201
+
202
+ if (
203
+ operationId === 'ai.embed' ||
204
+ operationId === 'ai.embedMany' ||
205
+ operationId === 'ai.rerank'
206
+ ) {
207
+ return;
208
+ }
209
+
210
+ const startEvent = event as OnStartEvent<ToolSet> | ObjectOnStartEvent;
211
+
212
+ const parentInfo = resolveParentInfo();
213
+
214
+ const state = getOrCreateCallState(
215
+ startEvent.callId,
216
+ operationId,
217
+ startEvent,
218
+ );
219
+
220
+ await createRun(state.runId, parentInfo, state.functionId);
221
+ },
222
+
223
+ onStepStart: async event => {
224
+ const stepStartEvent = event as OnStepStartEvent<ToolSet> & {
225
+ promptMessages?: unknown[];
226
+ };
227
+
228
+ const state = callStates.get(stepStartEvent.callId);
229
+ if (!state) return;
230
+
231
+ const stepId = crypto.randomUUID();
232
+ const startTime = Date.now();
233
+
234
+ const stepState: StepState = {
235
+ stepId,
236
+ startTime,
237
+ streamChunks: [],
238
+ rawStreamChunks: [],
239
+ };
240
+ state.stepStates.set(stepStartEvent.stepNumber, stepState);
241
+ activeSteps.set(stepId, stepState);
242
+
243
+ const prompt = stepStartEvent.promptMessages ?? stepStartEvent.messages;
244
+
245
+ await createStep({
246
+ id: stepId,
247
+ run_id: state.runId,
248
+ step_number: stepStartEvent.stepNumber + 1,
249
+ type: state.operationType,
250
+ model_id: stepStartEvent.modelId,
251
+ provider: stepStartEvent.provider ?? null,
252
+ started_at: new Date().toISOString(),
253
+ input: JSON.stringify({
254
+ prompt,
255
+ tools: stepStartEvent.tools
256
+ ? Object.entries(stepStartEvent.tools).map(([name, tool]) => ({
257
+ name,
258
+ description: (tool as { description?: string }).description,
259
+ parameters: (tool as { parameters?: unknown }).parameters,
260
+ }))
261
+ : undefined,
262
+ toolChoice: stepStartEvent.toolChoice,
263
+ maxOutputTokens: state.settings.maxOutputTokens,
264
+ temperature: state.settings.temperature,
265
+ topP: state.settings.topP,
266
+ topK: state.settings.topK,
267
+ presencePenalty: state.settings.presencePenalty,
268
+ frequencyPenalty: state.settings.frequencyPenalty,
269
+ seed: state.settings.seed,
270
+ }),
271
+ provider_options: stepStartEvent.providerOptions
272
+ ? JSON.stringify(stepStartEvent.providerOptions)
273
+ : null,
274
+ });
275
+ },
276
+
277
+ onObjectStepStart: async event => {
278
+ const stepStartEvent = event as ObjectOnStepStartEvent & {
279
+ promptMessages?: unknown[];
280
+ };
281
+
282
+ const state = callStates.get(stepStartEvent.callId);
283
+ if (!state) return;
284
+
285
+ const stepId = crypto.randomUUID();
286
+ const startTime = Date.now();
287
+
288
+ const stepState: StepState = {
289
+ stepId,
290
+ startTime,
291
+ streamChunks: [],
292
+ rawStreamChunks: [],
293
+ };
294
+ state.stepStates.set(stepStartEvent.stepNumber, stepState);
295
+ activeSteps.set(stepId, stepState);
296
+
297
+ await createStep({
298
+ id: stepId,
299
+ run_id: state.runId,
300
+ step_number: stepStartEvent.stepNumber + 1,
301
+ type: state.operationType,
302
+ model_id: stepStartEvent.modelId,
303
+ provider: stepStartEvent.provider ?? null,
304
+ started_at: new Date().toISOString(),
305
+ input: JSON.stringify({
306
+ prompt: stepStartEvent.promptMessages,
307
+ maxOutputTokens: state.settings.maxOutputTokens,
308
+ temperature: state.settings.temperature,
309
+ topP: state.settings.topP,
310
+ topK: state.settings.topK,
311
+ presencePenalty: state.settings.presencePenalty,
312
+ frequencyPenalty: state.settings.frequencyPenalty,
313
+ seed: state.settings.seed,
314
+ }),
315
+ provider_options: stepStartEvent.providerOptions
316
+ ? JSON.stringify(stepStartEvent.providerOptions)
317
+ : null,
318
+ });
319
+ },
320
+
321
+ onChunk: async event => {
322
+ const { chunk } = event as OnChunkEvent;
323
+
324
+ if (chunk.type === 'raw') {
325
+ const rawValue = (chunk as { rawValue: unknown }).rawValue;
326
+ for (const [, state] of callStates) {
327
+ let latestStepState: StepState | undefined;
328
+ let latestStepNumber = -1;
329
+ for (const [stepNumber, ss] of state.stepStates) {
330
+ if (stepNumber > latestStepNumber) {
331
+ latestStepNumber = stepNumber;
332
+ latestStepState = ss;
333
+ }
334
+ }
335
+ if (latestStepState) {
336
+ latestStepState.rawStreamChunks.push(rawValue);
337
+ return;
338
+ }
339
+ }
340
+ return;
341
+ }
342
+
343
+ if ('callId' in chunk && 'stepNumber' in chunk) {
344
+ const typed = chunk as { callId: string; stepNumber: number };
345
+ const state = callStates.get(typed.callId);
346
+ if (!state) return;
347
+ const stepState = state.stepStates.get(typed.stepNumber);
348
+ if (!stepState) return;
349
+ stepState.streamChunks.push(chunk);
350
+ return;
351
+ }
352
+
353
+ for (const [, state] of callStates) {
354
+ let latestStepState: StepState | undefined;
355
+ let latestStepNumber = -1;
356
+ for (const [stepNumber, ss] of state.stepStates) {
357
+ if (stepNumber > latestStepNumber) {
358
+ latestStepNumber = stepNumber;
359
+ latestStepState = ss;
360
+ }
361
+ }
362
+ if (latestStepState) {
363
+ latestStepState.streamChunks.push(chunk);
364
+ return;
365
+ }
366
+ }
367
+ },
368
+
369
+ onStepFinish: async event => {
370
+ const stepResult = event as OnStepFinishEvent<ToolSet>;
371
+
372
+ const state = callStates.get(stepResult.callId);
373
+ if (!state) return;
374
+
375
+ const stepState = state.stepStates.get(stepResult.stepNumber);
376
+ if (!stepState) return;
377
+
378
+ activeSteps.delete(stepState.stepId);
379
+
380
+ const durationMs = Date.now() - stepState.startTime;
381
+
382
+ const output = {
383
+ content: stepResult.content,
384
+ finishReason: stepResult.finishReason,
385
+ response: {
386
+ id: stepResult.response.id,
387
+ modelId: stepResult.response.modelId,
388
+ timestamp: stepResult.response.timestamp,
389
+ messages: stepResult.response.messages,
390
+ },
391
+ };
392
+
393
+ const hasStreamChunks = stepState.streamChunks.length > 0;
394
+ const hasRawStreamChunks = stepState.rawStreamChunks.length > 0;
395
+
396
+ await updateStepResult(stepState.stepId, {
397
+ duration_ms: durationMs,
398
+ output: JSON.stringify(output),
399
+ usage: stepResult.usage ? JSON.stringify(stepResult.usage) : null,
400
+ error: null,
401
+ raw_request: stepResult.request?.body
402
+ ? JSON.stringify(stepResult.request.body)
403
+ : null,
404
+ raw_response: stepResult.response?.body
405
+ ? JSON.stringify(stepResult.response.body)
406
+ : hasStreamChunks
407
+ ? JSON.stringify(stepState.streamChunks)
408
+ : null,
409
+ raw_chunks: hasRawStreamChunks
410
+ ? JSON.stringify(stepState.rawStreamChunks)
411
+ : null,
412
+ });
413
+
414
+ state.stepStates.delete(stepResult.stepNumber);
415
+ },
416
+
417
+ onObjectStepFinish: async event => {
418
+ const stepResult = event as ObjectOnStepFinishEvent;
419
+
420
+ const state = callStates.get(stepResult.callId);
421
+ if (!state) return;
422
+
423
+ const stepState = state.stepStates.get(stepResult.stepNumber);
424
+ if (!stepState) return;
425
+
426
+ activeSteps.delete(stepState.stepId);
427
+
428
+ const durationMs = Date.now() - stepState.startTime;
429
+
430
+ const output = {
431
+ finishReason: stepResult.finishReason,
432
+ objectText: stepResult.objectText,
433
+ response: {
434
+ id: stepResult.response.id,
435
+ modelId: stepResult.response.modelId,
436
+ timestamp: stepResult.response.timestamp,
437
+ },
438
+ };
439
+
440
+ await updateStepResult(stepState.stepId, {
441
+ duration_ms: durationMs,
442
+ output: JSON.stringify(output),
443
+ usage: stepResult.usage ? JSON.stringify(stepResult.usage) : null,
444
+ error: null,
445
+ raw_request: stepResult.request?.body
446
+ ? JSON.stringify(stepResult.request.body)
447
+ : null,
448
+ raw_response: stepResult.response?.body
449
+ ? JSON.stringify(stepResult.response.body)
450
+ : null,
451
+ });
452
+
453
+ state.stepStates.delete(stepResult.stepNumber);
454
+ },
455
+
456
+ onFinish: async event => {
457
+ const finishEvent = event as { callId: string };
458
+ callStates.delete(finishEvent.callId);
459
+ },
460
+
461
+ onError: async error => {
462
+ const errorObj = error as
463
+ | { callId?: string; error?: unknown }
464
+ | undefined;
465
+ const callId = errorObj?.callId;
466
+ if (!callId) return;
467
+
468
+ const state = callStates.get(callId);
469
+ if (!state) return;
470
+
471
+ const cause = errorObj?.error ?? error;
472
+ const errorMessage =
473
+ cause instanceof Error ? cause.message : String(cause);
474
+
475
+ for (const [, stepState] of state.stepStates) {
476
+ activeSteps.delete(stepState.stepId);
477
+ const durationMs = Date.now() - stepState.startTime;
478
+ await updateStepResult(stepState.stepId, {
479
+ duration_ms: durationMs,
480
+ output: null,
481
+ usage: null,
482
+ error: errorMessage,
483
+ raw_request: null,
484
+ raw_response: null,
485
+ raw_chunks: null,
486
+ });
487
+ }
488
+
489
+ callStates.delete(callId);
490
+ },
491
+
492
+ executeTool: async ({ callId, toolCallId, execute }) => {
493
+ toolContextMap.set(toolCallId, {
494
+ parentCallId: callId,
495
+ parentToolCallId: toolCallId,
496
+ });
497
+
498
+ const previousToolCallId = currentToolCallId;
499
+ currentToolCallId = toolCallId;
500
+
501
+ try {
502
+ return await execute();
503
+ } finally {
504
+ currentToolCallId = previousToolCallId;
505
+ toolContextMap.delete(toolCallId);
506
+ }
507
+ },
508
+ };
509
+
510
+ return integration;
511
+ }
package/src/middleware.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
- type LanguageModelV3FinishReason,
3
- type LanguageModelV3Usage,
4
- type LanguageModelV3Middleware,
5
- type LanguageModelV3StreamPart,
2
+ type LanguageModelV4FinishReason,
3
+ type LanguageModelV4Usage,
4
+ type LanguageModelV4Middleware,
5
+ type LanguageModelV4StreamPart,
6
6
  } from '@ai-sdk/provider';
7
7
  import {
8
8
  createRun,
@@ -97,7 +97,7 @@ const generateRunId = (): string => {
97
97
  * });
98
98
  * ```
99
99
  */
100
- export const devToolsMiddleware = (): LanguageModelV3Middleware => {
100
+ export const devToolsMiddleware = (): LanguageModelV4Middleware => {
101
101
  if (process.env.NODE_ENV === 'production') {
102
102
  throw new Error(
103
103
  '@ai-sdk/devtools should not be used in production. ' +
@@ -125,7 +125,7 @@ export const devToolsMiddleware = (): LanguageModelV3Middleware => {
125
125
  };
126
126
 
127
127
  return {
128
- specificationVersion: 'v3',
128
+ specificationVersion: 'v4',
129
129
 
130
130
  wrapGenerate: async ({ doGenerate, params, model }) => {
131
131
  const startTime = Date.now();
@@ -242,9 +242,9 @@ export const devToolsMiddleware = (): LanguageModelV3Middleware => {
242
242
  const collectedOutput: {
243
243
  textParts: Array<{ id: string; text: string }>;
244
244
  reasoningParts: Array<{ id: string; text: string }>;
245
- toolCalls: LanguageModelV3StreamPart[];
246
- finishReason?: LanguageModelV3FinishReason;
247
- usage?: LanguageModelV3Usage;
245
+ toolCalls: LanguageModelV4StreamPart[];
246
+ finishReason?: LanguageModelV4FinishReason;
247
+ usage?: LanguageModelV4Usage;
248
248
  } = {
249
249
  textParts: [],
250
250
  reasoningParts: [],
@@ -253,7 +253,7 @@ export const devToolsMiddleware = (): LanguageModelV3Middleware => {
253
253
 
254
254
  const currentText: Map<string, string> = new Map();
255
255
  const currentReasoning: Map<string, string> = new Map();
256
- const fullStreamChunks: LanguageModelV3StreamPart[] = [];
256
+ const fullStreamChunks: LanguageModelV4StreamPart[] = [];
257
257
  const rawChunks: unknown[] = [];
258
258
 
259
259
  // Track this step for cleanup on process exit
@@ -266,8 +266,8 @@ export const devToolsMiddleware = (): LanguageModelV3Middleware => {
266
266
  });
267
267
 
268
268
  const transformStream = new TransformStream<
269
- LanguageModelV3StreamPart,
270
- LanguageModelV3StreamPart
269
+ LanguageModelV4StreamPart,
270
+ LanguageModelV4StreamPart
271
271
  >({
272
272
  transform(chunk, controller) {
273
273
  // Separate raw provider chunks from other stream chunks