@ai-sdk/devtools 0.0.6 → 0.0.7
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 +3 -2
- package/src/db.ts +242 -0
- package/src/index.ts +1 -0
- package/src/middleware.ts +392 -0
- package/src/viewer/client/app.tsx +2257 -0
- package/src/viewer/client/components/icons.tsx +29 -0
- package/src/viewer/client/components/ui/badge.tsx +46 -0
- package/src/viewer/client/components/ui/button.tsx +60 -0
- package/src/viewer/client/components/ui/card.tsx +92 -0
- package/src/viewer/client/components/ui/collapsible.tsx +31 -0
- package/src/viewer/client/components/ui/drawer.tsx +133 -0
- package/src/viewer/client/components/ui/scroll-area.tsx +58 -0
- package/src/viewer/client/components/ui/tooltip.tsx +58 -0
- package/src/viewer/client/index.html +18 -0
- package/src/viewer/client/lib/utils.ts +6 -0
- package/src/viewer/client/main.tsx +6 -0
- package/src/viewer/client/styles.css +145 -0
- package/src/viewer/server.ts +286 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/devtools",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"dist",
|
|
20
|
+
"src",
|
|
20
21
|
"bin"
|
|
21
22
|
],
|
|
22
23
|
"dependencies": {
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
"vaul": "^1.1.2",
|
|
51
52
|
"vite": "^6.0.3",
|
|
52
53
|
"zod": "3.25.76",
|
|
53
|
-
"ai": "6.0.
|
|
54
|
+
"ai": "6.0.46"
|
|
54
55
|
},
|
|
55
56
|
"scripts": {
|
|
56
57
|
"dev": "concurrently -k \"pnpm dev:api\" \"pnpm dev:client\"",
|
package/src/db.ts
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
|
|
4
|
+
const DB_DIR = path.join(process.cwd(), '.devtools');
|
|
5
|
+
const DB_PATH = path.join(DB_DIR, 'generations.json');
|
|
6
|
+
const DEVTOOLS_PORT = process.env.AI_SDK_DEVTOOLS_PORT
|
|
7
|
+
? parseInt(process.env.AI_SDK_DEVTOOLS_PORT)
|
|
8
|
+
: 4983;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Notify the devtools server that data has changed.
|
|
12
|
+
* Fire-and-forget: doesn't block, ignores errors if server isn't running.
|
|
13
|
+
*/
|
|
14
|
+
const notifyServer = (event: 'run' | 'step' | 'step-update' | 'clear') => {
|
|
15
|
+
notifyServerAsync(event);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Notify the devtools server and wait for the request to complete.
|
|
20
|
+
* Used during process cleanup to ensure notifications are sent before exit.
|
|
21
|
+
*/
|
|
22
|
+
export const notifyServerAsync = async (
|
|
23
|
+
event: 'run' | 'step' | 'step-update' | 'clear',
|
|
24
|
+
): Promise<void> => {
|
|
25
|
+
try {
|
|
26
|
+
await fetch(`http://localhost:${DEVTOOLS_PORT}/api/notify`, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: { 'Content-Type': 'application/json' },
|
|
29
|
+
body: JSON.stringify({ event, timestamp: Date.now() }),
|
|
30
|
+
});
|
|
31
|
+
} catch {
|
|
32
|
+
// Ignore errors - server might not be running
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export interface Run {
|
|
37
|
+
id: string;
|
|
38
|
+
started_at: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface Step {
|
|
42
|
+
id: string;
|
|
43
|
+
run_id: string;
|
|
44
|
+
step_number: number;
|
|
45
|
+
type: 'generate' | 'stream';
|
|
46
|
+
model_id: string;
|
|
47
|
+
provider: string | null;
|
|
48
|
+
started_at: string;
|
|
49
|
+
duration_ms: number | null;
|
|
50
|
+
input: string;
|
|
51
|
+
output: string | null;
|
|
52
|
+
usage: string | null;
|
|
53
|
+
error: string | null;
|
|
54
|
+
raw_request: string | null;
|
|
55
|
+
raw_response: string | null;
|
|
56
|
+
raw_chunks: string | null;
|
|
57
|
+
provider_options: string | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface StepResult {
|
|
61
|
+
duration_ms: number;
|
|
62
|
+
output: string | null;
|
|
63
|
+
usage: string | null;
|
|
64
|
+
error: string | null;
|
|
65
|
+
raw_request?: string | null;
|
|
66
|
+
raw_response?: string | null;
|
|
67
|
+
raw_chunks?: string | null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface Database {
|
|
71
|
+
runs: Run[];
|
|
72
|
+
steps: Step[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Ensure .devtools is in .gitignore.
|
|
77
|
+
* Only writes if .gitignore exists and doesn't already contain .devtools.
|
|
78
|
+
*/
|
|
79
|
+
const ensureGitignore = (): void => {
|
|
80
|
+
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
81
|
+
|
|
82
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
87
|
+
const lines = content.split('\n');
|
|
88
|
+
|
|
89
|
+
// Check if .devtools is already ignored (exact match or with trailing slash)
|
|
90
|
+
const alreadyIgnored = lines.some(
|
|
91
|
+
line => line.trim() === '.devtools' || line.trim() === '.devtools/',
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
if (!alreadyIgnored) {
|
|
95
|
+
const newContent = content.endsWith('\n')
|
|
96
|
+
? `${content}.devtools\n`
|
|
97
|
+
: `${content}\n.devtools\n`;
|
|
98
|
+
fs.writeFileSync(gitignorePath, newContent);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const readDb = (): Database => {
|
|
103
|
+
try {
|
|
104
|
+
if (fs.existsSync(DB_PATH)) {
|
|
105
|
+
const content = fs.readFileSync(DB_PATH, 'utf-8');
|
|
106
|
+
return JSON.parse(content);
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// If file is corrupted, start fresh
|
|
110
|
+
}
|
|
111
|
+
return { runs: [], steps: [] };
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const writeDb = (db: Database): void => {
|
|
115
|
+
const isFirstRun = !fs.existsSync(DB_DIR);
|
|
116
|
+
|
|
117
|
+
if (isFirstRun) {
|
|
118
|
+
fs.mkdirSync(DB_DIR, { recursive: true });
|
|
119
|
+
ensureGitignore();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fs.writeFileSync(DB_PATH, JSON.stringify(db, null, 2));
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// In-memory cache for performance
|
|
126
|
+
let dbCache: Database | null = null;
|
|
127
|
+
|
|
128
|
+
const getDb = (): Database => {
|
|
129
|
+
if (!dbCache) {
|
|
130
|
+
dbCache = readDb();
|
|
131
|
+
}
|
|
132
|
+
return dbCache;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const saveDb = (db: Database): void => {
|
|
136
|
+
dbCache = db;
|
|
137
|
+
writeDb(db);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Reload the database from disk.
|
|
142
|
+
* Used by the viewer server to pick up changes made by the middleware.
|
|
143
|
+
*/
|
|
144
|
+
export const reloadDb = async (): Promise<void> => {
|
|
145
|
+
dbCache = readDb();
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const createRun = async (id: string): Promise<Run> => {
|
|
149
|
+
const db = getDb();
|
|
150
|
+
const started_at = new Date().toISOString();
|
|
151
|
+
|
|
152
|
+
// Check if run already exists
|
|
153
|
+
const existing = db.runs.find(r => r.id === id);
|
|
154
|
+
if (existing) {
|
|
155
|
+
return existing;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const run: Run = { id, started_at };
|
|
159
|
+
db.runs.push(run);
|
|
160
|
+
saveDb(db);
|
|
161
|
+
notifyServer('run');
|
|
162
|
+
return run;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export const createStep = async (
|
|
166
|
+
step: Omit<
|
|
167
|
+
Step,
|
|
168
|
+
| 'duration_ms'
|
|
169
|
+
| 'output'
|
|
170
|
+
| 'usage'
|
|
171
|
+
| 'error'
|
|
172
|
+
| 'raw_request'
|
|
173
|
+
| 'raw_response'
|
|
174
|
+
| 'raw_chunks'
|
|
175
|
+
>,
|
|
176
|
+
): Promise<void> => {
|
|
177
|
+
const db = getDb();
|
|
178
|
+
const newStep: Step = {
|
|
179
|
+
...step,
|
|
180
|
+
duration_ms: null,
|
|
181
|
+
output: null,
|
|
182
|
+
usage: null,
|
|
183
|
+
error: null,
|
|
184
|
+
raw_request: null,
|
|
185
|
+
raw_response: null,
|
|
186
|
+
raw_chunks: null,
|
|
187
|
+
};
|
|
188
|
+
db.steps.push(newStep);
|
|
189
|
+
saveDb(db);
|
|
190
|
+
notifyServer('step');
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export const updateStepResult = async (
|
|
194
|
+
stepId: string,
|
|
195
|
+
result: StepResult,
|
|
196
|
+
): Promise<void> => {
|
|
197
|
+
const db = getDb();
|
|
198
|
+
const step = db.steps.find(s => s.id === stepId);
|
|
199
|
+
if (step) {
|
|
200
|
+
step.duration_ms = result.duration_ms;
|
|
201
|
+
step.output = result.output;
|
|
202
|
+
step.usage = result.usage;
|
|
203
|
+
step.error = result.error;
|
|
204
|
+
step.raw_request = result.raw_request ?? null;
|
|
205
|
+
step.raw_response = result.raw_response ?? null;
|
|
206
|
+
step.raw_chunks = result.raw_chunks ?? null;
|
|
207
|
+
saveDb(db);
|
|
208
|
+
notifyServer('step-update');
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export const getRuns = async (): Promise<Run[]> => {
|
|
213
|
+
const db = getDb();
|
|
214
|
+
// Return runs sorted by started_at DESC
|
|
215
|
+
return [...db.runs].sort(
|
|
216
|
+
(a, b) =>
|
|
217
|
+
new Date(b.started_at).getTime() - new Date(a.started_at).getTime(),
|
|
218
|
+
);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
export const getStepsForRun = async (runId: string): Promise<Step[]> => {
|
|
222
|
+
const db = getDb();
|
|
223
|
+
return db.steps
|
|
224
|
+
.filter(s => s.run_id === runId)
|
|
225
|
+
.sort((a, b) => a.step_number - b.step_number);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export const getRunWithSteps = async (
|
|
229
|
+
runId: string,
|
|
230
|
+
): Promise<{ run: Run; steps: Step[] } | null> => {
|
|
231
|
+
const db = getDb();
|
|
232
|
+
const run = db.runs.find(r => r.id === runId);
|
|
233
|
+
if (!run) return null;
|
|
234
|
+
const steps = await getStepsForRun(runId);
|
|
235
|
+
return { run, steps };
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export const clearDatabase = async (): Promise<void> => {
|
|
239
|
+
const db: Database = { runs: [], steps: [] };
|
|
240
|
+
saveDb(db);
|
|
241
|
+
notifyServer('clear');
|
|
242
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { devToolsMiddleware } from './middleware.js';
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type LanguageModelV3FinishReason,
|
|
3
|
+
type LanguageModelV3Usage,
|
|
4
|
+
type LanguageModelV3Middleware,
|
|
5
|
+
type LanguageModelV3StreamPart,
|
|
6
|
+
} from '@ai-sdk/provider';
|
|
7
|
+
import {
|
|
8
|
+
createRun,
|
|
9
|
+
createStep,
|
|
10
|
+
updateStepResult,
|
|
11
|
+
notifyServerAsync,
|
|
12
|
+
} from './db.js';
|
|
13
|
+
|
|
14
|
+
const generateId = () => crypto.randomUUID();
|
|
15
|
+
|
|
16
|
+
// Track active streaming steps for cleanup on process exit
|
|
17
|
+
const activeSteps = new Map<
|
|
18
|
+
string,
|
|
19
|
+
{
|
|
20
|
+
startTime: number;
|
|
21
|
+
collectedOutput: unknown;
|
|
22
|
+
request: unknown;
|
|
23
|
+
fullStreamChunks: unknown[];
|
|
24
|
+
rawChunks: unknown[];
|
|
25
|
+
}
|
|
26
|
+
>();
|
|
27
|
+
|
|
28
|
+
// Handle process termination signals
|
|
29
|
+
let signalHandlersRegistered = false;
|
|
30
|
+
const registerSignalHandlers = () => {
|
|
31
|
+
if (signalHandlersRegistered) return;
|
|
32
|
+
signalHandlersRegistered = true;
|
|
33
|
+
|
|
34
|
+
const cleanup = async () => {
|
|
35
|
+
if (activeSteps.size === 0) return;
|
|
36
|
+
|
|
37
|
+
const promises = Array.from(activeSteps.entries()).map(
|
|
38
|
+
async ([stepId, data]) => {
|
|
39
|
+
const durationMs = Date.now() - data.startTime;
|
|
40
|
+
await updateStepResult(stepId, {
|
|
41
|
+
duration_ms: durationMs,
|
|
42
|
+
output: JSON.stringify(data.collectedOutput),
|
|
43
|
+
usage: null,
|
|
44
|
+
error: 'Request aborted',
|
|
45
|
+
raw_request:
|
|
46
|
+
data.request &&
|
|
47
|
+
typeof data.request === 'object' &&
|
|
48
|
+
'body' in data.request
|
|
49
|
+
? JSON.stringify((data.request as { body: unknown }).body)
|
|
50
|
+
: null,
|
|
51
|
+
raw_response: JSON.stringify(data.fullStreamChunks),
|
|
52
|
+
raw_chunks: JSON.stringify(data.rawChunks),
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
await Promise.all(promises);
|
|
57
|
+
|
|
58
|
+
// Wait for the server notification to complete before process exits
|
|
59
|
+
await notifyServerAsync('step-update');
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
process.on('SIGINT', () => {
|
|
63
|
+
cleanup().then(() => process.exit(130));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
process.on('SIGTERM', () => {
|
|
67
|
+
cleanup().then(() => process.exit(143));
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Generate a unique run ID with timestamp prefix for sorting.
|
|
73
|
+
*/
|
|
74
|
+
const generateRunId = (): string => {
|
|
75
|
+
const now = new Date();
|
|
76
|
+
const timestamp = now
|
|
77
|
+
.toISOString()
|
|
78
|
+
.replace(/[-:T.Z]/g, '')
|
|
79
|
+
.slice(0, 17);
|
|
80
|
+
const uniqueId = crypto.randomUUID().slice(0, 8);
|
|
81
|
+
return `${timestamp}-${uniqueId}`;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Factory function that creates a devtools middleware instance.
|
|
86
|
+
* Each call generates a unique run ID, so all steps within a single
|
|
87
|
+
* streamText/generateText call share the same run.
|
|
88
|
+
*
|
|
89
|
+
* Usage:
|
|
90
|
+
* ```ts
|
|
91
|
+
* const result = streamText({
|
|
92
|
+
* model: wrapLanguageModel({
|
|
93
|
+
* middleware: devToolsMiddleware(),
|
|
94
|
+
* model: yourModel,
|
|
95
|
+
* }),
|
|
96
|
+
* prompt: "...",
|
|
97
|
+
* });
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export const devToolsMiddleware = (): LanguageModelV3Middleware => {
|
|
101
|
+
if (process.env.NODE_ENV === 'production') {
|
|
102
|
+
throw new Error(
|
|
103
|
+
'@ai-sdk/devtools should not be used in production. ' +
|
|
104
|
+
'Remove devToolsMiddleware from your model configuration for production builds.',
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Register signal handlers once for cleanup on process exit
|
|
109
|
+
registerSignalHandlers();
|
|
110
|
+
|
|
111
|
+
const runId = generateRunId();
|
|
112
|
+
let runCreated = false;
|
|
113
|
+
let stepCounter = 0;
|
|
114
|
+
|
|
115
|
+
const ensureRunCreated = async () => {
|
|
116
|
+
if (!runCreated) {
|
|
117
|
+
await createRun(runId);
|
|
118
|
+
runCreated = true;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const getNextStepNumber = () => {
|
|
123
|
+
stepCounter++;
|
|
124
|
+
return stepCounter;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
specificationVersion: 'v3',
|
|
129
|
+
|
|
130
|
+
wrapGenerate: async ({ doGenerate, params, model }) => {
|
|
131
|
+
const startTime = Date.now();
|
|
132
|
+
const stepId = generateId();
|
|
133
|
+
const stepNumber = getNextStepNumber();
|
|
134
|
+
await ensureRunCreated();
|
|
135
|
+
|
|
136
|
+
// Log step start
|
|
137
|
+
await createStep({
|
|
138
|
+
id: stepId,
|
|
139
|
+
run_id: runId,
|
|
140
|
+
step_number: stepNumber,
|
|
141
|
+
type: 'generate',
|
|
142
|
+
model_id: model.modelId,
|
|
143
|
+
// @ts-expect-error broken type
|
|
144
|
+
provider: model.config?.provider,
|
|
145
|
+
started_at: new Date().toISOString(),
|
|
146
|
+
input: JSON.stringify({
|
|
147
|
+
prompt: params.prompt,
|
|
148
|
+
tools: params.tools,
|
|
149
|
+
toolChoice: params.toolChoice,
|
|
150
|
+
maxOutputTokens: params.maxOutputTokens,
|
|
151
|
+
temperature: params.temperature,
|
|
152
|
+
topP: params.topP,
|
|
153
|
+
topK: params.topK,
|
|
154
|
+
presencePenalty: params.presencePenalty,
|
|
155
|
+
frequencyPenalty: params.frequencyPenalty,
|
|
156
|
+
seed: params.seed,
|
|
157
|
+
responseFormat: params.responseFormat,
|
|
158
|
+
}),
|
|
159
|
+
provider_options: params.providerOptions
|
|
160
|
+
? JSON.stringify(params.providerOptions)
|
|
161
|
+
: null,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const result = await doGenerate();
|
|
166
|
+
const durationMs = Date.now() - startTime;
|
|
167
|
+
|
|
168
|
+
await updateStepResult(stepId, {
|
|
169
|
+
duration_ms: durationMs,
|
|
170
|
+
output: JSON.stringify({
|
|
171
|
+
content: result.content,
|
|
172
|
+
finishReason: result.finishReason,
|
|
173
|
+
response: result.response,
|
|
174
|
+
}),
|
|
175
|
+
usage: result.usage ? JSON.stringify(result.usage) : null,
|
|
176
|
+
error: null,
|
|
177
|
+
raw_request: result.request?.body
|
|
178
|
+
? JSON.stringify(result.request.body)
|
|
179
|
+
: null,
|
|
180
|
+
raw_response: result.response?.body
|
|
181
|
+
? JSON.stringify(result.response.body)
|
|
182
|
+
: null,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return result;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
const durationMs = Date.now() - startTime;
|
|
188
|
+
await updateStepResult(stepId, {
|
|
189
|
+
duration_ms: durationMs,
|
|
190
|
+
output: null,
|
|
191
|
+
usage: null,
|
|
192
|
+
error: error instanceof Error ? error.message : String(error),
|
|
193
|
+
raw_request: null,
|
|
194
|
+
raw_response: null,
|
|
195
|
+
});
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
wrapStream: async ({ doStream, params, model }) => {
|
|
201
|
+
const startTime = Date.now();
|
|
202
|
+
const stepId = generateId();
|
|
203
|
+
const stepNumber = getNextStepNumber();
|
|
204
|
+
await ensureRunCreated();
|
|
205
|
+
|
|
206
|
+
// Store original setting before overriding
|
|
207
|
+
const userRequestedRawChunks = params.includeRawChunks === true;
|
|
208
|
+
params.includeRawChunks = true;
|
|
209
|
+
|
|
210
|
+
// Log step start
|
|
211
|
+
await createStep({
|
|
212
|
+
id: stepId,
|
|
213
|
+
run_id: runId,
|
|
214
|
+
step_number: stepNumber,
|
|
215
|
+
type: 'stream',
|
|
216
|
+
model_id: model.modelId,
|
|
217
|
+
// @ts-expect-error broken type
|
|
218
|
+
provider: model.config?.provider,
|
|
219
|
+
started_at: new Date().toISOString(),
|
|
220
|
+
input: JSON.stringify({
|
|
221
|
+
prompt: params.prompt,
|
|
222
|
+
tools: params.tools,
|
|
223
|
+
toolChoice: params.toolChoice,
|
|
224
|
+
maxOutputTokens: params.maxOutputTokens,
|
|
225
|
+
temperature: params.temperature,
|
|
226
|
+
topP: params.topP,
|
|
227
|
+
topK: params.topK,
|
|
228
|
+
presencePenalty: params.presencePenalty,
|
|
229
|
+
frequencyPenalty: params.frequencyPenalty,
|
|
230
|
+
seed: params.seed,
|
|
231
|
+
responseFormat: params.responseFormat,
|
|
232
|
+
}),
|
|
233
|
+
provider_options: params.providerOptions
|
|
234
|
+
? JSON.stringify(params.providerOptions)
|
|
235
|
+
: null,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const { stream, request, response, ...rest } = await doStream();
|
|
240
|
+
|
|
241
|
+
// Collect stream output for logging
|
|
242
|
+
const collectedOutput: {
|
|
243
|
+
textParts: Array<{ id: string; text: string }>;
|
|
244
|
+
reasoningParts: Array<{ id: string; text: string }>;
|
|
245
|
+
toolCalls: LanguageModelV3StreamPart[];
|
|
246
|
+
finishReason?: LanguageModelV3FinishReason;
|
|
247
|
+
usage?: LanguageModelV3Usage;
|
|
248
|
+
} = {
|
|
249
|
+
textParts: [],
|
|
250
|
+
reasoningParts: [],
|
|
251
|
+
toolCalls: [],
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const currentText: Map<string, string> = new Map();
|
|
255
|
+
const currentReasoning: Map<string, string> = new Map();
|
|
256
|
+
const fullStreamChunks: LanguageModelV3StreamPart[] = [];
|
|
257
|
+
const rawChunks: unknown[] = [];
|
|
258
|
+
|
|
259
|
+
// Track this step for cleanup on process exit
|
|
260
|
+
activeSteps.set(stepId, {
|
|
261
|
+
startTime,
|
|
262
|
+
collectedOutput,
|
|
263
|
+
request,
|
|
264
|
+
fullStreamChunks,
|
|
265
|
+
rawChunks,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const transformStream = new TransformStream<
|
|
269
|
+
LanguageModelV3StreamPart,
|
|
270
|
+
LanguageModelV3StreamPart
|
|
271
|
+
>({
|
|
272
|
+
transform(chunk, controller) {
|
|
273
|
+
// Separate raw provider chunks from other stream chunks
|
|
274
|
+
if (chunk.type === 'raw') {
|
|
275
|
+
// Store just the unwrapped rawValue for cleaner data
|
|
276
|
+
rawChunks.push(chunk.rawValue);
|
|
277
|
+
// Only pass raw chunks through if user originally requested them
|
|
278
|
+
if (userRequestedRawChunks) {
|
|
279
|
+
controller.enqueue(chunk);
|
|
280
|
+
}
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Collect all non-raw chunks for full stream logging
|
|
285
|
+
fullStreamChunks.push(chunk);
|
|
286
|
+
|
|
287
|
+
// Collect relevant data from stream
|
|
288
|
+
switch (chunk.type) {
|
|
289
|
+
case 'text-start':
|
|
290
|
+
currentText.set(chunk.id, '');
|
|
291
|
+
break;
|
|
292
|
+
case 'text-delta':
|
|
293
|
+
currentText.set(
|
|
294
|
+
chunk.id,
|
|
295
|
+
(currentText.get(chunk.id) ?? '') + chunk.delta,
|
|
296
|
+
);
|
|
297
|
+
break;
|
|
298
|
+
case 'text-end':
|
|
299
|
+
collectedOutput.textParts.push({
|
|
300
|
+
id: chunk.id,
|
|
301
|
+
text: currentText.get(chunk.id) ?? '',
|
|
302
|
+
});
|
|
303
|
+
break;
|
|
304
|
+
case 'reasoning-start':
|
|
305
|
+
currentReasoning.set(chunk.id, '');
|
|
306
|
+
break;
|
|
307
|
+
case 'reasoning-delta':
|
|
308
|
+
currentReasoning.set(
|
|
309
|
+
chunk.id,
|
|
310
|
+
(currentReasoning.get(chunk.id) ?? '') + chunk.delta,
|
|
311
|
+
);
|
|
312
|
+
break;
|
|
313
|
+
case 'reasoning-end':
|
|
314
|
+
collectedOutput.reasoningParts.push({
|
|
315
|
+
id: chunk.id,
|
|
316
|
+
text: currentReasoning.get(chunk.id) ?? '',
|
|
317
|
+
});
|
|
318
|
+
break;
|
|
319
|
+
case 'tool-call':
|
|
320
|
+
collectedOutput.toolCalls.push(chunk);
|
|
321
|
+
break;
|
|
322
|
+
case 'finish':
|
|
323
|
+
collectedOutput.finishReason = chunk.finishReason;
|
|
324
|
+
collectedOutput.usage = chunk.usage;
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
controller.enqueue(chunk);
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
async flush() {
|
|
332
|
+
// Remove from active tracking - stream completed normally
|
|
333
|
+
activeSteps.delete(stepId);
|
|
334
|
+
|
|
335
|
+
const durationMs = Date.now() - startTime;
|
|
336
|
+
await updateStepResult(stepId, {
|
|
337
|
+
duration_ms: durationMs,
|
|
338
|
+
output: JSON.stringify(collectedOutput),
|
|
339
|
+
usage: collectedOutput.usage
|
|
340
|
+
? JSON.stringify(collectedOutput.usage)
|
|
341
|
+
: null,
|
|
342
|
+
error: null,
|
|
343
|
+
raw_request: request?.body ? JSON.stringify(request.body) : null,
|
|
344
|
+
raw_response: JSON.stringify(fullStreamChunks),
|
|
345
|
+
raw_chunks: JSON.stringify(rawChunks),
|
|
346
|
+
});
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
// @ts-expect-error - cancel is valid per WHATWG Streams spec but missing from TS types
|
|
350
|
+
async cancel() {
|
|
351
|
+
// Remove from active tracking - stream was cancelled
|
|
352
|
+
activeSteps.delete(stepId);
|
|
353
|
+
|
|
354
|
+
const durationMs = Date.now() - startTime;
|
|
355
|
+
await updateStepResult(stepId, {
|
|
356
|
+
duration_ms: durationMs,
|
|
357
|
+
output: JSON.stringify(collectedOutput),
|
|
358
|
+
usage: collectedOutput.usage
|
|
359
|
+
? JSON.stringify(collectedOutput.usage)
|
|
360
|
+
: null,
|
|
361
|
+
error: 'Request aborted',
|
|
362
|
+
raw_request: request?.body ? JSON.stringify(request.body) : null,
|
|
363
|
+
raw_response: JSON.stringify(fullStreamChunks),
|
|
364
|
+
raw_chunks: JSON.stringify(rawChunks),
|
|
365
|
+
});
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
stream: stream.pipeThrough(transformStream),
|
|
371
|
+
request,
|
|
372
|
+
response,
|
|
373
|
+
...rest,
|
|
374
|
+
};
|
|
375
|
+
} catch (error) {
|
|
376
|
+
activeSteps.delete(stepId);
|
|
377
|
+
|
|
378
|
+
const durationMs = Date.now() - startTime;
|
|
379
|
+
await updateStepResult(stepId, {
|
|
380
|
+
duration_ms: durationMs,
|
|
381
|
+
output: null,
|
|
382
|
+
usage: null,
|
|
383
|
+
error: error instanceof Error ? error.message : String(error),
|
|
384
|
+
raw_request: null,
|
|
385
|
+
raw_response: null,
|
|
386
|
+
raw_chunks: null,
|
|
387
|
+
});
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
};
|