@ai-sdk/openai 4.0.108 → 4.0.110

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/index.d.ts +79 -15
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +788 -423
  5. package/dist/index.js.map +1 -1
  6. package/dist/internal/index.d.ts +389 -9
  7. package/dist/internal/index.d.ts.map +1 -1
  8. package/dist/internal/index.js +629 -380
  9. package/dist/internal/index.js.map +1 -1
  10. package/docs/03-openai.mdx +224 -18
  11. package/package.json +3 -3
  12. package/src/chat/convert-to-openai-chat-messages.ts +29 -4
  13. package/src/chat/map-openai-finish-reason.ts +11 -4
  14. package/src/chat/openai-chat-api.ts +36 -0
  15. package/src/chat/openai-chat-language-model-options.ts +13 -0
  16. package/src/chat/openai-chat-language-model.ts +111 -22
  17. package/src/chat/openai-chat-prompt.ts +4 -0
  18. package/src/image/openai-image-model-options.ts +48 -0
  19. package/src/image/openai-image-model.ts +49 -4
  20. package/src/index.ts +1 -0
  21. package/src/internal/index.ts +4 -0
  22. package/src/live/openai-realtime-model-live.ts +21 -2
  23. package/src/openai-batch.ts +8 -0
  24. package/src/openai-model-catalog.ts +3 -1
  25. package/src/openai-tools.ts +4 -3
  26. package/src/realtime/openai-realtime-event-mapper.ts +172 -11
  27. package/src/realtime/openai-realtime-model.ts +57 -3
  28. package/src/responses/map-openai-responses-finish-reason.ts +7 -1
  29. package/src/responses/openai-responses-api.ts +90 -49
  30. package/src/responses/openai-responses-language-model.ts +56 -1
  31. package/src/responses/openai-responses-prepare-tools.ts +18 -1
  32. package/src/tool/code-interpreter.ts +38 -2
  33. package/src/tool/mcp.ts +27 -8
  34. package/src/transcription/openai-transcription-model.ts +43 -8
@@ -53,6 +53,15 @@ import {
53
53
  import { prepareChatTools } from './openai-chat-prepare-tools';
54
54
  import { normalizeOpenAIJsonSchema } from '../normalize-openai-json-schema';
55
55
 
56
+ type OpenAIChatLogprob = NonNullable<
57
+ NonNullable<
58
+ Extract<
59
+ OpenAIChatChunk,
60
+ { choices: unknown }
61
+ >['choices'][number]['logprobs']
62
+ >['content']
63
+ >[number];
64
+
56
65
  type OpenAIChatConfig = {
57
66
  provider: string;
58
67
  /** Model ID sent to the API. Defaults to the model's reported ID. */
@@ -178,16 +187,15 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
178
187
  });
179
188
  }
180
189
 
181
- const { messages, warnings: messageWarnings } = convertToOpenAIChatMessages(
182
- {
190
+ const { messages, warnings: messageWarnings } =
191
+ await convertToOpenAIChatMessages({
183
192
  prompt,
184
193
  systemMessageMode:
185
194
  openaiOptions.systemMessageMode ??
186
195
  (isReasoningModel
187
196
  ? 'developer'
188
197
  : modelRequestConfig.systemMessageMode),
189
- },
190
- );
198
+ });
191
199
 
192
200
  warnings.push(...messageWarnings);
193
201
 
@@ -414,16 +422,17 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
414
422
  options: LanguageModelV4CallOptions,
415
423
  ): Promise<LanguageModelV4GenerateResult> {
416
424
  const { args: body, warnings } = await this.getArgs(options);
425
+ const url = this.config.url({
426
+ path: '/chat/completions',
427
+ modelId: this.config.requestModelId ?? this.modelId,
428
+ });
417
429
 
418
430
  const {
419
431
  responseHeaders,
420
432
  value: response,
421
433
  rawValue: rawResponse,
422
434
  } = await postJsonToApi({
423
- url: this.config.url({
424
- path: '/chat/completions',
425
- modelId: this.config.requestModelId ?? this.modelId,
426
- }),
435
+ url,
427
436
  headers: combineHeaders(
428
437
  await resolve(this.config.headers),
429
438
  options.headers,
@@ -447,7 +456,10 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
447
456
 
448
457
  const content: Array<LanguageModelV4Content> = [];
449
458
 
450
- // text content:
459
+ // text content. When the model refuses to answer, the API returns the
460
+ // refusal in `refusal` and sets `content` to null, so surface it as text
461
+ // instead of dropping it. Content and refusal are both returned when the
462
+ // model refuses after answering, so keep both:
451
463
  const text =
452
464
  choice.message.content != null && choice.message.content.length > 0
453
465
  ? choice.message.content
@@ -456,6 +468,12 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
456
468
  content.push({ type: 'text', text });
457
469
  }
458
470
 
471
+ const refusal = choice.message.refusal;
472
+ const hasRefusal = refusal != null && refusal.length > 0;
473
+ if (hasRefusal) {
474
+ content.push({ type: 'text', text: refusal });
475
+ }
476
+
459
477
  // tool calls:
460
478
  for (const toolCall of choice.message.tool_calls ?? []) {
461
479
  content.push({
@@ -488,18 +506,29 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
488
506
  providerMetadata.openai.rejectedPredictionTokens =
489
507
  completionTokenDetails?.rejected_prediction_tokens;
490
508
  }
491
- if (choice.logprobs?.content != null) {
492
- providerMetadata.openai.logprobs = choice.logprobs.content;
509
+ // logprobs either follow the text content (content) or the refusal, so
510
+ // combine them in the order the text is emitted above. Explicitly empty
511
+ // logprobs are reported as an empty array, as before:
512
+ const contentLogprobs = choice.logprobs?.content;
513
+ const refusalLogprobs = choice.logprobs?.refusal;
514
+ if (contentLogprobs != null || refusalLogprobs != null) {
515
+ providerMetadata.openai.logprobs = [
516
+ ...(contentLogprobs ?? []),
517
+ ...(refusalLogprobs ?? []),
518
+ ];
493
519
  }
494
520
 
495
521
  return {
496
522
  content,
497
523
  finishReason: {
498
- unified: mapOpenAIFinishReason(choice.finish_reason),
524
+ unified: mapOpenAIFinishReason({
525
+ finishReason: choice.finish_reason,
526
+ hasRefusal,
527
+ }),
499
528
  raw: choice.finish_reason ?? undefined,
500
529
  },
501
530
  usage: convertOpenAIChatUsage(response.usage),
502
- request: { body },
531
+ request: { body, url },
503
532
  response: {
504
533
  ...getResponseMetadata(response),
505
534
  headers: responseHeaders,
@@ -562,6 +591,13 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
562
591
  let usage: OpenAIChatUsage | undefined = undefined;
563
592
  let metadataExtracted = false;
564
593
  let isActiveText = false;
594
+ let hasRefusal = false;
595
+ let rawFinishReason: string | undefined = undefined;
596
+ let hasLogprobs = false;
597
+
598
+ // logprobs of all streamed chunks (content token logprobs followed by
599
+ // refusal token logprobs), collected across chunks:
600
+ const streamedLogprobs: OpenAIChatLogprob[] = [];
565
601
 
566
602
  const providerMetadata: SharedV4ProviderMetadata = { openai: {} };
567
603
 
@@ -587,6 +623,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
587
623
  // handle failed chunk parsing / validation:
588
624
  if (!chunk.success) {
589
625
  finishReason = { unified: 'error', raw: undefined };
626
+ rawFinishReason = undefined;
590
627
  controller.enqueue({ type: 'error', error: chunk.error });
591
628
  return;
592
629
  }
@@ -596,6 +633,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
596
633
  // handle error chunks:
597
634
  if ('error' in value) {
598
635
  finishReason = { unified: 'error', raw: undefined };
636
+ rawFinishReason = undefined;
599
637
  controller.enqueue({
600
638
  type: 'error',
601
639
  error:
@@ -639,15 +677,29 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
639
677
 
640
678
  const choice = value.choices[0];
641
679
 
680
+ // store the raw finish reason and map it in `flush`, after all
681
+ // chunks have been read: some servers send the last delta (which
682
+ // can contain a refusal) in the same chunk as the finish reason,
683
+ // and a refusal turns `stop` into `content-filter`:
642
684
  if (choice?.finish_reason != null) {
643
- finishReason = {
644
- unified: mapOpenAIFinishReason(choice.finish_reason),
645
- raw: choice.finish_reason,
646
- };
685
+ rawFinishReason = choice.finish_reason;
647
686
  }
648
687
 
649
- if (choice?.logprobs?.content != null) {
650
- providerMetadata.openai.logprobs = choice.logprobs.content;
688
+ // logprobs either follow the text content (content) or the refusal.
689
+ // They are streamed token by token, so collect the logprobs of all
690
+ // chunks (content logprobs followed by refusal logprobs) and report
691
+ // the complete list at the end of the stream:
692
+ if (choice?.logprobs != null) {
693
+ const contentLogprobs = choice.logprobs.content;
694
+ const refusalLogprobs = choice.logprobs.refusal;
695
+
696
+ if (contentLogprobs != null || refusalLogprobs != null) {
697
+ hasLogprobs = true;
698
+ streamedLogprobs.push(
699
+ ...(contentLogprobs ?? []),
700
+ ...(refusalLogprobs ?? []),
701
+ );
702
+ }
651
703
  }
652
704
 
653
705
  if (choice?.delta == null) {
@@ -656,7 +708,24 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
656
708
 
657
709
  const delta = choice.delta;
658
710
 
659
- if (delta.content != null) {
711
+ // When the model refuses to answer, the API streams the refusal in
712
+ // `refusal` instead of `content`. Content and refusal deltas can
713
+ // both occur, so append each to the same text part instead of
714
+ // dropping one:
715
+ const textDeltas: string[] = [];
716
+ if (delta.content != null && delta.content.length > 0) {
717
+ textDeltas.push(delta.content);
718
+ }
719
+ if (delta.refusal != null && delta.refusal.length > 0) {
720
+ hasRefusal = true;
721
+ textDeltas.push(delta.refusal);
722
+ }
723
+ // empty `content: ""` deltas keep their previous behavior:
724
+ if (textDeltas.length === 0 && delta.content === '') {
725
+ textDeltas.push(delta.content);
726
+ }
727
+
728
+ for (const textDelta of textDeltas) {
660
729
  if (!isActiveText) {
661
730
  controller.enqueue({ type: 'text-start', id: '0' });
662
731
  isActiveText = true;
@@ -665,7 +734,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
665
734
  controller.enqueue({
666
735
  type: 'text-delta',
667
736
  id: '0',
668
- delta: delta.content,
737
+ delta: textDelta,
669
738
  });
670
739
  }
671
740
 
@@ -696,6 +765,25 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
696
765
 
697
766
  toolCallTracker.flush();
698
767
 
768
+ // map the stored finish reason now that all deltas (including
769
+ // refusals that arrived in the same chunk as the finish reason)
770
+ // have been read:
771
+ if (rawFinishReason != null) {
772
+ finishReason = {
773
+ unified: mapOpenAIFinishReason({
774
+ finishReason: rawFinishReason,
775
+ hasRefusal,
776
+ }),
777
+ raw: rawFinishReason,
778
+ };
779
+ }
780
+
781
+ // report the logprobs of all streamed chunks (content logprobs
782
+ // followed by refusal logprobs), not only the last chunk:
783
+ if (hasLogprobs) {
784
+ providerMetadata.openai.logprobs = streamedLogprobs;
785
+ }
786
+
699
787
  controller.enqueue({
700
788
  type: 'finish',
701
789
  finishReason,
@@ -705,7 +793,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
705
793
  },
706
794
  }),
707
795
  ),
708
- request: { body },
796
+ request: { body, url },
709
797
  response: { headers: responseHeaders },
710
798
  };
711
799
 
@@ -723,6 +811,7 @@ function isOpenAIChatOutputChunk(chunk: OpenAIChatChunk): boolean {
723
811
 
724
812
  return (
725
813
  (delta?.content != null && delta.content.length > 0) ||
814
+ (delta?.refusal != null && delta.refusal.length > 0) ||
726
815
  (delta?.tool_calls != null && delta.tool_calls.length > 0) ||
727
816
  (delta?.annotations != null && delta.annotations.length > 0)
728
817
  );
@@ -8,16 +8,19 @@ export type ChatCompletionMessage =
8
8
  | ChatCompletionToolMessage;
9
9
 
10
10
  export interface ChatCompletionSystemMessage {
11
+ name?: string;
11
12
  role: 'system';
12
13
  content: string | Array<ChatCompletionContentPartText>;
13
14
  }
14
15
 
15
16
  export interface ChatCompletionDeveloperMessage {
17
+ name?: string;
16
18
  role: 'developer';
17
19
  content: string | Array<ChatCompletionContentPartText>;
18
20
  }
19
21
 
20
22
  export interface ChatCompletionUserMessage {
23
+ name?: string;
21
24
  role: 'user';
22
25
  content: string | Array<ChatCompletionContentPart>;
23
26
  }
@@ -53,6 +56,7 @@ export interface ChatCompletionContentPartFile {
53
56
  }
54
57
 
55
58
  export interface ChatCompletionAssistantMessage {
59
+ name?: string;
56
60
  role: 'assistant';
57
61
  content?: string | null | Array<ChatCompletionContentPartText>;
58
62
  tool_calls?: Array<ChatCompletionMessageToolCall>;
@@ -16,6 +16,8 @@ export type OpenAIImageModelId =
16
16
  | 'gpt-image-1-mini'
17
17
  | 'gpt-image-1.5'
18
18
  | 'gpt-image-2'
19
+ | 'gpt-image-2.6-flare'
20
+ | 'gpt-image-2.6-sunburst'
19
21
  | 'gpt-image-2.5-flare'
20
22
  | 'gpt-image-2.5-flare-2026-09-08'
21
23
  | 'gpt-image-2.5-sunburst'
@@ -64,6 +66,12 @@ const baseImageModelOptionsObject = () =>
64
66
  */
65
67
  outputFormat: z.enum(['png', 'jpeg', 'webp']).optional(),
66
68
 
69
+ /**
70
+ * How much detail the model puts into the image. Supported by GPT Image
71
+ * 2.6 models. The API defaults to `high`.
72
+ */
73
+ complexity: z.enum(['low', 'medium', 'high']).optional(),
74
+
67
75
  /**
68
76
  * Compression level (0-100) for the generated image(s). Applies to the
69
77
  * `jpeg` and `webp` output formats.
@@ -75,6 +83,13 @@ const baseImageModelOptionsObject = () =>
75
83
  * to monitor and detect abuse.
76
84
  */
77
85
  user: z.string().optional(),
86
+
87
+ /**
88
+ * A stable identifier used to help detect users of your application that
89
+ * may be violating OpenAI's usage policies. Should be a hashed username
90
+ * or email address.
91
+ */
92
+ safetyIdentifier: z.string().max(128).optional(),
78
93
  });
79
94
 
80
95
  export const openaiImageModelOptions = lazySchema(() =>
@@ -95,6 +110,25 @@ export const openaiImageModelGenerationOptions = lazySchema(() =>
95
110
  */
96
111
  style: z.enum(['vivid', 'natural']).optional(),
97
112
 
113
+ /**
114
+ * Format in which the generated image(s) are returned. `gif` produces
115
+ * an animated GIF and is supported by GPT Image 2.6 models.
116
+ */
117
+ outputFormat: z.enum(['png', 'jpeg', 'webp', 'gif']).optional(),
118
+
119
+ /**
120
+ * Number of frames (2-64) of an animated GIF. Only applies to the `gif`
121
+ * output format. The API defaults to 9.
122
+ */
123
+ frameCount: z.number().int().min(2).max(64).optional(),
124
+
125
+ /**
126
+ * Duration of each frame in milliseconds (20-2000) of an animated GIF.
127
+ * The API rounds it to the nearest 10 ms. Only applies to the `gif`
128
+ * output format. The API defaults to 200.
129
+ */
130
+ frameDurationMs: z.number().int().min(20).max(2000).optional(),
131
+
98
132
  /**
99
133
  * Content moderation level for the generated image(s). `low` applies
100
134
  * less restrictive filtering.
@@ -115,6 +149,20 @@ export const openaiImageModelEditOptions = lazySchema(() =>
115
149
  * Fidelity of the output image(s) to the input image(s).
116
150
  */
117
151
  inputFidelity: z.enum(['high', 'low']).optional(),
152
+
153
+ /**
154
+ * A transparent PNG overlaid on the first input image to mark up the
155
+ * regions to change, as an HTTPS URL or a base64 data URL. It must match
156
+ * the dimensions of the first image. Supported by GPT Image 2.5 and 2.6
157
+ * models. Cannot be combined with a `mask`.
158
+ */
159
+ markup: z.string().optional(),
160
+
161
+ /**
162
+ * Content moderation level for the edited image(s). `low` applies
163
+ * less restrictive filtering. Supported by GPT Image models.
164
+ */
165
+ moderation: z.enum(['auto', 'low']).optional(),
118
166
  }),
119
167
  ),
120
168
  );
@@ -1,7 +1,8 @@
1
- import type {
2
- ImageModelV4,
3
- ImageModelV4File,
4
- SharedV4Warning,
1
+ import {
2
+ InvalidArgumentError,
3
+ type ImageModelV4,
4
+ type ImageModelV4File,
5
+ type SharedV4Warning,
5
6
  } from '@ai-sdk/provider';
6
7
  import {
7
8
  resolve,
@@ -128,6 +129,13 @@ export class OpenAIImageModel implements ImageModelV4 {
128
129
  schema: openaiImageModelEditOptions,
129
130
  })) ?? {};
130
131
 
132
+ if (mask != null && openaiOptions.markup != null) {
133
+ throw new InvalidArgumentError({
134
+ argument: 'providerOptions.openai.markup',
135
+ message: '`markup` cannot be combined with `mask`.',
136
+ });
137
+ }
138
+
131
139
  const { value: response, responseHeaders } = await postFormDataToApi({
132
140
  url: this.config.url({
133
141
  path: '/images/edits',
@@ -156,14 +164,21 @@ export class OpenAIImageModel implements ImageModelV4 {
156
164
  ),
157
165
  ),
158
166
  mask: mask != null ? await fileToBlob(mask, abortSignal) : undefined,
167
+ markup:
168
+ openaiOptions.markup != null
169
+ ? await markupToBlob(openaiOptions.markup, abortSignal)
170
+ : undefined,
159
171
  n,
160
172
  size,
161
173
  quality: openaiOptions.quality,
162
174
  background: openaiOptions.background,
163
175
  output_format: openaiOptions.outputFormat,
164
176
  output_compression: openaiOptions.outputCompression,
177
+ complexity: openaiOptions.complexity,
165
178
  input_fidelity: openaiOptions.inputFidelity,
179
+ moderation: openaiOptions.moderation,
166
180
  user: openaiOptions.user,
181
+ safety_identifier: openaiOptions.safetyIdentifier,
167
182
  }),
168
183
  failedResponseHandler: openaiFailedResponseHandler,
169
184
  successfulResponseHandler: createJsonResponseHandler(
@@ -235,7 +250,11 @@ export class OpenAIImageModel implements ImageModelV4 {
235
250
  moderation: openaiOptions.moderation,
236
251
  output_format: openaiOptions.outputFormat,
237
252
  output_compression: openaiOptions.outputCompression,
253
+ complexity: openaiOptions.complexity,
254
+ frame_count: openaiOptions.frameCount,
255
+ frame_duration_ms: openaiOptions.frameDurationMs,
238
256
  user: openaiOptions.user,
257
+ safety_identifier: openaiOptions.safetyIdentifier,
239
258
  ...(!hasDefaultResponseFormat(this.modelId)
240
259
  ? { response_format: 'b64_json' }
241
260
  : {}),
@@ -326,16 +345,42 @@ type OpenAIImageEditInput = {
326
345
  prompt?: string;
327
346
  image: Blob | Blob[];
328
347
  mask?: Blob;
348
+ markup?: Blob;
329
349
  n?: number;
330
350
  size?: `${number}x${number}`;
331
351
  quality?: OpenAIImageModelEditOptions['quality'];
332
352
  background?: OpenAIImageModelEditOptions['background'];
333
353
  output_format?: OpenAIImageModelEditOptions['outputFormat'];
334
354
  output_compression?: OpenAIImageModelEditOptions['outputCompression'];
355
+ complexity?: OpenAIImageModelEditOptions['complexity'];
335
356
  input_fidelity?: OpenAIImageModelEditOptions['inputFidelity'];
357
+ moderation?: OpenAIImageModelEditOptions['moderation'];
336
358
  user?: OpenAIImageModelEditOptions['user'];
359
+ safety_identifier?: OpenAIImageModelEditOptions['safetyIdentifier'];
337
360
  };
338
361
 
362
+ async function markupToBlob(
363
+ markup: string,
364
+ abortSignal: AbortSignal | undefined,
365
+ ): Promise<Blob> {
366
+ const dataUrl = /^data:([^;,]*)(;base64)?,(.*)$/s.exec(markup);
367
+
368
+ if (dataUrl == null) {
369
+ return downloadBlob(markup, { abortSignal });
370
+ }
371
+
372
+ if (dataUrl[2] == null) {
373
+ throw new InvalidArgumentError({
374
+ argument: 'providerOptions.openai.markup',
375
+ message: '`markup` data URLs must be base64 encoded.',
376
+ });
377
+ }
378
+
379
+ return new Blob([convertBase64ToUint8Array(dataUrl[3]) as BlobPart], {
380
+ type: dataUrl[1] || 'image/png',
381
+ });
382
+ }
383
+
339
384
  async function fileToBlob(
340
385
  file: ImageModelV4File | undefined,
341
386
  abortSignal: AbortSignal | undefined,
package/src/index.ts CHANGED
@@ -22,6 +22,7 @@ export type {
22
22
  export type { OpenAIToolOptions } from './responses/openai-responses-prepare-tools';
23
23
  export type {
24
24
  OpenAILanguageModelChatOptions,
25
+ OpenAIChatMessageProviderOptions,
25
26
  /** @deprecated Use `OpenAILanguageModelChatOptions` instead. */
26
27
  OpenAILanguageModelChatOptions as OpenAIChatLanguageModelOptions,
27
28
  } from './chat/openai-chat-language-model-options';
@@ -13,10 +13,14 @@ export * from '../speech/openai-speech-model-options';
13
13
  export * from '../responses/openai-responses-language-model';
14
14
  export * from '../responses/openai-responses-provider-metadata';
15
15
  export * from '../tool/apply-patch';
16
+ export * from '../tool/custom';
16
17
  export * from '../tool/code-interpreter';
17
18
  export * from '../tool/file-search';
18
19
  export * from '../tool/image-generation';
19
20
  export * from '../tool/programmatic-tool-calling';
21
+ export * from '../tool/shell';
20
22
  export * from '../tool/tool-search';
21
23
  export * from '../tool/web-search';
22
24
  export * from '../tool/web-search-preview';
25
+
26
+ export { openaiModelCatalog } from '../openai-model-catalog';
@@ -2,6 +2,7 @@ import {
2
2
  type Experimental_RealtimeModelV4 as RealtimeModelV4,
3
3
  type Experimental_RealtimeModelV4ClientEvent as RealtimeModelV4ClientEvent,
4
4
  type Experimental_RealtimeModelV4SessionConfig as RealtimeModelV4SessionConfig,
5
+ type Experimental_RealtimeModelV4SessionIntent as RealtimeModelV4SessionIntent,
5
6
  } from '@ai-sdk/provider';
6
7
  import {
7
8
  resolveWith,
@@ -54,7 +55,10 @@ export class OpenAIRealtimeModelLive implements RealtimeModelV4 {
54
55
  return { dataChannelLabel: 'oai-events' };
55
56
  }
56
57
 
57
- getServerWebSocketConfig() {
58
+ getServerWebSocketConfig(options?: {
59
+ intent?: RealtimeModelV4SessionIntent;
60
+ }) {
61
+ this.assertConversationIntent(options);
58
62
  return resolveWith(this.config.headers, configHeaders => {
59
63
  const url = new URL(`${this.config.baseURL}/live/sessions`);
60
64
  url.protocol = url.protocol === 'http:' ? 'ws:' : 'wss:';
@@ -103,13 +107,28 @@ export class OpenAIRealtimeModelLive implements RealtimeModelV4 {
103
107
  return createOpenAILiveServerEventParser();
104
108
  }
105
109
 
106
- serializeClientEvent(event: RealtimeModelV4ClientEvent): unknown {
110
+ private assertConversationIntent(options?: {
111
+ intent?: RealtimeModelV4SessionIntent;
112
+ }): void {
113
+ if (options?.intent != null && options.intent !== 'conversation')
114
+ throw new Error(
115
+ `OpenAI Live session intent '${options.intent}' is not supported.`,
116
+ );
117
+ }
118
+
119
+ serializeClientEvent(
120
+ event: RealtimeModelV4ClientEvent,
121
+ options?: { intent?: RealtimeModelV4SessionIntent },
122
+ ): unknown {
123
+ this.assertConversationIntent(options);
107
124
  return serializeOpenAILiveClientEvent(event, this.modelId);
108
125
  }
109
126
 
110
127
  buildSessionConfig(
111
128
  config: RealtimeModelV4SessionConfig,
129
+ options?: { intent?: RealtimeModelV4SessionIntent },
112
130
  ): Record<string, unknown> {
131
+ this.assertConversationIntent(options);
113
132
  return buildOpenAILiveSessionConfig(config, this.modelId);
114
133
  }
115
134
  }
@@ -748,6 +748,7 @@ async function convertOpenAIBatchResult(
748
748
  const content: LanguageModelV4GenerateResult['content'] = [];
749
749
  const logprobs: Array<NonNullable<OpenAIResponsesLogprobs>> = [];
750
750
  let hasFunctionCall = false;
751
+ let hasRefusal = false;
751
752
 
752
753
  for (const part of response.output) {
753
754
  switch (part.type) {
@@ -774,6 +775,12 @@ async function convertOpenAIBatchResult(
774
775
 
775
776
  case 'message': {
776
777
  for (const contentPart of part.content) {
778
+ if (contentPart.type === 'refusal') {
779
+ hasRefusal = true;
780
+ content.push({ type: 'text', text: contentPart.refusal });
781
+ continue;
782
+ }
783
+
777
784
  content.push({ type: 'text', text: contentPart.text });
778
785
  if (contentPart.logprobs != null) {
779
786
  logprobs.push(contentPart.logprobs);
@@ -916,6 +923,7 @@ async function convertOpenAIBatchResult(
916
923
  unified: mapOpenAIResponseFinishReason({
917
924
  finishReason: response.incomplete_details?.reason,
918
925
  hasFunctionCall,
926
+ hasRefusal,
919
927
  }),
920
928
  raw: response.incomplete_details?.reason ?? undefined,
921
929
  },
@@ -461,6 +461,8 @@ function createGptImageEntry(
461
461
 
462
462
  const openaiImageModelEntries: Readonly<Record<string, OpenAIImageModelEntry>> =
463
463
  {
464
+ 'gpt-image-2.6-flare': createGptImageEntry(customImageSizes),
465
+ 'gpt-image-2.6-sunburst': createGptImageEntry(customImageSizes),
464
466
  'gpt-image-2.5-flare': createGptImageEntry(customImageSizes),
465
467
  'gpt-image-2.5-sunburst': createGptImageEntry(customImageSizes),
466
468
  'gpt-image-2': createGptImageEntry(gptImageSizes),
@@ -757,7 +759,7 @@ const realtimeModelEntry = {
757
759
  realtime: {
758
760
  conversation: 'turn-based',
759
761
  transports: ['websocket'],
760
- connections: ['client-secret-websocket'],
762
+ connections: ['client-secret-websocket', 'server-websocket'],
761
763
  },
762
764
  },
763
765
  requestConfig: {
@@ -126,16 +126,17 @@ export const openaiTools = {
126
126
 
127
127
  /**
128
128
  * MCP (Model Context Protocol) allows models to call tools exposed by
129
- * remote MCP servers or service connectors.
129
+ * remote MCP servers, service connectors, or Secure MCP tunnels.
130
130
  *
131
131
  * @param serverLabel - Label to identify the MCP server.
132
132
  * @param allowedTools - Allowed tool names or filter object.
133
- * @param authorization - OAuth access token for the MCP server/connector.
133
+ * @param authorization - OAuth access token for the MCP server or connector (including tunneled servers).
134
134
  * @param connectorId - Identifier for a service connector.
135
135
  * @param headers - Optional headers to include in MCP requests.
136
- * // param requireApproval - Approval policy ('always'|'never'|filter object). (Removed - always 'never')
136
+ * @param requireApproval - Approval policy ('always'|'never'|filter object).
137
137
  * @param serverDescription - Optional description of the server.
138
138
  * @param serverUrl - URL for the MCP server.
139
+ * @param tunnelId - Identifier for an OpenAI Secure MCP tunnel.
139
140
  */
140
141
  mcp,
141
142