@ai-sdk/open-responses 2.0.26 → 2.0.28

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,20 +9,22 @@ 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,
16
15
  InputTextContentParam,
17
16
  OpenResponsesRequestBody,
18
17
  OutputTextContentParam,
18
+ ReasoningItemParam,
19
19
  RefusalContentParam,
20
20
  } from './open-responses-api';
21
21
 
22
22
  export async function convertToOpenResponsesInput({
23
23
  prompt,
24
+ providerOptionsName = 'open-responses',
24
25
  }: {
25
26
  prompt: LanguageModelV4Prompt;
27
+ providerOptionsName?: string;
26
28
  }): Promise<{
27
29
  input: OpenResponsesRequestBody['input'];
28
30
  instructions: string | undefined;
@@ -102,24 +104,128 @@ export async function convertToOpenResponsesInput({
102
104
  }
103
105
 
104
106
  case 'assistant': {
105
- const assistantContent: Array<
107
+ let assistantContent: Array<
106
108
  OutputTextContentParam | RefusalContentParam
107
109
  > = [];
108
- 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
+ };
109
126
 
110
127
  for (const part of content) {
111
128
  switch (part.type) {
129
+ case 'reasoning': {
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 = {
151
+ type: 'reasoning',
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
+ }
186
+ break;
187
+ }
112
188
  case 'text': {
113
- 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
+ });
114
211
  break;
115
212
  }
116
213
  case 'tool-call': {
214
+ flushAssistantContent();
215
+
117
216
  const argumentsValue =
118
217
  typeof part.input === 'string'
119
218
  ? part.input
120
219
  : JSON.stringify(part.input);
121
- 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({
122
227
  type: 'function_call',
228
+ ...(itemId != null && { id: itemId }),
123
229
  call_id: part.toolCallId,
124
230
  name: part.toolName,
125
231
  arguments: argumentsValue,
@@ -129,19 +235,7 @@ export async function convertToOpenResponsesInput({
129
235
  }
130
236
  }
131
237
 
132
- // Push assistant message with text content if any
133
- if (assistantContent.length > 0) {
134
- input.push({
135
- type: 'message',
136
- role: 'assistant',
137
- content: assistantContent,
138
- });
139
- }
140
-
141
- // Push function calls as separate items
142
- for (const toolCall of toolCalls) {
143
- input.push(toolCall);
144
- }
238
+ flushAssistantContent();
145
239
 
146
240
  break;
147
241
  }
@@ -251,3 +345,97 @@ export async function convertToOpenResponsesInput({
251
345
  warnings,
252
346
  };
253
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
+ }
@@ -143,7 +143,7 @@ export type ReasoningItemParam = {
143
143
  id?: string;
144
144
  type: 'reasoning';
145
145
  summary: ReasoningSummaryContentParam[];
146
- content?: unknown;
146
+ content?: ReasoningTextContent[];
147
147
  encrypted_content?: string;
148
148
  };
149
149
 
@@ -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
+ }