@ai-sdk/devtools 1.0.0-canary.28 → 1.0.0-canary.30
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/README.md +40 -9
- package/dist/index.js +6 -3
- package/dist/viewer/server.js +72 -13
- package/package.json +2 -2
- package/src/db.ts +52 -11
- package/src/integration.ts +1 -1
- package/src/viewer/server.ts +43 -7
package/README.md
CHANGED
|
@@ -14,20 +14,46 @@ pnpm add @ai-sdk/devtools
|
|
|
14
14
|
|
|
15
15
|
## Requirements
|
|
16
16
|
|
|
17
|
-
- AI SDK
|
|
17
|
+
- AI SDK v7 canary (`ai@canary`)
|
|
18
18
|
- Node.js compatible runtime
|
|
19
19
|
|
|
20
20
|
## Usage
|
|
21
21
|
|
|
22
|
-
### 1.
|
|
22
|
+
### 1. Register the telemetry integration
|
|
23
|
+
|
|
24
|
+
Register `DevToolsTelemetry` globally so it captures all AI SDK calls:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { registerTelemetry } from 'ai';
|
|
28
|
+
import { DevToolsTelemetry } from '@ai-sdk/devtools';
|
|
29
|
+
|
|
30
|
+
registerTelemetry(DevToolsTelemetry());
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Telemetry is enabled automatically once an integration is registered:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { generateText } from 'ai';
|
|
37
|
+
|
|
38
|
+
const result = await generateText({
|
|
39
|
+
model: yourModel,
|
|
40
|
+
prompt: 'What cities are in the United States?',
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
You can also pass the integration to individual calls instead of registering it
|
|
45
|
+
globally:
|
|
23
46
|
|
|
24
47
|
```typescript
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
48
|
+
import { streamText } from 'ai';
|
|
49
|
+
import { DevToolsTelemetry } from '@ai-sdk/devtools';
|
|
27
50
|
|
|
28
|
-
const
|
|
29
|
-
middleware: devToolsMiddleware(),
|
|
51
|
+
const result = streamText({
|
|
30
52
|
model: yourModel,
|
|
53
|
+
prompt: 'Hello!',
|
|
54
|
+
telemetry: {
|
|
55
|
+
integrations: [DevToolsTelemetry()],
|
|
56
|
+
},
|
|
31
57
|
});
|
|
32
58
|
```
|
|
33
59
|
|
|
@@ -39,21 +65,26 @@ npx @ai-sdk/devtools
|
|
|
39
65
|
|
|
40
66
|
Open http://localhost:4983 to view your AI SDK interactions.
|
|
41
67
|
|
|
68
|
+
If you are using a monorepo, start DevTools from the same workspace where your
|
|
69
|
+
AI SDK code runs.
|
|
70
|
+
|
|
42
71
|
## How it works
|
|
43
72
|
|
|
44
|
-
The
|
|
73
|
+
The `DevToolsTelemetry` integration hooks into the AI SDK telemetry lifecycle to
|
|
74
|
+
capture `generateText`, `streamText`, `generateObject`, and `streamObject` calls.
|
|
75
|
+
It captures:
|
|
45
76
|
|
|
46
77
|
- Input parameters and prompts
|
|
47
78
|
- Output content and tool calls
|
|
48
79
|
- Token usage and timing
|
|
49
|
-
- Raw provider
|
|
80
|
+
- Raw provider data
|
|
50
81
|
|
|
51
82
|
Data is stored locally in a JSON file (`.devtools/generations.json`) and served through a web UI.
|
|
52
83
|
|
|
53
84
|
### Data flow
|
|
54
85
|
|
|
55
86
|
```
|
|
56
|
-
AI SDK call
|
|
87
|
+
AI SDK call -> DevToolsTelemetry -> JSON file -> Hono API -> React UI
|
|
57
88
|
```
|
|
58
89
|
|
|
59
90
|
### Key concepts
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// src/db.ts
|
|
2
2
|
import path from "path";
|
|
3
3
|
import fs from "fs";
|
|
4
|
-
var
|
|
5
|
-
var
|
|
4
|
+
var DEVTOOLS_DB_DIR = ".devtools";
|
|
5
|
+
var DEVTOOLS_DB_FILE = "generations.json";
|
|
6
|
+
var MAX_DB_BYTES = 100 * 1024 * 1024;
|
|
7
|
+
var DB_DIR = path.join(process.cwd(), DEVTOOLS_DB_DIR);
|
|
8
|
+
var DB_PATH = path.join(DB_DIR, DEVTOOLS_DB_FILE);
|
|
6
9
|
var DEVTOOLS_PORT = process.env.AI_SDK_DEVTOOLS_PORT ? parseInt(process.env.AI_SDK_DEVTOOLS_PORT) : 4983;
|
|
7
10
|
var notifyServer = (event) => {
|
|
8
11
|
notifyServerAsync(event);
|
|
@@ -596,7 +599,7 @@ function DevToolsTelemetry() {
|
|
|
596
599
|
});
|
|
597
600
|
state.stepStates.delete(stepResult.stepNumber);
|
|
598
601
|
},
|
|
599
|
-
|
|
602
|
+
onObjectStepEnd: async (event) => {
|
|
600
603
|
const stepResult = event;
|
|
601
604
|
const state = callStates.get(stepResult.callId);
|
|
602
605
|
if (!state) return;
|
package/dist/viewer/server.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { serve } from "@hono/node-server";
|
|
3
3
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
4
4
|
import { Hono } from "hono";
|
|
5
|
-
import { cors } from "hono/cors";
|
|
6
5
|
import { streamSSE } from "hono/streaming";
|
|
7
6
|
import path2 from "path";
|
|
8
7
|
import fs2 from "fs";
|
|
@@ -11,8 +10,11 @@ import { fileURLToPath } from "url";
|
|
|
11
10
|
// src/db.ts
|
|
12
11
|
import path from "path";
|
|
13
12
|
import fs from "fs";
|
|
14
|
-
var
|
|
15
|
-
var
|
|
13
|
+
var DEVTOOLS_DB_DIR = ".devtools";
|
|
14
|
+
var DEVTOOLS_DB_FILE = "generations.json";
|
|
15
|
+
var MAX_DB_BYTES = 100 * 1024 * 1024;
|
|
16
|
+
var DB_DIR = path.join(process.cwd(), DEVTOOLS_DB_DIR);
|
|
17
|
+
var DB_PATH = path.join(DB_DIR, DEVTOOLS_DB_FILE);
|
|
16
18
|
var DEVTOOLS_PORT = process.env.AI_SDK_DEVTOOLS_PORT ? parseInt(process.env.AI_SDK_DEVTOOLS_PORT) : 4983;
|
|
17
19
|
var notifyServer = (event) => {
|
|
18
20
|
notifyServerAsync(event);
|
|
@@ -74,14 +76,40 @@ var saveDb = (db) => {
|
|
|
74
76
|
dbCache = db;
|
|
75
77
|
writeDb(db);
|
|
76
78
|
};
|
|
79
|
+
var normalizeDevtoolsDbPath = (dbPath) => {
|
|
80
|
+
const resolvedPath = path.resolve(dbPath);
|
|
81
|
+
const dbDir = path.dirname(resolvedPath);
|
|
82
|
+
if (path.basename(resolvedPath) !== DEVTOOLS_DB_FILE || path.basename(dbDir) !== DEVTOOLS_DB_DIR) {
|
|
83
|
+
return void 0;
|
|
84
|
+
}
|
|
85
|
+
return resolvedPath;
|
|
86
|
+
};
|
|
87
|
+
var validateRemoteDbPath = (dbPath) => {
|
|
88
|
+
if (typeof dbPath !== "string") {
|
|
89
|
+
return void 0;
|
|
90
|
+
}
|
|
91
|
+
const normalizedPath = normalizeDevtoolsDbPath(dbPath);
|
|
92
|
+
if (!normalizedPath) {
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const stats = fs.statSync(normalizedPath);
|
|
97
|
+
if (!stats.isFile() || stats.size > MAX_DB_BYTES) {
|
|
98
|
+
return void 0;
|
|
99
|
+
}
|
|
100
|
+
const realPath = fs.realpathSync(normalizedPath);
|
|
101
|
+
return normalizeDevtoolsDbPath(realPath);
|
|
102
|
+
} catch {
|
|
103
|
+
return void 0;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
77
106
|
var reloadDb = async (remoteDbPath2) => {
|
|
78
|
-
|
|
107
|
+
const validatedRemoteDbPath = validateRemoteDbPath(remoteDbPath2);
|
|
108
|
+
if (validatedRemoteDbPath) {
|
|
79
109
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
110
|
+
const content = fs.readFileSync(validatedRemoteDbPath, "utf-8");
|
|
111
|
+
dbCache = JSON.parse(content);
|
|
112
|
+
return;
|
|
85
113
|
} catch {
|
|
86
114
|
}
|
|
87
115
|
}
|
|
@@ -131,8 +159,32 @@ var isDevMode = devEnv !== void 0 && devEnv !== "false" && devEnv !== "0";
|
|
|
131
159
|
var projectRoot = path2.resolve(__dirname, "../..");
|
|
132
160
|
var clientDir = path2.join(projectRoot, "dist/client");
|
|
133
161
|
var remoteDbPath;
|
|
162
|
+
var viewerPort = 4983;
|
|
134
163
|
var app = new Hono();
|
|
135
|
-
|
|
164
|
+
var getAllowedHosts = () => /* @__PURE__ */ new Set([
|
|
165
|
+
`localhost:${viewerPort}`,
|
|
166
|
+
`127.0.0.1:${viewerPort}`,
|
|
167
|
+
`[::1]:${viewerPort}`
|
|
168
|
+
]);
|
|
169
|
+
var getAllowedOrigins = () => /* @__PURE__ */ new Set([
|
|
170
|
+
`http://localhost:${viewerPort}`,
|
|
171
|
+
`http://127.0.0.1:${viewerPort}`,
|
|
172
|
+
`http://[::1]:${viewerPort}`,
|
|
173
|
+
"http://localhost:5173",
|
|
174
|
+
"http://127.0.0.1:5173",
|
|
175
|
+
"http://[::1]:5173"
|
|
176
|
+
]);
|
|
177
|
+
app.use("/api/*", async (c, next) => {
|
|
178
|
+
const host = c.req.header("host");
|
|
179
|
+
if (!host || !getAllowedHosts().has(host)) {
|
|
180
|
+
return c.text("Forbidden", 403);
|
|
181
|
+
}
|
|
182
|
+
const origin = c.req.header("origin");
|
|
183
|
+
if (origin && !getAllowedOrigins().has(origin)) {
|
|
184
|
+
return c.text("Forbidden", 403);
|
|
185
|
+
}
|
|
186
|
+
await next();
|
|
187
|
+
});
|
|
136
188
|
app.get("/api/runs", async (c) => {
|
|
137
189
|
await reloadDb(remoteDbPath);
|
|
138
190
|
const runs = await getRuns();
|
|
@@ -258,8 +310,12 @@ app.get("/api/events", (c) => {
|
|
|
258
310
|
});
|
|
259
311
|
app.post("/api/notify", async (c) => {
|
|
260
312
|
const body = await c.req.json();
|
|
261
|
-
if (body.dbPath) {
|
|
262
|
-
|
|
313
|
+
if (body.dbPath !== void 0) {
|
|
314
|
+
const validatedDbPath = validateRemoteDbPath(body.dbPath);
|
|
315
|
+
if (!validatedDbPath) {
|
|
316
|
+
return c.text("Invalid dbPath", 400);
|
|
317
|
+
}
|
|
318
|
+
remoteDbPath = validatedDbPath;
|
|
263
319
|
}
|
|
264
320
|
await reloadDb(remoteDbPath);
|
|
265
321
|
broadcastToClients("update", body);
|
|
@@ -306,10 +362,12 @@ app.get("*", async (c) => {
|
|
|
306
362
|
}
|
|
307
363
|
});
|
|
308
364
|
var startViewer = (port = 4983) => {
|
|
365
|
+
viewerPort = port;
|
|
309
366
|
const server = serve(
|
|
310
367
|
{
|
|
311
368
|
fetch: app.fetch,
|
|
312
|
-
port
|
|
369
|
+
port,
|
|
370
|
+
hostname: "localhost"
|
|
313
371
|
},
|
|
314
372
|
() => {
|
|
315
373
|
if (isDevMode) {
|
|
@@ -346,5 +404,6 @@ if (isDirectRun) {
|
|
|
346
404
|
startViewer(port);
|
|
347
405
|
}
|
|
348
406
|
export {
|
|
407
|
+
app,
|
|
349
408
|
startViewer
|
|
350
409
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/devtools",
|
|
3
|
-
"version": "1.0.0-canary.
|
|
3
|
+
"version": "1.0.0-canary.30",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"vite": "^6.4.2",
|
|
57
57
|
"vitest": "^4.1.6",
|
|
58
58
|
"zod": "3.25.76",
|
|
59
|
-
"ai": "7.0.0-canary.
|
|
59
|
+
"ai": "7.0.0-canary.172"
|
|
60
60
|
},
|
|
61
61
|
"publishConfig": {
|
|
62
62
|
"access": "public",
|
package/src/db.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
const
|
|
4
|
+
const DEVTOOLS_DB_DIR = '.devtools';
|
|
5
|
+
const DEVTOOLS_DB_FILE = 'generations.json';
|
|
6
|
+
// Cap how many bytes we read from a remote database file. Guards against a
|
|
7
|
+
// synchronous hang / OOM if the file is enormous.
|
|
8
|
+
const MAX_DB_BYTES = 100 * 1024 * 1024; // 100 MB
|
|
9
|
+
const DB_DIR = path.join(process.cwd(), DEVTOOLS_DB_DIR);
|
|
10
|
+
const DB_PATH = path.join(DB_DIR, DEVTOOLS_DB_FILE);
|
|
6
11
|
const DEVTOOLS_PORT = process.env.AI_SDK_DEVTOOLS_PORT
|
|
7
12
|
? parseInt(process.env.AI_SDK_DEVTOOLS_PORT)
|
|
8
13
|
: 4983;
|
|
@@ -140,21 +145,57 @@ const saveDb = (db: Database): void => {
|
|
|
140
145
|
writeDb(db);
|
|
141
146
|
};
|
|
142
147
|
|
|
148
|
+
const normalizeDevtoolsDbPath = (dbPath: string): string | undefined => {
|
|
149
|
+
const resolvedPath = path.resolve(dbPath);
|
|
150
|
+
const dbDir = path.dirname(resolvedPath);
|
|
151
|
+
|
|
152
|
+
if (
|
|
153
|
+
path.basename(resolvedPath) !== DEVTOOLS_DB_FILE ||
|
|
154
|
+
path.basename(dbDir) !== DEVTOOLS_DB_DIR
|
|
155
|
+
) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return resolvedPath;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const validateRemoteDbPath = (dbPath: unknown): string | undefined => {
|
|
163
|
+
if (typeof dbPath !== 'string') {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const normalizedPath = normalizeDevtoolsDbPath(dbPath);
|
|
168
|
+
if (!normalizedPath) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const stats = fs.statSync(normalizedPath);
|
|
174
|
+
if (!stats.isFile() || stats.size > MAX_DB_BYTES) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const realPath = fs.realpathSync(normalizedPath);
|
|
179
|
+
return normalizeDevtoolsDbPath(realPath);
|
|
180
|
+
} catch {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
143
185
|
/**
|
|
144
186
|
* Reload the database from disk.
|
|
145
187
|
* Used by the viewer server to pick up changes made by the middleware/integration.
|
|
146
|
-
* When a remote dbPath is provided (from a notify POST), reads from that
|
|
147
|
-
* instead of the local CWD-based path, so the viewer works regardless of
|
|
148
|
-
* it was started.
|
|
188
|
+
* When a valid remote dbPath is provided (from a notify POST), reads from that
|
|
189
|
+
* path instead of the local CWD-based path, so the viewer works regardless of
|
|
190
|
+
* where it was started.
|
|
149
191
|
*/
|
|
150
192
|
export const reloadDb = async (remoteDbPath?: string): Promise<void> => {
|
|
151
|
-
|
|
193
|
+
const validatedRemoteDbPath = validateRemoteDbPath(remoteDbPath);
|
|
194
|
+
if (validatedRemoteDbPath) {
|
|
152
195
|
try {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
196
|
+
const content = fs.readFileSync(validatedRemoteDbPath, 'utf-8');
|
|
197
|
+
dbCache = JSON.parse(content);
|
|
198
|
+
return;
|
|
158
199
|
} catch {
|
|
159
200
|
// Fall through to default
|
|
160
201
|
}
|
package/src/integration.ts
CHANGED
|
@@ -350,7 +350,7 @@ export function DevToolsTelemetry(): Telemetry {
|
|
|
350
350
|
state.stepStates.delete(stepResult.stepNumber);
|
|
351
351
|
},
|
|
352
352
|
|
|
353
|
-
|
|
353
|
+
onObjectStepEnd: async event => {
|
|
354
354
|
const stepResult = event as GenerateObjectStepEndEvent;
|
|
355
355
|
|
|
356
356
|
const state = callStates.get(stepResult.callId);
|
package/src/viewer/server.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { serve } from '@hono/node-server';
|
|
2
2
|
import { serveStatic } from '@hono/node-server/serve-static';
|
|
3
3
|
import { Hono } from 'hono';
|
|
4
|
-
import { cors } from 'hono/cors';
|
|
5
4
|
import { streamSSE } from 'hono/streaming';
|
|
6
5
|
import path from 'path';
|
|
7
6
|
import fs from 'fs';
|
|
@@ -12,6 +11,7 @@ import {
|
|
|
12
11
|
getStepsForRun,
|
|
13
12
|
clearDatabase,
|
|
14
13
|
reloadDb,
|
|
14
|
+
validateRemoteDbPath,
|
|
15
15
|
} from '../db.js';
|
|
16
16
|
|
|
17
17
|
// SSE client management
|
|
@@ -56,11 +56,40 @@ const clientDir = path.join(projectRoot, 'dist/client');
|
|
|
56
56
|
// Track the DB path from the most recent notify call so all reads
|
|
57
57
|
// use the correct file, even when the viewer runs in a different CWD.
|
|
58
58
|
let remoteDbPath: string | undefined;
|
|
59
|
+
let viewerPort = 4983;
|
|
60
|
+
|
|
61
|
+
export const app = new Hono();
|
|
62
|
+
|
|
63
|
+
const getAllowedHosts = () =>
|
|
64
|
+
new Set([
|
|
65
|
+
`localhost:${viewerPort}`,
|
|
66
|
+
`127.0.0.1:${viewerPort}`,
|
|
67
|
+
`[::1]:${viewerPort}`,
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
const getAllowedOrigins = () =>
|
|
71
|
+
new Set([
|
|
72
|
+
`http://localhost:${viewerPort}`,
|
|
73
|
+
`http://127.0.0.1:${viewerPort}`,
|
|
74
|
+
`http://[::1]:${viewerPort}`,
|
|
75
|
+
'http://localhost:5173',
|
|
76
|
+
'http://127.0.0.1:5173',
|
|
77
|
+
'http://[::1]:5173',
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
app.use('/api/*', async (c, next) => {
|
|
81
|
+
const host = c.req.header('host');
|
|
82
|
+
if (!host || !getAllowedHosts().has(host)) {
|
|
83
|
+
return c.text('Forbidden', 403);
|
|
84
|
+
}
|
|
59
85
|
|
|
60
|
-
const
|
|
86
|
+
const origin = c.req.header('origin');
|
|
87
|
+
if (origin && !getAllowedOrigins().has(origin)) {
|
|
88
|
+
return c.text('Forbidden', 403);
|
|
89
|
+
}
|
|
61
90
|
|
|
62
|
-
|
|
63
|
-
|
|
91
|
+
await next();
|
|
92
|
+
});
|
|
64
93
|
|
|
65
94
|
// API Routes
|
|
66
95
|
app.get('/api/runs', async c => {
|
|
@@ -230,9 +259,13 @@ app.get('/api/events', c => {
|
|
|
230
259
|
|
|
231
260
|
// Notification endpoint (called by middleware/integration)
|
|
232
261
|
app.post('/api/notify', async c => {
|
|
233
|
-
const body = await c.req.json()
|
|
234
|
-
if (body.dbPath) {
|
|
235
|
-
|
|
262
|
+
const body = (await c.req.json()) as Record<string, unknown>;
|
|
263
|
+
if (body.dbPath !== undefined) {
|
|
264
|
+
const validatedDbPath = validateRemoteDbPath(body.dbPath);
|
|
265
|
+
if (!validatedDbPath) {
|
|
266
|
+
return c.text('Invalid dbPath', 400);
|
|
267
|
+
}
|
|
268
|
+
remoteDbPath = validatedDbPath;
|
|
236
269
|
}
|
|
237
270
|
// Reload database from disk, using the remote dbPath if provided so the
|
|
238
271
|
// viewer works even when started from a different directory than the app.
|
|
@@ -288,10 +321,13 @@ app.get('*', async c => {
|
|
|
288
321
|
});
|
|
289
322
|
|
|
290
323
|
export const startViewer = (port = 4983) => {
|
|
324
|
+
viewerPort = port;
|
|
325
|
+
|
|
291
326
|
const server = serve(
|
|
292
327
|
{
|
|
293
328
|
fetch: app.fetch,
|
|
294
329
|
port,
|
|
330
|
+
hostname: 'localhost',
|
|
295
331
|
},
|
|
296
332
|
() => {
|
|
297
333
|
if (isDevMode) {
|