@ai-sdk/open-responses 2.0.31 → 2.0.35

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.
@@ -16,22 +16,34 @@ import {
16
16
  createEventSourceResponseHandler,
17
17
  createJsonErrorResponseHandler,
18
18
  createJsonResponseHandler,
19
+ createProviderStreamError,
19
20
  isCustomReasoning,
20
21
  jsonSchema,
21
22
  mapReasoningToProviderEffort,
22
23
  parseProviderOptions,
23
24
  postJsonToApi,
25
+ SerializationError,
24
26
  serializeModelOptions,
25
27
  WORKFLOW_SERIALIZE,
26
28
  WORKFLOW_DESERIALIZE,
27
29
  type ParseResult,
28
30
  } from '@ai-sdk/provider-utils';
29
31
  import { z } from 'zod/v4';
32
+ import {
33
+ createOpenResponsesExtensionRegistry,
34
+ isOpenResponsesExtensionEvent,
35
+ isOpenResponsesExtensionItem,
36
+ isOpenResponsesJSONObject,
37
+ type OpenResponsesExtension,
38
+ type OpenResponsesExtensionContentPart,
39
+ type OpenResponsesExtensionItem,
40
+ type OpenResponsesExtensionRecord,
41
+ type OpenResponsesExtensionRegistry,
42
+ } from '../open-responses-extension';
30
43
  import { convertToOpenResponsesInput } from './convert-to-open-responses-input';
31
44
  import {
32
45
  openResponsesErrorSchema,
33
46
  type Annotation,
34
- type FunctionToolParam,
35
47
  type OpenResponsesRequestBody,
36
48
  type OpenResponsesResponseBody,
37
49
  type OpenResponsesChunk,
@@ -48,8 +60,16 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
48
60
  readonly modelId: string;
49
61
 
50
62
  private readonly config: OpenResponsesConfig;
63
+ private readonly extensionRegistry: OpenResponsesExtensionRegistry;
51
64
 
52
65
  static [WORKFLOW_SERIALIZE](model: OpenResponsesLanguageModel) {
66
+ if (model.extensionRegistry.byExtensionId.size > 0) {
67
+ throw new SerializationError({
68
+ message:
69
+ 'Open Responses models with registered extensions cannot be serialized across workflow boundaries. Recreate the provider with its extension codecs inside the workflow step.',
70
+ });
71
+ }
72
+
53
73
  return serializeModelOptions({
54
74
  modelId: model.modelId,
55
75
  config: model.config,
@@ -66,6 +86,8 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
66
86
  constructor(modelId: string, config: OpenResponsesConfig) {
67
87
  this.modelId = modelId;
68
88
  this.config = config;
89
+ this.extensionRegistry =
90
+ config.extensionRegistry ?? createOpenResponsesExtensionRegistry();
69
91
  }
70
92
 
71
93
  readonly supportedUrls: Record<string, RegExp[]> = {
@@ -109,6 +131,12 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
109
131
  warnings.push({ type: 'unsupported', feature: 'seed' });
110
132
  }
111
133
 
134
+ const providerToolsByName = new Map(
135
+ (tools ?? [])
136
+ .filter(tool => tool.type === 'provider')
137
+ .map(tool => [tool.name, tool]),
138
+ );
139
+
112
140
  const {
113
141
  input,
114
142
  instructions,
@@ -116,21 +144,63 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
116
144
  } = await convertToOpenResponsesInput({
117
145
  prompt,
118
146
  providerOptionsName: this.config.providerOptionsName,
147
+ extensionRegistry: this.extensionRegistry,
148
+ providerToolsByName,
119
149
  });
120
150
 
121
151
  warnings.push(...inputWarnings);
122
152
 
123
- // Convert function tools to the Open Responses format
124
- const functionTools: FunctionToolParam[] = [];
153
+ const convertedTools: NonNullable<OpenResponsesRequestBody['tools']> = [];
154
+ const encodedProviderToolsByName = new Map<
155
+ string,
156
+ {
157
+ toolType: OpenResponsesExtensionRecord['type'];
158
+ encodeToolChoice: OpenResponsesExtension['encodeToolChoice'];
159
+ tool: Extract<
160
+ NonNullable<LanguageModelV4CallOptions['tools']>[number],
161
+ { type: 'provider' }
162
+ >;
163
+ }
164
+ >();
125
165
 
126
166
  for (const tool of tools ?? []) {
127
167
  if (tool.type === 'provider') {
128
- warnings.push({
129
- type: 'unsupported',
130
- feature: `provider-defined tool ${tool.id}`,
131
- });
168
+ const extension = this.extensionRegistry.byProviderToolId.get(tool.id);
169
+ let encoded: OpenResponsesExtensionRecord | undefined;
170
+
171
+ if (extension != null) {
172
+ try {
173
+ const fields = await extension.encodeTool({
174
+ name: tool.name,
175
+ args: tool.args,
176
+ });
177
+
178
+ if (isOpenResponsesJSONObject(fields)) {
179
+ encoded = {
180
+ ...fields,
181
+ type: extension.toolType,
182
+ };
183
+ }
184
+ } catch {
185
+ // Encoding failures are reported as unsupported below.
186
+ }
187
+ }
188
+
189
+ if (encoded == null) {
190
+ warnings.push({
191
+ type: 'unsupported',
192
+ feature: `provider-defined tool ${tool.id}`,
193
+ });
194
+ } else if (extension != null) {
195
+ convertedTools.push(encoded);
196
+ encodedProviderToolsByName.set(tool.name, {
197
+ toolType: extension.toolType,
198
+ encodeToolChoice: extension.encodeToolChoice,
199
+ tool,
200
+ });
201
+ }
132
202
  } else {
133
- functionTools.push({
203
+ convertedTools.push({
134
204
  type: 'function',
135
205
  name: tool.name,
136
206
  description: tool.description,
@@ -141,12 +211,47 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
141
211
  }
142
212
 
143
213
  // Convert tool choice to the Open Responses format
144
- const convertedToolChoice: ToolChoiceParam | undefined =
145
- toolChoice == null
146
- ? undefined
147
- : toolChoice.type === 'tool'
148
- ? { type: 'function', name: toolChoice.toolName }
149
- : toolChoice.type; // 'auto' | 'none' | 'required'
214
+ let convertedToolChoice: ToolChoiceParam | undefined;
215
+ if (toolChoice?.type === 'tool') {
216
+ const registeredTool = encodedProviderToolsByName.get(
217
+ toolChoice.toolName,
218
+ );
219
+
220
+ if (registeredTool == null) {
221
+ if (!providerToolsByName.has(toolChoice.toolName)) {
222
+ convertedToolChoice = {
223
+ type: 'function',
224
+ name: toolChoice.toolName,
225
+ };
226
+ }
227
+ } else {
228
+ const { encodeToolChoice, tool, toolType } = registeredTool;
229
+ let fields: unknown = {};
230
+
231
+ try {
232
+ fields = await encodeToolChoice?.({
233
+ name: tool.name,
234
+ args: tool.args,
235
+ });
236
+ } catch {
237
+ fields = undefined;
238
+ }
239
+
240
+ if (encodeToolChoice != null && !isOpenResponsesJSONObject(fields)) {
241
+ warnings.push({
242
+ type: 'unsupported',
243
+ feature: `tool choice for provider-defined tool ${tool.id}`,
244
+ });
245
+ } else {
246
+ convertedToolChoice = {
247
+ ...(isOpenResponsesJSONObject(fields) ? fields : {}),
248
+ type: toolType,
249
+ };
250
+ }
251
+ }
252
+ } else {
253
+ convertedToolChoice = toolChoice?.type;
254
+ }
150
255
 
151
256
  const textFormat =
152
257
  responseFormat?.type === 'json'
@@ -209,7 +314,7 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
209
314
  }),
210
315
  }
211
316
  : undefined,
212
- tools: functionTools.length ? functionTools : undefined,
317
+ tools: convertedTools.length ? convertedTools : undefined,
213
318
  tool_choice: convertedToolChoice,
214
319
  ...(textFormat != null && { text: { format: textFormat } }),
215
320
  },
@@ -335,6 +440,25 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
335
440
  });
336
441
  break;
337
442
  }
443
+
444
+ default: {
445
+ if (!isOpenResponsesExtensionItem(part)) {
446
+ break;
447
+ }
448
+
449
+ const decoded = await decodeExtensionItem({
450
+ extensionRegistry: this.extensionRegistry,
451
+ item: part,
452
+ mode: 'generate',
453
+ providerOptionsName: this.config.providerOptionsName,
454
+ });
455
+
456
+ if (decoded != null) {
457
+ content.push(...decoded);
458
+ hasToolCalls ||= decoded.some(part => part.type === 'tool-call');
459
+ }
460
+ break;
461
+ }
338
462
  }
339
463
  }
340
464
 
@@ -454,6 +578,8 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
454
578
  { toolName?: string; toolCallId?: string; arguments?: string }
455
579
  >();
456
580
  const providerOptionsName = this.config.providerOptionsName;
581
+ const extensionRegistry = this.extensionRegistry;
582
+ const extensionStreamState = new Map<string, unknown>();
457
583
 
458
584
  return {
459
585
  stream: response.pipeThrough(
@@ -465,7 +591,7 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
465
591
  controller.enqueue({ type: 'stream-start', warnings });
466
592
  },
467
593
 
468
- transform(parseResult, controller) {
594
+ async transform(parseResult, controller) {
469
595
  if (options.includeRawChunks) {
470
596
  controller.enqueue({
471
597
  type: 'raw',
@@ -480,6 +606,27 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
480
606
 
481
607
  const chunk = parseResult.value;
482
608
 
609
+ if (isOpenResponsesExtensionEvent(chunk)) {
610
+ const extension = extensionRegistry.byEventType.get(chunk.type);
611
+ if (extension?.decodeEvent != null) {
612
+ try {
613
+ const decoded = await extension.decodeEvent({
614
+ event: chunk,
615
+ state: extensionStreamState,
616
+ });
617
+ for (const part of decoded ?? []) {
618
+ controller.enqueue(part);
619
+ hasToolCalls ||=
620
+ part.type === 'tool-call' ||
621
+ part.type === 'tool-input-start';
622
+ }
623
+ } catch (error) {
624
+ controller.enqueue({ type: 'error', error });
625
+ }
626
+ }
627
+ return;
628
+ }
629
+
483
630
  // Tool call events (single-shot tool-call when complete)
484
631
  if (
485
632
  chunk.type === 'response.output_item.added' &&
@@ -540,6 +687,25 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
540
687
  hasToolCalls = true;
541
688
 
542
689
  toolCallsByItemId.delete(chunk.item.id);
690
+ } else if (
691
+ chunk.type === 'response.output_item.done' &&
692
+ isOpenResponsesExtensionItem(chunk.item)
693
+ ) {
694
+ try {
695
+ const decoded = await decodeExtensionItem({
696
+ extensionRegistry,
697
+ item: chunk.item,
698
+ mode: 'stream',
699
+ providerOptionsName,
700
+ });
701
+
702
+ for (const part of decoded ?? []) {
703
+ controller.enqueue(part);
704
+ hasToolCalls ||= part.type === 'tool-call';
705
+ }
706
+ } catch (error) {
707
+ controller.enqueue({ type: 'error', error });
708
+ }
543
709
  }
544
710
 
545
711
  // Reasoning events (note: response.reasoning_text.delta is an LM Studio extension, not in official spec)
@@ -553,8 +719,9 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
553
719
  });
554
720
  activeReasoningId = chunk.item.id;
555
721
  } else if (
722
+ chunk.type === 'response.reasoning_summary_text.delta' ||
556
723
  (chunk as { type: string }).type ===
557
- 'response.reasoning_text.delta'
724
+ 'response.reasoning_text.delta'
558
725
  ) {
559
726
  const reasoningChunk = chunk as {
560
727
  item_id: string;
@@ -631,6 +798,29 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
631
798
  raw: chunk.response.error?.code ?? chunk.response.status,
632
799
  };
633
800
  updateUsage(chunk.response.usage);
801
+ if (chunk.response.error != null) {
802
+ controller.enqueue({
803
+ type: 'error',
804
+ error: createOpenResponsesStreamError({
805
+ type: chunk.type,
806
+ error: chunk.response.error,
807
+ data: chunk,
808
+ }),
809
+ });
810
+ }
811
+ } else if (chunk.type === 'error') {
812
+ finishReason = {
813
+ unified: 'error',
814
+ raw: chunk.error.code,
815
+ };
816
+ controller.enqueue({
817
+ type: 'error',
818
+ error: createOpenResponsesStreamError({
819
+ type: chunk.type,
820
+ error: chunk.error,
821
+ data: chunk,
822
+ }),
823
+ });
634
824
  }
635
825
  },
636
826
 
@@ -657,6 +847,23 @@ export class OpenResponsesLanguageModel implements LanguageModelV4 {
657
847
  }
658
848
  }
659
849
 
850
+ function createOpenResponsesStreamError({
851
+ type,
852
+ error,
853
+ data,
854
+ }: {
855
+ type: 'error' | 'response.failed';
856
+ error: { message: string; code: string };
857
+ data: unknown;
858
+ }) {
859
+ return createProviderStreamError({
860
+ message: error.message,
861
+ type,
862
+ code: error.code,
863
+ data,
864
+ });
865
+ }
866
+
660
867
  function createReasoningProviderMetadata({
661
868
  part,
662
869
  providerOptionsName,
@@ -687,6 +894,95 @@ function createReasoningProviderMetadata({
687
894
  };
688
895
  }
689
896
 
897
+ async function decodeExtensionItem({
898
+ extensionRegistry,
899
+ item,
900
+ mode,
901
+ providerOptionsName,
902
+ }: {
903
+ extensionRegistry: OpenResponsesExtensionRegistry;
904
+ item: OpenResponsesExtensionItem;
905
+ mode: 'generate' | 'stream';
906
+ providerOptionsName: string;
907
+ }): Promise<OpenResponsesExtensionContentPart[] | undefined> {
908
+ const extension = extensionRegistry.byItemType.get(item.type);
909
+ if (extension == null) {
910
+ return undefined;
911
+ }
912
+
913
+ const decoded = await extension.decodeItem({ item, mode });
914
+ if (decoded == null) {
915
+ return undefined;
916
+ }
917
+
918
+ return [
919
+ createExtensionReplayCarrier({
920
+ extension,
921
+ item,
922
+ providerOptionsName,
923
+ }),
924
+ ...decoded.map(part =>
925
+ addExtensionItemReferenceMetadata({
926
+ extension,
927
+ item,
928
+ part,
929
+ providerOptionsName,
930
+ }),
931
+ ),
932
+ ];
933
+ }
934
+
935
+ function createExtensionReplayCarrier({
936
+ extension,
937
+ item,
938
+ providerOptionsName,
939
+ }: {
940
+ extension: OpenResponsesExtension;
941
+ item: OpenResponsesExtensionItem;
942
+ providerOptionsName: string;
943
+ }): OpenResponsesExtensionContentPart {
944
+ return {
945
+ type: 'custom',
946
+ kind: 'open-responses.extension-replay',
947
+ providerMetadata: {
948
+ [providerOptionsName]: {
949
+ openResponsesExtension: {
950
+ id: extension.id,
951
+ item,
952
+ },
953
+ },
954
+ },
955
+ };
956
+ }
957
+
958
+ function addExtensionItemReferenceMetadata({
959
+ extension,
960
+ item,
961
+ part,
962
+ providerOptionsName,
963
+ }: {
964
+ extension: OpenResponsesExtension;
965
+ item: OpenResponsesExtensionItem;
966
+ part: OpenResponsesExtensionContentPart;
967
+ providerOptionsName: string;
968
+ }): OpenResponsesExtensionContentPart {
969
+ const providerMetadata = part.providerMetadata ?? {};
970
+
971
+ return {
972
+ ...part,
973
+ providerMetadata: {
974
+ ...providerMetadata,
975
+ [providerOptionsName]: {
976
+ ...providerMetadata[providerOptionsName],
977
+ openResponsesExtension: {
978
+ id: extension.id,
979
+ itemId: item.id,
980
+ },
981
+ },
982
+ },
983
+ };
984
+ }
985
+
690
986
  function getOutputTextAnnotations(value: unknown): Annotation[] {
691
987
  if (
692
988
  value == null ||