@ai-sdk/workflow 0.0.0-07500555-20260730161446

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,672 @@
1
+ import type {
2
+ LanguageModelV4CallOptions,
3
+ LanguageModelV4Prompt,
4
+ LanguageModelV4ToolResultPart,
5
+ SharedV4ProviderOptions,
6
+ } from '@ai-sdk/provider';
7
+ import type { Context } from '@ai-sdk/provider-utils';
8
+ import {
9
+ experimental_filterActiveTools as filterActiveTools,
10
+ type Experimental_LanguageModelStreamPart as ModelCallStreamPart,
11
+ type Experimental_SandboxSession as SandboxSession,
12
+ type LanguageModel,
13
+ type LanguageModelUsage,
14
+ type ModelMessage,
15
+ type StepResult,
16
+ type ToolCallRepairFunction,
17
+ type ToolChoice,
18
+ type ToolSet,
19
+ } from 'ai';
20
+ import { createRestrictedTelemetryDispatcher } from 'ai/internal';
21
+ import {
22
+ type DoStreamStepRawResult,
23
+ doStreamStep,
24
+ type ModelStopCondition,
25
+ type ParsedToolCall,
26
+ type ProviderExecutedToolResult,
27
+ type StreamFinish,
28
+ } from './do-stream-step.js';
29
+ import { serializeToolSet } from './serializable-schema.js';
30
+ import type {
31
+ GenerationSettings,
32
+ PrepareStepCallback,
33
+ WorkflowAgentOnErrorCallback,
34
+ WorkflowAgentOnStepEndCallback,
35
+ WorkflowAgentOnStepFinishCallback,
36
+ TelemetryOptions,
37
+ WorkflowAgentOnStepStartCallback,
38
+ } from './workflow-agent.js';
39
+
40
+ // Re-export for consumers
41
+ export type { ProviderExecutedToolResult } from './do-stream-step.js';
42
+
43
+ /**
44
+ * The value yielded by the stream text iterator when tool calls are requested.
45
+ * Contains both the tool calls and the current conversation messages.
46
+ */
47
+ export interface StreamTextIteratorYieldValue {
48
+ /** The tool calls requested by the model (parsed with typed inputs) */
49
+ toolCalls: ParsedToolCall[];
50
+ /** The conversation messages up to (and including) the tool call request */
51
+ messages: LanguageModelV4Prompt;
52
+ /** The step result from the current step */
53
+ step?: StepResult<ToolSet, any>;
54
+ /** The current runtime context shared across the agent loop */
55
+ runtimeContext?: Context;
56
+ /** The current per-tool context, keyed by tool name */
57
+ toolsContext?: Record<string, Context | undefined>;
58
+ /** Provider-executed tool results (keyed by tool call ID) */
59
+ providerExecutedToolResults?: Map<string, ProviderExecutedToolResult>;
60
+ /** The sandbox selected for the current step. */
61
+ experimental_sandbox?: SandboxSession;
62
+ }
63
+
64
+ // This runs in the workflow context
65
+ export async function* streamTextIterator({
66
+ prompt,
67
+ tools = {},
68
+ writable,
69
+ model,
70
+ stopConditions,
71
+ onStepEnd,
72
+ onStepFinish,
73
+ onStepStart,
74
+ onError,
75
+ prepareStep,
76
+ generationSettings,
77
+ toolChoice,
78
+ runtimeContext,
79
+ toolsContext,
80
+ telemetry,
81
+ includeRawChunks = false,
82
+ repairToolCall,
83
+ responseFormat,
84
+ experimental_sandbox: sandbox,
85
+ }: {
86
+ prompt: LanguageModelV4Prompt;
87
+ tools: ToolSet;
88
+ writable?: WritableStream<ModelCallStreamPart<ToolSet>>;
89
+ model: LanguageModel;
90
+ stopConditions?: ModelStopCondition[] | ModelStopCondition;
91
+ onStepEnd?: WorkflowAgentOnStepEndCallback<any>;
92
+ /** @deprecated Use `onStepEnd` instead. */
93
+ onStepFinish?: WorkflowAgentOnStepFinishCallback<any>;
94
+ onStepStart?: WorkflowAgentOnStepStartCallback;
95
+ onError?: WorkflowAgentOnErrorCallback;
96
+ prepareStep?: PrepareStepCallback<any>;
97
+ generationSettings?: GenerationSettings;
98
+ toolChoice?: ToolChoice<ToolSet>;
99
+ runtimeContext?: Context;
100
+ toolsContext?: Record<string, Context | undefined>;
101
+ telemetry?: TelemetryOptions<Context, ToolSet>;
102
+ includeRawChunks?: boolean;
103
+ repairToolCall?: ToolCallRepairFunction<ToolSet>;
104
+ responseFormat?: LanguageModelV4CallOptions['responseFormat'];
105
+ experimental_sandbox?: SandboxSession;
106
+ }): AsyncGenerator<
107
+ StreamTextIteratorYieldValue,
108
+ LanguageModelV4Prompt,
109
+ LanguageModelV4ToolResultPart[]
110
+ > {
111
+ let conversationPrompt = [...prompt]; // Create a mutable copy
112
+ let currentModel: LanguageModel = model;
113
+ let currentGenerationSettings = generationSettings ?? {};
114
+ let currentToolChoice = toolChoice;
115
+ let currentRuntimeContext: Context = runtimeContext ?? {};
116
+ let currentToolsContext: Record<string, Context | undefined> =
117
+ toolsContext ?? {};
118
+ let currentActiveTools: string[] | undefined;
119
+
120
+ const steps: StepResult<any, any>[] = [];
121
+ let done = false;
122
+ let _isFirstIteration = true;
123
+ let stepNumber = 0;
124
+ let lastStep: StepResult<any, any> | undefined;
125
+ let lastStepWasToolCalls = false;
126
+
127
+ // TODO(#12164): replace this AI-core telemetry bridge with a
128
+ // WorkflowAgent-specific typed dispatcher. `streamTextIterator` widens
129
+ // tools/runtime context and emits Workflow-shaped events that are only
130
+ // approximately compatible with generateText telemetry event types.
131
+ const telemetryDispatcher = createRestrictedTelemetryDispatcher<
132
+ any,
133
+ any,
134
+ any
135
+ >({
136
+ telemetry: telemetry as any,
137
+ includeRuntimeContext: telemetry?.includeRuntimeContext,
138
+ includeToolsContext: telemetry?.includeToolsContext,
139
+ }) as any;
140
+
141
+ while (!done) {
142
+ // Check for abort signal
143
+ if (currentGenerationSettings.abortSignal?.aborted) {
144
+ break;
145
+ }
146
+
147
+ let stepSandbox = sandbox;
148
+
149
+ // Call prepareStep callback before each step if provided
150
+ if (prepareStep) {
151
+ const prepareResult = await prepareStep({
152
+ model: currentModel,
153
+ stepNumber,
154
+ steps,
155
+ messages: conversationPrompt,
156
+ runtimeContext: currentRuntimeContext,
157
+ toolsContext: currentToolsContext as never,
158
+ experimental_sandbox: sandbox,
159
+ });
160
+
161
+ stepSandbox = prepareResult?.experimental_sandbox ?? sandbox;
162
+
163
+ // Apply any overrides from prepareStep
164
+ if (prepareResult?.model !== undefined) {
165
+ currentModel = prepareResult.model;
166
+ }
167
+ // Apply messages override BEFORE system so the system message
168
+ // isn't lost when messages replaces the prompt.
169
+ if (prepareResult?.messages !== undefined) {
170
+ conversationPrompt = [...prepareResult.messages];
171
+ }
172
+ if (prepareResult?.system !== undefined) {
173
+ // Update or prepend system message in the conversation prompt.
174
+ // Applied AFTER messages override so the system message isn't
175
+ // lost when messages replaces the prompt.
176
+ if (
177
+ conversationPrompt.length > 0 &&
178
+ conversationPrompt[0].role === 'system'
179
+ ) {
180
+ // Replace existing system message
181
+ conversationPrompt[0] = {
182
+ role: 'system',
183
+ content: prepareResult.system,
184
+ };
185
+ } else {
186
+ // Prepend new system message
187
+ conversationPrompt.unshift({
188
+ role: 'system',
189
+ content: prepareResult.system,
190
+ });
191
+ }
192
+ }
193
+ if (prepareResult?.runtimeContext !== undefined) {
194
+ currentRuntimeContext = prepareResult.runtimeContext;
195
+ }
196
+ if (prepareResult?.toolsContext !== undefined) {
197
+ currentToolsContext = prepareResult.toolsContext as Record<
198
+ string,
199
+ Context | undefined
200
+ >;
201
+ }
202
+ if (prepareResult?.activeTools !== undefined) {
203
+ currentActiveTools = prepareResult.activeTools;
204
+ }
205
+ // Apply generation settings overrides
206
+ if (prepareResult?.maxOutputTokens !== undefined) {
207
+ currentGenerationSettings = {
208
+ ...currentGenerationSettings,
209
+ maxOutputTokens: prepareResult.maxOutputTokens,
210
+ };
211
+ }
212
+ if (prepareResult?.temperature !== undefined) {
213
+ currentGenerationSettings = {
214
+ ...currentGenerationSettings,
215
+ temperature: prepareResult.temperature,
216
+ };
217
+ }
218
+ if (prepareResult?.topP !== undefined) {
219
+ currentGenerationSettings = {
220
+ ...currentGenerationSettings,
221
+ topP: prepareResult.topP,
222
+ };
223
+ }
224
+ if (prepareResult?.topK !== undefined) {
225
+ currentGenerationSettings = {
226
+ ...currentGenerationSettings,
227
+ topK: prepareResult.topK,
228
+ };
229
+ }
230
+ if (prepareResult?.presencePenalty !== undefined) {
231
+ currentGenerationSettings = {
232
+ ...currentGenerationSettings,
233
+ presencePenalty: prepareResult.presencePenalty,
234
+ };
235
+ }
236
+ if (prepareResult?.frequencyPenalty !== undefined) {
237
+ currentGenerationSettings = {
238
+ ...currentGenerationSettings,
239
+ frequencyPenalty: prepareResult.frequencyPenalty,
240
+ };
241
+ }
242
+ if (prepareResult?.stopSequences !== undefined) {
243
+ currentGenerationSettings = {
244
+ ...currentGenerationSettings,
245
+ stopSequences: prepareResult.stopSequences,
246
+ };
247
+ }
248
+ if (prepareResult?.seed !== undefined) {
249
+ currentGenerationSettings = {
250
+ ...currentGenerationSettings,
251
+ seed: prepareResult.seed,
252
+ };
253
+ }
254
+ if (prepareResult?.maxRetries !== undefined) {
255
+ currentGenerationSettings = {
256
+ ...currentGenerationSettings,
257
+ maxRetries: prepareResult.maxRetries,
258
+ };
259
+ }
260
+ if (prepareResult?.headers !== undefined) {
261
+ currentGenerationSettings = {
262
+ ...currentGenerationSettings,
263
+ headers: prepareResult.headers,
264
+ };
265
+ }
266
+ if (prepareResult?.reasoning !== undefined) {
267
+ currentGenerationSettings = {
268
+ ...currentGenerationSettings,
269
+ reasoning: prepareResult.reasoning,
270
+ };
271
+ }
272
+ if (prepareResult?.providerOptions !== undefined) {
273
+ currentGenerationSettings = {
274
+ ...currentGenerationSettings,
275
+ providerOptions: prepareResult.providerOptions,
276
+ };
277
+ }
278
+ if (prepareResult?.toolChoice !== undefined) {
279
+ currentToolChoice = prepareResult.toolChoice;
280
+ }
281
+ }
282
+
283
+ if (onStepStart) {
284
+ await onStepStart({
285
+ stepNumber,
286
+ model: currentModel,
287
+ messages: conversationPrompt as unknown as ModelMessage[],
288
+ steps: [...steps],
289
+ runtimeContext: currentRuntimeContext,
290
+ toolsContext: currentToolsContext as never,
291
+ });
292
+ }
293
+
294
+ const stepStartModelInfo = getModelInfo(currentModel);
295
+ await telemetryDispatcher.onStepStart?.({
296
+ callId: 'workflow-agent',
297
+ provider: stepStartModelInfo.provider,
298
+ modelId: stepStartModelInfo.modelId,
299
+ stepNumber,
300
+ system: undefined,
301
+ messages: conversationPrompt as unknown as ModelMessage[],
302
+ tools,
303
+ toolChoice: currentToolChoice,
304
+ activeTools: currentActiveTools as never,
305
+ steps: steps.map(normalizeStepForTelemetry),
306
+ providerOptions: currentGenerationSettings.providerOptions,
307
+ output: undefined,
308
+ runtimeContext: currentRuntimeContext,
309
+ toolsContext: currentToolsContext as never,
310
+ });
311
+
312
+ try {
313
+ // Filter tools if activeTools is specified
314
+ const effectiveTools =
315
+ currentActiveTools && currentActiveTools.length > 0
316
+ ? (filterActiveTools({
317
+ tools,
318
+ activeTools: currentActiveTools,
319
+ }) ?? tools)
320
+ : tools;
321
+
322
+ // Serialize tools before crossing the step boundary — zod schemas
323
+ // contain functions that can't be serialized by the workflow runtime.
324
+ // Tools are reconstructed with Ajv validation inside doStreamStep.
325
+ const serializedTools = serializeToolSet(effectiveTools);
326
+ const modelCallInfo = getModelInfo(currentModel);
327
+
328
+ await telemetryDispatcher.onLanguageModelCallStart?.({
329
+ callId: 'workflow-agent',
330
+ provider: modelCallInfo.provider,
331
+ modelId: modelCallInfo.modelId,
332
+ system: undefined,
333
+ messages: conversationPrompt as unknown as ModelMessage[],
334
+ tools:
335
+ serializedTools == null
336
+ ? undefined
337
+ : Object.values(serializedTools).map(tool => ({ ...tool })),
338
+ maxOutputTokens: currentGenerationSettings.maxOutputTokens,
339
+ temperature: currentGenerationSettings.temperature,
340
+ topP: currentGenerationSettings.topP,
341
+ topK: currentGenerationSettings.topK,
342
+ presencePenalty: currentGenerationSettings.presencePenalty,
343
+ frequencyPenalty: currentGenerationSettings.frequencyPenalty,
344
+ stopSequences: currentGenerationSettings.stopSequences,
345
+ seed: currentGenerationSettings.seed,
346
+ reasoning: currentGenerationSettings.reasoning,
347
+ providerOptions: currentGenerationSettings.providerOptions,
348
+ headers: currentGenerationSettings.headers,
349
+ } as never);
350
+
351
+ const { toolCalls, finish, raw, providerExecutedToolResults } =
352
+ await doStreamStep(
353
+ conversationPrompt,
354
+ currentModel,
355
+ writable,
356
+ serializedTools,
357
+ {
358
+ ...currentGenerationSettings,
359
+ toolChoice: currentToolChoice,
360
+ includeRawChunks,
361
+ repairToolCall,
362
+ responseFormat,
363
+ },
364
+ );
365
+ // Reconstruct the full StepResult outside the step boundary so the
366
+ // durable event log doesn't carry StepResult's redundant copies (or the
367
+ // per-chunk snapshot the step used to return).
368
+ const step = buildStepResult(raw, toolCalls, finish, {
369
+ stepNumber,
370
+ runtimeContext: currentRuntimeContext,
371
+ toolsContext: currentToolsContext,
372
+ });
373
+
374
+ await telemetryDispatcher.onLanguageModelCallEnd?.({
375
+ callId: step.callId,
376
+ provider: step.model?.provider ?? 'unknown',
377
+ modelId: step.model?.modelId ?? 'unknown',
378
+ finishReason: step.finishReason,
379
+ usage: step.usage,
380
+ content: step.content,
381
+ responseId: step.response.id,
382
+ });
383
+
384
+ _isFirstIteration = false;
385
+ stepNumber++;
386
+ steps.push(step);
387
+ lastStep = step;
388
+ lastStepWasToolCalls = false;
389
+
390
+ const finishReason = finish?.finishReason;
391
+
392
+ if (finishReason === 'tool-calls') {
393
+ lastStepWasToolCalls = true;
394
+
395
+ const textContent = step.content.filter(
396
+ item => item.type === 'text',
397
+ ) as Array<{ type: 'text'; text: string }>;
398
+
399
+ // Add assistant message with text and tool calls to the conversation
400
+ // Note: providerMetadata from the tool call is mapped to providerOptions
401
+ // in the prompt format, following the AI SDK convention. This is critical
402
+ // for providers like Gemini that require thoughtSignature to be preserved
403
+ // across multi-turn tool calls. Some fields are sanitized before mapping.
404
+ conversationPrompt.push({
405
+ role: 'assistant',
406
+ content: [
407
+ ...textContent,
408
+ ...toolCalls.map(toolCall => {
409
+ const sanitizedMetadata = sanitizeProviderMetadataForToolCall(
410
+ toolCall.providerMetadata,
411
+ );
412
+ return {
413
+ type: 'tool-call' as const,
414
+ toolCallId: toolCall.toolCallId,
415
+ toolName: toolCall.toolName,
416
+ input: toolCall.input,
417
+ ...(sanitizedMetadata != null
418
+ ? {
419
+ providerOptions:
420
+ sanitizedMetadata as SharedV4ProviderOptions,
421
+ }
422
+ : {}),
423
+ };
424
+ }),
425
+ ],
426
+ });
427
+
428
+ // Yield the tool calls along with the current conversation messages
429
+ // This allows executeTool to pass the conversation context to tool execute functions
430
+ // Also include provider-executed tool results so they can be used instead of local execution
431
+ const toolResults = yield {
432
+ toolCalls,
433
+ messages: conversationPrompt,
434
+ step,
435
+ runtimeContext: currentRuntimeContext,
436
+ toolsContext: currentToolsContext,
437
+ experimental_sandbox: stepSandbox,
438
+ providerExecutedToolResults,
439
+ };
440
+
441
+ conversationPrompt.push({
442
+ role: 'tool',
443
+ content: toolResults,
444
+ });
445
+
446
+ if (stopConditions) {
447
+ const stopConditionList = Array.isArray(stopConditions)
448
+ ? stopConditions
449
+ : [stopConditions];
450
+ if (stopConditionList.some(test => test({ steps }))) {
451
+ done = true;
452
+ }
453
+ }
454
+ } else if (finishReason === 'stop') {
455
+ // Add assistant message with text content to the conversation
456
+ const textContent = step.content.filter(
457
+ item => item.type === 'text',
458
+ ) as Array<{ type: 'text'; text: string }>;
459
+
460
+ if (textContent.length > 0) {
461
+ conversationPrompt.push({
462
+ role: 'assistant',
463
+ content: textContent,
464
+ });
465
+ }
466
+
467
+ done = true;
468
+ } else if (finishReason === 'length') {
469
+ // Model hit max tokens - stop but don't throw
470
+ done = true;
471
+ } else if (finishReason === 'content-filter') {
472
+ // Content filter triggered - stop but don't throw
473
+ done = true;
474
+ } else if (finishReason === 'error') {
475
+ // Model error - stop but don't throw
476
+ done = true;
477
+ } else if (finishReason === 'other') {
478
+ // Other reason - stop but don't throw
479
+ done = true;
480
+ } else if (finishReason === 'unknown') {
481
+ // Unknown reason - stop but don't throw
482
+ done = true;
483
+ } else if (!finishReason) {
484
+ // No finish reason - this might happen on incomplete streams
485
+ done = true;
486
+ } else {
487
+ throw new Error(
488
+ `Unexpected finish reason: ${typeof finish?.finishReason === 'object' ? JSON.stringify(finish?.finishReason) : finish?.finishReason}`,
489
+ );
490
+ }
491
+
492
+ const resolvedOnStepEnd = onStepEnd ?? onStepFinish;
493
+ if (resolvedOnStepEnd) {
494
+ await resolvedOnStepEnd(step);
495
+ }
496
+ await telemetryDispatcher.onStepEnd?.(normalizeStepForTelemetry(step));
497
+ } catch (error) {
498
+ if (onError) {
499
+ await onError({ error });
500
+ }
501
+ throw error;
502
+ }
503
+ }
504
+
505
+ // Yield the final step if it wasn't already yielded (tool-calls steps are yielded inside the loop)
506
+ if (lastStep && !lastStepWasToolCalls) {
507
+ yield {
508
+ toolCalls: [],
509
+ messages: conversationPrompt,
510
+ step: lastStep,
511
+ runtimeContext: currentRuntimeContext,
512
+ toolsContext: currentToolsContext,
513
+ experimental_sandbox: sandbox,
514
+ };
515
+ }
516
+
517
+ return conversationPrompt;
518
+ }
519
+
520
+ function getModelInfo(model: LanguageModel): {
521
+ provider: string;
522
+ modelId: string;
523
+ } {
524
+ return typeof model === 'string'
525
+ ? { provider: model.split('/')[0] ?? 'gateway', modelId: model }
526
+ : { provider: model.provider, modelId: model.modelId };
527
+ }
528
+
529
+ function normalizeStepForTelemetry(step: StepResult<any, any>) {
530
+ return {
531
+ ...step,
532
+ model: step.model ?? { provider: 'unknown', modelId: 'unknown' },
533
+ };
534
+ }
535
+
536
+ /**
537
+ * Reconstruct a full `StepResult` from the minimal aggregates returned by
538
+ * `doStreamStep`. Runs outside the step boundary so StepResult's redundant
539
+ * fields (duplicate tool-call lists, `content`, `reasoningText`, the
540
+ * always-empty `*ToolResults` arrays) and the per-chunk snapshot don't cross
541
+ * it. The shape matches what the AI SDK's `streamText` exposes to callers.
542
+ */
543
+ function buildStepResult(
544
+ raw: DoStreamStepRawResult,
545
+ toolCalls: ParsedToolCall[],
546
+ finish: StreamFinish | undefined,
547
+ opts: {
548
+ stepNumber: number;
549
+ runtimeContext: Context;
550
+ toolsContext: Record<string, Context | undefined>;
551
+ },
552
+ ): StepResult<ToolSet, any> {
553
+ const { text, reasoning: reasoningParts, responseMetadata, warnings } = raw;
554
+ const reasoningText = reasoningParts.map(r => r.text).join('') || undefined;
555
+
556
+ const validToolCalls = toolCalls
557
+ .filter(tc => !tc.invalid)
558
+ .map(tc => ({
559
+ type: 'tool-call' as const,
560
+ toolCallId: tc.toolCallId,
561
+ toolName: tc.toolName,
562
+ input: tc.input,
563
+ ...(tc.dynamic ? { dynamic: true as const } : {}),
564
+ }));
565
+
566
+ return {
567
+ callId: 'workflow-agent',
568
+ stepNumber: opts.stepNumber,
569
+ model: {
570
+ provider: responseMetadata?.modelId?.split(':')[0] ?? 'unknown',
571
+ modelId: responseMetadata?.modelId ?? 'unknown',
572
+ },
573
+ functionId: undefined,
574
+ metadata: undefined,
575
+ runtimeContext: opts.runtimeContext ?? {},
576
+ toolsContext: opts.toolsContext ?? {},
577
+ content: [
578
+ ...(text ? [{ type: 'text' as const, text }] : []),
579
+ ...validToolCalls,
580
+ ],
581
+ text,
582
+ reasoning: reasoningParts.map(r => ({
583
+ type: 'reasoning' as const,
584
+ text: r.text,
585
+ })),
586
+ reasoningText,
587
+ files: [],
588
+ sources: [],
589
+ toolCalls: validToolCalls,
590
+ staticToolCalls: [],
591
+ dynamicToolCalls: validToolCalls.filter(tc => tc.dynamic),
592
+ toolResults: [],
593
+ staticToolResults: [],
594
+ dynamicToolResults: [],
595
+ finishReason: finish?.finishReason ?? 'other',
596
+ rawFinishReason: finish?.rawFinishReason,
597
+ usage:
598
+ finish?.usage ??
599
+ ({
600
+ inputTokens: 0,
601
+ inputTokenDetails: {
602
+ noCacheTokens: undefined,
603
+ cacheReadTokens: undefined,
604
+ cacheWriteTokens: undefined,
605
+ },
606
+ outputTokens: 0,
607
+ outputTokenDetails: {
608
+ textTokens: undefined,
609
+ reasoningTokens: undefined,
610
+ },
611
+ totalTokens: 0,
612
+ } as LanguageModelUsage),
613
+ performance: {
614
+ effectiveOutputTokensPerSecond: 0,
615
+ outputTokensPerSecond: undefined,
616
+ inputTokensPerSecond: undefined,
617
+ effectiveTotalTokensPerSecond: 0,
618
+ stepTimeMs: 0,
619
+ responseTimeMs: 0,
620
+ toolExecutionMs: {},
621
+ timeToFirstOutputMs: undefined,
622
+ },
623
+ warnings,
624
+ request: {
625
+ body: '',
626
+ messages: [], // TODO implement step request messages
627
+ },
628
+ response: {
629
+ id: responseMetadata?.id ?? 'unknown',
630
+ timestamp: responseMetadata?.timestamp ?? new Date(),
631
+ modelId: responseMetadata?.modelId ?? 'unknown',
632
+ messages: [],
633
+ },
634
+ providerMetadata: finish?.providerMetadata ?? {},
635
+ } as StepResult<ToolSet, any>;
636
+ }
637
+
638
+ /**
639
+ * Strip OpenAI's itemId from providerMetadata (requires reasoning items we don't preserve).
640
+ * Preserves all other provider metadata (e.g., Gemini's thoughtSignature).
641
+ */
642
+ function sanitizeProviderMetadataForToolCall(
643
+ metadata: unknown,
644
+ ): Record<string, unknown> | undefined {
645
+ if (metadata == null) return undefined;
646
+
647
+ const meta = metadata as Record<string, unknown>;
648
+
649
+ // Check if OpenAI metadata exists and needs sanitization
650
+ if ('openai' in meta && meta.openai != null) {
651
+ const { openai, ...restProviders } = meta;
652
+ const openaiMeta = openai as Record<string, unknown>;
653
+
654
+ // Remove itemId from OpenAI metadata - it requires reasoning items we don't preserve
655
+ const { itemId: _itemId, ...restOpenai } = openaiMeta;
656
+
657
+ // Reconstruct metadata without itemId
658
+ const hasOtherOpenaiFields = Object.keys(restOpenai).length > 0;
659
+ const hasOtherProviders = Object.keys(restProviders).length > 0;
660
+
661
+ if (hasOtherOpenaiFields && hasOtherProviders) {
662
+ return { ...restProviders, openai: restOpenai };
663
+ } else if (hasOtherOpenaiFields) {
664
+ return { openai: restOpenai };
665
+ } else if (hasOtherProviders) {
666
+ return restProviders;
667
+ }
668
+ return undefined;
669
+ }
670
+
671
+ return meta;
672
+ }