@ai-sdk/open-responses 2.0.27 → 2.0.29

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.
@@ -9,7 +9,6 @@ import {
9
9
  resolveFullMediaType,
10
10
  } from '@ai-sdk/provider-utils';
11
11
  import type {
12
- FunctionCallItemParam,
13
12
  FunctionCallOutputItemParam,
14
13
  InputFileContentParam,
15
14
  InputImageContentParam,
@@ -22,8 +21,10 @@ import type {
22
21
 
23
22
  export async function convertToOpenResponsesInput({
24
23
  prompt,
24
+ providerOptionsName = 'open-responses',
25
25
  }: {
26
26
  prompt: LanguageModelV4Prompt;
27
+ providerOptionsName?: string;
27
28
  }): Promise<{
28
29
  input: OpenResponsesRequestBody['input'];
29
30
  instructions: string | undefined;
@@ -103,33 +104,128 @@ export async function convertToOpenResponsesInput({
103
104
  }
104
105
 
105
106
  case 'assistant': {
106
- const assistantContent: Array<
107
+ let assistantContent: Array<
107
108
  OutputTextContentParam | RefusalContentParam
108
109
  > = [];
109
- const reasoningItems: Array<ReasoningItemParam> = [];
110
- const toolCalls: Array<FunctionCallItemParam> = [];
110
+ let assistantMessageId: string | undefined;
111
+
112
+ const flushAssistantContent = () => {
113
+ if (assistantContent.length === 0) {
114
+ return;
115
+ }
116
+
117
+ input.push({
118
+ type: 'message',
119
+ role: 'assistant',
120
+ content: assistantContent,
121
+ ...(assistantMessageId != null && { id: assistantMessageId }),
122
+ });
123
+ assistantContent = [];
124
+ assistantMessageId = undefined;
125
+ };
111
126
 
112
127
  for (const part of content) {
113
128
  switch (part.type) {
114
129
  case 'reasoning': {
115
- reasoningItems.push({
130
+ flushAssistantContent();
131
+
132
+ const providerData = getProviderData(part, providerOptionsName);
133
+ const itemId =
134
+ typeof providerData?.itemId === 'string'
135
+ ? providerData.itemId
136
+ : undefined;
137
+ const summary = parseReasoningSummary(
138
+ providerData?.reasoningSummary,
139
+ );
140
+ const reasoningContent = parseReasoningContent(
141
+ providerData?.reasoningContent,
142
+ );
143
+ const hasReasoningContent =
144
+ providerData != null && 'reasoningContent' in providerData;
145
+ const encryptedContent =
146
+ typeof providerData?.reasoningEncryptedContent === 'string'
147
+ ? providerData.reasoningEncryptedContent
148
+ : undefined;
149
+
150
+ const reasoningItem: ReasoningItemParam = {
116
151
  type: 'reasoning',
117
- summary: [],
118
- content: [{ type: 'reasoning_text', text: part.text }],
119
- });
152
+ summary: summary ?? [],
153
+ ...(itemId != null && { id: itemId }),
154
+ ...(reasoningContent != null
155
+ ? { content: reasoningContent }
156
+ : !hasReasoningContent && part.text.length > 0
157
+ ? {
158
+ content: [
159
+ {
160
+ type: 'reasoning_text' as const,
161
+ text: part.text,
162
+ },
163
+ ],
164
+ }
165
+ : {}),
166
+ ...(encryptedContent != null && {
167
+ encrypted_content: encryptedContent,
168
+ }),
169
+ };
170
+ const previousItem = input[input.length - 1];
171
+
172
+ if (
173
+ reasoningItem.id != null &&
174
+ previousItem?.type === 'reasoning' &&
175
+ previousItem.id === reasoningItem.id
176
+ ) {
177
+ if (reasoningItem.content != null) {
178
+ previousItem.content = [
179
+ ...(previousItem.content ?? []),
180
+ ...reasoningItem.content,
181
+ ];
182
+ }
183
+ } else {
184
+ input.push(reasoningItem);
185
+ }
120
186
  break;
121
187
  }
122
188
  case 'text': {
123
- assistantContent.push({ type: 'output_text', text: part.text });
189
+ const providerData = getProviderData(part, providerOptionsName);
190
+ const itemId =
191
+ typeof providerData?.itemId === 'string'
192
+ ? providerData.itemId
193
+ : undefined;
194
+ const annotations = parseOutputTextAnnotations(
195
+ providerData?.annotations,
196
+ );
197
+
198
+ if (
199
+ assistantContent.length > 0 &&
200
+ assistantMessageId !== itemId
201
+ ) {
202
+ flushAssistantContent();
203
+ }
204
+
205
+ assistantMessageId = itemId;
206
+ assistantContent.push({
207
+ type: 'output_text',
208
+ text: part.text,
209
+ ...(annotations != null && { annotations }),
210
+ });
124
211
  break;
125
212
  }
126
213
  case 'tool-call': {
214
+ flushAssistantContent();
215
+
127
216
  const argumentsValue =
128
217
  typeof part.input === 'string'
129
218
  ? part.input
130
219
  : JSON.stringify(part.input);
131
- toolCalls.push({
220
+ const providerData = getProviderData(part, providerOptionsName);
221
+ const itemId =
222
+ typeof providerData?.itemId === 'string'
223
+ ? providerData.itemId
224
+ : undefined;
225
+
226
+ input.push({
132
227
  type: 'function_call',
228
+ ...(itemId != null && { id: itemId }),
133
229
  call_id: part.toolCallId,
134
230
  name: part.toolName,
135
231
  arguments: argumentsValue,
@@ -139,24 +235,7 @@ export async function convertToOpenResponsesInput({
139
235
  }
140
236
  }
141
237
 
142
- // Push reasoning as separate items
143
- for (const reasoningItem of reasoningItems) {
144
- input.push(reasoningItem);
145
- }
146
-
147
- // Push assistant message with text content if any
148
- if (assistantContent.length > 0) {
149
- input.push({
150
- type: 'message',
151
- role: 'assistant',
152
- content: assistantContent,
153
- });
154
- }
155
-
156
- // Push function calls as separate items
157
- for (const toolCall of toolCalls) {
158
- input.push(toolCall);
159
- }
238
+ flushAssistantContent();
160
239
 
161
240
  break;
162
241
  }
@@ -266,3 +345,97 @@ export async function convertToOpenResponsesInput({
266
345
  warnings,
267
346
  };
268
347
  }
348
+
349
+ function getProviderData(
350
+ part: {
351
+ providerOptions?: Record<string, unknown>;
352
+ },
353
+ providerOptionsName: string,
354
+ ): Record<string, unknown> | undefined {
355
+ const providerData =
356
+ part.providerOptions?.[providerOptionsName] ??
357
+ (
358
+ part as {
359
+ providerMetadata?: Record<string, unknown>;
360
+ }
361
+ ).providerMetadata?.[providerOptionsName];
362
+
363
+ return providerData != null &&
364
+ typeof providerData === 'object' &&
365
+ !Array.isArray(providerData)
366
+ ? (providerData as Record<string, unknown>)
367
+ : undefined;
368
+ }
369
+
370
+ function parseReasoningSummary(
371
+ value: unknown,
372
+ ): ReasoningItemParam['summary'] | undefined {
373
+ if (
374
+ !Array.isArray(value) ||
375
+ !value.every(
376
+ part =>
377
+ part != null &&
378
+ typeof part === 'object' &&
379
+ (part as { type?: unknown }).type === 'summary_text' &&
380
+ typeof (part as { text?: unknown }).text === 'string',
381
+ )
382
+ ) {
383
+ return undefined;
384
+ }
385
+
386
+ return value.map(part => ({
387
+ type: 'summary_text',
388
+ text: (part as { text: string }).text,
389
+ }));
390
+ }
391
+
392
+ function parseReasoningContent(
393
+ value: unknown,
394
+ ): ReasoningItemParam['content'] | undefined {
395
+ if (
396
+ !Array.isArray(value) ||
397
+ !value.every(
398
+ part =>
399
+ part != null &&
400
+ typeof part === 'object' &&
401
+ (part as { type?: unknown }).type === 'reasoning_text' &&
402
+ typeof (part as { text?: unknown }).text === 'string',
403
+ )
404
+ ) {
405
+ return undefined;
406
+ }
407
+
408
+ return value.map(part => ({
409
+ type: 'reasoning_text',
410
+ text: (part as { text: string }).text,
411
+ }));
412
+ }
413
+
414
+ function parseOutputTextAnnotations(
415
+ value: unknown,
416
+ ): OutputTextContentParam['annotations'] | undefined {
417
+ if (
418
+ !Array.isArray(value) ||
419
+ !value.every(
420
+ annotation =>
421
+ annotation != null &&
422
+ typeof annotation === 'object' &&
423
+ (annotation as { type?: unknown }).type === 'url_citation' &&
424
+ typeof (annotation as { start_index?: unknown }).start_index ===
425
+ 'number' &&
426
+ typeof (annotation as { end_index?: unknown }).end_index === 'number' &&
427
+ typeof (annotation as { url?: unknown }).url === 'string' &&
428
+ typeof (annotation as { title?: unknown }).title === 'string',
429
+ )
430
+ ) {
431
+ return undefined;
432
+ }
433
+
434
+ return value.map(annotation => ({
435
+ type: 'url_citation',
436
+ start_index: (annotation as { start_index: number }).start_index,
437
+ end_index: (annotation as { end_index: number }).end_index,
438
+ url: (annotation as { url: string }).url,
439
+ title: (annotation as { title: string }).title,
440
+ }));
441
+ }
@@ -311,7 +311,7 @@ export type StreamOptionsParam = {
311
311
  * Configuration options for reasoning behavior.
312
312
  */
313
313
  export type ReasoningParam = {
314
- effort?: ReasoningEffortEnum;
314
+ effort?: string;
315
315
  summary?: ReasoningSummaryEnum;
316
316
  };
317
317
 
@@ -8,6 +8,12 @@ import { z } from 'zod/v4';
8
8
  export const openResponsesLanguageModelOptions = lazySchema(() =>
9
9
  zodSchema(
10
10
  z.object({
11
+ /**
12
+ * Provider-native reasoning effort. The value is passed through to the
13
+ * endpoint and takes precedence over the top-level `reasoning` setting.
14
+ */
15
+ reasoningEffort: z.string().nullish(),
16
+
11
17
  /**
12
18
  * Controls reasoning summary output from the model.
13
19
  * Valid values: 'concise', 'detailed', 'auto'.
@@ -8,6 +8,7 @@ import {
8
8
  type LanguageModelV4StreamPart,
9
9
  type LanguageModelV4StreamResult,
10
10
  type LanguageModelV4Usage,
11
+ type SharedV4ProviderMetadata,
11
12
  type SharedV4Warning,
12
13
  } from '@ai-sdk/provider';
13
14
  import {
@@ -29,10 +30,12 @@ import { z } from 'zod/v4';
29
30
  import { convertToOpenResponsesInput } from './convert-to-open-responses-input';
30
31
  import {
31
32
  openResponsesErrorSchema,
33
+ type Annotation,
32
34
  type FunctionToolParam,
33
35
  type OpenResponsesRequestBody,
34
36
  type OpenResponsesResponseBody,
35
37
  type OpenResponsesChunk,
38
+ type ReasoningBody,
36
39
  type ToolChoiceParam,
37
40
  } from './open-responses-api';
38
41
  import { mapOpenResponsesFinishReason } from './map-open-responses-finish-reason';
@@ -112,20 +115,30 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
112
115
  warnings: inputWarnings,
113
116
  } = await convertToOpenResponsesInput({
114
117
  prompt,
118
+ providerOptionsName: this.config.providerOptionsName,
115
119
  });
116
120
 
117
121
  warnings.push(...inputWarnings);
118
122
 
119
123
  // Convert function tools to the Open Responses format
120
- const functionTools: FunctionToolParam[] | undefined = tools
121
- ?.filter(tool => tool.type === 'function')
122
- .map(tool => ({
123
- type: 'function' as const,
124
- name: tool.name,
125
- description: tool.description,
126
- parameters: tool.inputSchema,
127
- ...(tool.strict != null ? { strict: tool.strict } : {}),
128
- }));
124
+ const functionTools: FunctionToolParam[] = [];
125
+
126
+ for (const tool of tools ?? []) {
127
+ if (tool.type === 'provider') {
128
+ warnings.push({
129
+ type: 'unsupported',
130
+ feature: `provider-defined tool ${tool.id}`,
131
+ });
132
+ } else {
133
+ functionTools.push({
134
+ type: 'function',
135
+ name: tool.name,
136
+ description: tool.description,
137
+ parameters: tool.inputSchema,
138
+ ...(tool.strict != null ? { strict: tool.strict } : {}),
139
+ });
140
+ }
141
+ }
129
142
 
130
143
  // Convert tool choice to the Open Responses format
131
144
  const convertedToolChoice: ToolChoiceParam | undefined =
@@ -156,21 +169,23 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
156
169
  schema: openResponsesLanguageModelOptions,
157
170
  });
158
171
 
159
- const resolvedReasoningEffort = isCustomReasoning(reasoning)
160
- ? reasoning === 'none'
161
- ? 'none'
162
- : mapReasoningToProviderEffort({
163
- reasoning,
164
- effortMap: {
165
- minimal: 'low',
166
- low: 'low',
167
- medium: 'medium',
168
- high: 'high',
169
- xhigh: 'xhigh',
170
- },
171
- warnings,
172
- })
173
- : undefined;
172
+ const resolvedReasoningEffort =
173
+ openResponsesOptions?.reasoningEffort ??
174
+ (isCustomReasoning(reasoning)
175
+ ? reasoning === 'none'
176
+ ? 'none'
177
+ : mapReasoningToProviderEffort({
178
+ reasoning,
179
+ effortMap: {
180
+ minimal: 'low',
181
+ low: 'low',
182
+ medium: 'medium',
183
+ high: 'high',
184
+ xhigh: 'xhigh',
185
+ },
186
+ warnings,
187
+ })
188
+ : undefined);
174
189
 
175
190
  return {
176
191
  body: {
@@ -194,7 +209,7 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
194
209
  }),
195
210
  }
196
211
  : undefined,
197
- tools: functionTools?.length ? functionTools : undefined,
212
+ tools: functionTools.length ? functionTools : undefined,
198
213
  tool_choice: convertedToolChoice,
199
214
  ...(textFormat != null && { text: { format: textFormat } }),
200
215
  },
@@ -263,10 +278,26 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
263
278
  switch (part.type) {
264
279
  // TODO AI SDK 7 adjust reasoning in the specification to better support the reasoning structure from open responses.
265
280
  case 'reasoning': {
266
- for (const contentPart of part.content ?? []) {
281
+ if ((part.content?.length ?? 0) > 0) {
282
+ for (const contentPart of part.content!) {
283
+ content.push({
284
+ type: 'reasoning',
285
+ text: contentPart.text,
286
+ providerMetadata: createReasoningProviderMetadata({
287
+ part,
288
+ providerOptionsName: this.config.providerOptionsName,
289
+ reasoningContent: [contentPart],
290
+ }),
291
+ });
292
+ }
293
+ } else {
267
294
  content.push({
268
295
  type: 'reasoning',
269
- text: contentPart.text,
296
+ text: part.summary.map(summaryPart => summaryPart.text).join(''),
297
+ providerMetadata: createReasoningProviderMetadata({
298
+ part,
299
+ providerOptionsName: this.config.providerOptionsName,
300
+ }),
270
301
  });
271
302
  }
272
303
  break;
@@ -274,9 +305,17 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
274
305
 
275
306
  case 'message': {
276
307
  for (const contentPart of part.content) {
308
+ const annotations = getOutputTextAnnotations(contentPart);
309
+
277
310
  content.push({
278
311
  type: 'text',
279
312
  text: contentPart.text,
313
+ providerMetadata: {
314
+ [this.config.providerOptionsName]: {
315
+ itemId: part.id,
316
+ ...(annotations.length > 0 && { annotations }),
317
+ },
318
+ },
280
319
  });
281
320
  }
282
321
 
@@ -290,6 +329,9 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
290
329
  toolCallId: part.call_id,
291
330
  toolName: part.name,
292
331
  input: part.arguments,
332
+ providerMetadata: {
333
+ [this.config.providerOptionsName]: { itemId: part.id },
334
+ },
293
335
  });
294
336
  break;
295
337
  }
@@ -401,7 +443,7 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
401
443
  usage.raw = responseUsage;
402
444
  };
403
445
 
404
- let isActiveReasoning = false;
446
+ let activeReasoningId: string | undefined;
405
447
  let hasToolCalls = false;
406
448
  let finishReason: LanguageModelV4FinishReason = {
407
449
  unified: 'other',
@@ -411,6 +453,7 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
411
453
  string,
412
454
  { toolName?: string; toolCallId?: string; arguments?: string }
413
455
  >();
456
+ const providerOptionsName = this.config.providerOptionsName;
414
457
 
415
458
  return {
416
459
  stream: response.pipeThrough(
@@ -488,6 +531,11 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
488
531
  toolCallId,
489
532
  toolName,
490
533
  input,
534
+ providerMetadata: {
535
+ [providerOptionsName]: {
536
+ itemId: chunk.item.id,
537
+ },
538
+ },
491
539
  });
492
540
  hasToolCalls = true;
493
541
 
@@ -503,7 +551,7 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
503
551
  type: 'reasoning-start',
504
552
  id: chunk.item.id,
505
553
  });
506
- isActiveReasoning = true;
554
+ activeReasoningId = chunk.item.id;
507
555
  } else if (
508
556
  (chunk as { type: string }).type ===
509
557
  'response.reasoning_text.delta'
@@ -521,8 +569,17 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
521
569
  chunk.type === 'response.output_item.done' &&
522
570
  chunk.item.type === 'reasoning'
523
571
  ) {
524
- controller.enqueue({ type: 'reasoning-end', id: chunk.item.id });
525
- isActiveReasoning = false;
572
+ controller.enqueue({
573
+ type: 'reasoning-end',
574
+ id: chunk.item.id,
575
+ providerMetadata: createReasoningProviderMetadata({
576
+ part: chunk.item,
577
+ providerOptionsName,
578
+ }),
579
+ });
580
+ if (activeReasoningId === chunk.item.id) {
581
+ activeReasoningId = undefined;
582
+ }
526
583
  }
527
584
 
528
585
  // Text events
@@ -541,7 +598,20 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
541
598
  chunk.type === 'response.output_item.done' &&
542
599
  chunk.item.type === 'message'
543
600
  ) {
544
- controller.enqueue({ type: 'text-end', id: chunk.item.id });
601
+ const annotations = chunk.item.content.flatMap(
602
+ getOutputTextAnnotations,
603
+ );
604
+
605
+ controller.enqueue({
606
+ type: 'text-end',
607
+ id: chunk.item.id,
608
+ providerMetadata: {
609
+ [providerOptionsName]: {
610
+ itemId: chunk.item.id,
611
+ ...(annotations.length > 0 && { annotations }),
612
+ },
613
+ },
614
+ });
545
615
  } else if (
546
616
  chunk.type === 'response.completed' ||
547
617
  chunk.type === 'response.incomplete'
@@ -565,8 +635,11 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
565
635
  },
566
636
 
567
637
  flush(controller) {
568
- if (isActiveReasoning) {
569
- controller.enqueue({ type: 'reasoning-end', id: 'reasoning-0' });
638
+ if (activeReasoningId != null) {
639
+ controller.enqueue({
640
+ type: 'reasoning-end',
641
+ id: activeReasoningId,
642
+ });
570
643
  }
571
644
 
572
645
  controller.enqueue({
@@ -583,3 +656,63 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
583
656
  };
584
657
  }
585
658
  }
659
+
660
+ function createReasoningProviderMetadata({
661
+ part,
662
+ providerOptionsName,
663
+ reasoningContent = part.content,
664
+ }: {
665
+ part: ReasoningBody;
666
+ providerOptionsName: string;
667
+ reasoningContent?: ReasoningBody['content'];
668
+ }): SharedV4ProviderMetadata {
669
+ return {
670
+ [providerOptionsName]: {
671
+ itemId: part.id,
672
+ reasoningSummary: part.summary.map(summaryPart => ({
673
+ type: 'summary_text',
674
+ text: summaryPart.text,
675
+ })),
676
+ reasoningContent:
677
+ reasoningContent == null
678
+ ? null
679
+ : reasoningContent.map(contentPart => ({
680
+ type: 'reasoning_text',
681
+ text: contentPart.text,
682
+ })),
683
+ ...(part.encrypted_content != null && {
684
+ reasoningEncryptedContent: part.encrypted_content,
685
+ }),
686
+ },
687
+ };
688
+ }
689
+
690
+ function getOutputTextAnnotations(value: unknown): Annotation[] {
691
+ if (
692
+ value == null ||
693
+ typeof value !== 'object' ||
694
+ !('annotations' in value) ||
695
+ !Array.isArray(value.annotations) ||
696
+ !value.annotations.every(
697
+ annotation =>
698
+ annotation != null &&
699
+ typeof annotation === 'object' &&
700
+ (annotation as { type?: unknown }).type === 'url_citation' &&
701
+ typeof (annotation as { start_index?: unknown }).start_index ===
702
+ 'number' &&
703
+ typeof (annotation as { end_index?: unknown }).end_index === 'number' &&
704
+ typeof (annotation as { url?: unknown }).url === 'string' &&
705
+ typeof (annotation as { title?: unknown }).title === 'string',
706
+ )
707
+ ) {
708
+ return [];
709
+ }
710
+
711
+ return value.annotations.map(annotation => ({
712
+ type: 'url_citation',
713
+ start_index: (annotation as { start_index: number }).start_index,
714
+ end_index: (annotation as { end_index: number }).end_index,
715
+ url: (annotation as { url: string }).url,
716
+ title: (annotation as { title: string }).title,
717
+ }));
718
+ }