@zhivex-ai/openai 0.8.0 → 0.9.0

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.
package/dist/index.js CHANGED
@@ -1,5 +1,22 @@
1
1
  import { toJSONSchema, z } from "zod";
2
- import { CallbackRealtimeSession, ConfigurationError, ProviderHTTPError, openWebSocketConnection, hostedTool, providerDataPart, UnsupportedFeatureError, createProviderAdapter, encodeAudioFrame, encodeMediaFrame, isCallableToolDefinition, isHostedToolDefinition, normalizeFinishReason, streamSSE, toToolSet, toolResultPayload, withRetry, withTimeoutSignal } from "@zhivex-ai/core";
2
+ import { createOpenAIImageGenerationModel, normalizeOpenAIImageGenerationCall, normalizeOpenAIImageGenerationPartialImage } from "./image-generation.js";
3
+ import { CallbackRealtimeSession, ConfigurationError, ProviderHTTPError, ProviderResponseTooLargeError, readBodyWithLimit, readErrorBodyWithLimit, readJsonWithLimit, resolveAudioResponseLimits, openWebSocketConnection, hostedTool, providerDataPart, UnsupportedFeatureError, createProviderAdapter, encodeAudioFrame, encodeMediaFrame, isCallableToolDefinition, isHostedToolDefinition, normalizeFinishReason, streamSSE, toToolSet, toolResultPayload, withRetry, withTimeoutSignal } from "@zhivex-ai/core";
4
+ const MIB = 1024 * 1024;
5
+ const DEFAULT_HOSTED_IMAGE_EVENT_BYTES = 32 * MIB;
6
+ const DEFAULT_HOSTED_IMAGE_TOTAL_BYTES = 128 * MIB;
7
+ const HOSTED_IMAGE_EVENT_JSON_OVERHEAD_CHARS = MIB;
8
+ const normalizeOpenAIResponseLimit = (value, fallback, name) => {
9
+ const resolved = value ?? fallback;
10
+ if (!Number.isSafeInteger(resolved) || resolved <= 0) {
11
+ throw new ConfigurationError(`The "${name}" response limit must be a positive safe integer.`);
12
+ }
13
+ return resolved;
14
+ };
15
+ const resolveOpenAIResponseLimits = (limits = {}) => ({
16
+ ...resolveAudioResponseLimits(limits),
17
+ hostedImageEventBytes: normalizeOpenAIResponseLimit(limits.hostedImageEventBytes, DEFAULT_HOSTED_IMAGE_EVENT_BYTES, "hostedImageEventBytes"),
18
+ hostedImageTotalBytes: normalizeOpenAIResponseLimit(limits.hostedImageTotalBytes, DEFAULT_HOSTED_IMAGE_TOTAL_BYTES, "hostedImageTotalBytes")
19
+ });
3
20
  const openAIResponsesToolMetadataKey = "openai.responses_tool_type";
4
21
  const openAIResponsesFunctionConfigMetadataKey = "openai.responses_function_config";
5
22
  const openAIResponsesFunctionConfig = (tool) => {
@@ -27,6 +44,7 @@ const assertOpenAIToolPathInsideRoot = async (rootDir, targetPath, label) => {
27
44
  }
28
45
  return target;
29
46
  };
47
+ export const openAIRealtimeMcpMetadataKey = "openai.realtime_mcp";
30
48
  const capabilities = {
31
49
  streaming: true,
32
50
  tools: true,
@@ -154,17 +172,38 @@ const inferOpenAIRealtimeMode = (modelId, mode) => {
154
172
  }
155
173
  return "conversation";
156
174
  };
157
- const openAIRealtimeSupportsImageInput = (modelId) => /^(?:gpt-realtime|gpt-realtime-mini|gpt-realtime-1\.5|gpt-realtime-2)(?:-\d{4}-\d{2}-\d{2}|@.*)?$/.test(modelId);
175
+ const isOpenAIRealtimeReasoningModel = (modelId) => /^gpt-realtime-2(?:\.1(?:-mini)?)?(?:-\d{4}-\d{2}-\d{2}|@.*)?$/.test(modelId);
176
+ const openAIRealtimeSupportsImageInput = (modelId) => /^(?:gpt-realtime|gpt-realtime-mini|gpt-realtime-1\.5|gpt-realtime-2(?:\.1(?:-mini)?)?)(?:-\d{4}-\d{2}-\d{2}|@.*)?$/.test(modelId);
158
177
  const realtimeCapabilities = (modelId) => ({
159
178
  ...capabilities,
160
179
  streaming: false,
180
+ structuredOutput: false,
181
+ jsonMode: false,
182
+ embeddings: false,
161
183
  audioInput: true,
162
184
  audioOutput: !isOpenAIRealtimeTranscriptionModel(modelId),
163
185
  tools: !isOpenAIRealtimeTranslationModel(modelId) && !isOpenAIRealtimeTranscriptionModel(modelId),
164
186
  toolChoice: !isOpenAIRealtimeTranslationModel(modelId) && !isOpenAIRealtimeTranscriptionModel(modelId),
165
187
  parallelToolCalls: !isOpenAIRealtimeTranslationModel(modelId) && !isOpenAIRealtimeTranscriptionModel(modelId),
166
188
  vision: openAIRealtimeSupportsImageInput(modelId),
167
- reasoning: /^gpt-realtime-2(?:[-@]|$)/.test(modelId),
189
+ reasoning: isOpenAIRealtimeReasoningModel(modelId),
190
+ webSearch: false,
191
+ agentCapabilities: {
192
+ ...capabilities.agentCapabilities,
193
+ supportTier: isOpenAIRealtimeTranslationModel(modelId) || isOpenAIRealtimeTranscriptionModel(modelId) ? "tier-c" : "tier-a",
194
+ toolChoiceNone: !isOpenAIRealtimeTranslationModel(modelId) && !isOpenAIRealtimeTranscriptionModel(modelId),
195
+ approvalRequests: !isOpenAIRealtimeTranslationModel(modelId) && !isOpenAIRealtimeTranscriptionModel(modelId),
196
+ hostedWebSearch: false,
197
+ hostedFileSearch: false,
198
+ remoteMcp: !isOpenAIRealtimeTranslationModel(modelId) && !isOpenAIRealtimeTranscriptionModel(modelId),
199
+ computerUse: false,
200
+ codeExecution: false,
201
+ shell: false,
202
+ applyPatch: false,
203
+ toolSearch: false,
204
+ skills: false,
205
+ toolsets: false
206
+ },
168
207
  realtime: {
169
208
  sessions: true,
170
209
  audioInput: true,
@@ -178,6 +217,28 @@ const jsonHeaders = (apiKey) => ({
178
217
  "content-type": "application/json",
179
218
  authorization: `Bearer ${apiKey}`
180
219
  });
220
+ const resolveOpenAIRealtimeHeaders = (apiKey, providerOptions, includeContentType = false) => {
221
+ const rawHeaders = providerOptions?.headers;
222
+ const customHeaders = rawHeaders && typeof rawHeaders === "object" && !Array.isArray(rawHeaders)
223
+ ? { ...rawHeaders }
224
+ : {};
225
+ const safetyIdentifier = providerOptions?.safety_identifier;
226
+ for (const key of Object.keys(customHeaders)) {
227
+ const normalizedKey = key.toLowerCase();
228
+ if (normalizedKey === "authorization" ||
229
+ normalizedKey === "content-type" ||
230
+ (typeof safetyIdentifier === "string" && safetyIdentifier && normalizedKey === "openai-safety-identifier")) {
231
+ delete customHeaders[key];
232
+ }
233
+ }
234
+ return {
235
+ ...(includeContentType ? jsonHeaders(apiKey) : { authorization: `Bearer ${apiKey}` }),
236
+ ...customHeaders,
237
+ ...(typeof safetyIdentifier === "string" && safetyIdentifier
238
+ ? { "OpenAI-Safety-Identifier": safetyIdentifier }
239
+ : {})
240
+ };
241
+ };
181
242
  const resolveOpenAILanguageRequestOptions = (providerOptions, apiKey) => {
182
243
  const bodyOptions = { ...(providerOptions ?? {}) };
183
244
  const apiMode = bodyOptions.apiMode ?? "auto";
@@ -227,9 +288,12 @@ const toUint8Array = (data) => {
227
288
  return Uint8Array.from(Buffer.from(data, "base64"));
228
289
  };
229
290
  const toBase64 = (data) => typeof data === "string" ? data : Buffer.from(data instanceof Uint8Array ? data : new Uint8Array(data)).toString("base64");
230
- const createAudioFile = (audio) => new File([toUint8Array(audio.data).buffer], audio.filename ?? "audio", {
231
- type: audio.mediaType
232
- });
291
+ const createAudioFile = (audio) => {
292
+ const bytes = toUint8Array(audio.data);
293
+ return new File([bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)], audio.filename ?? "audio", {
294
+ type: audio.mediaType
295
+ });
296
+ };
233
297
  const inferOpenAIAudioFormat = (mediaType, explicitFormat) => {
234
298
  if (explicitFormat) {
235
299
  return explicitFormat;
@@ -255,14 +319,21 @@ const inferOpenAIAudioFormat = (mediaType, explicitFormat) => {
255
319
  }
256
320
  return normalized?.replace(/^audio\//, "") || mediaType;
257
321
  };
258
- const parseJson = async (response) => {
322
+ const parseJson = async (response, options = {}) => {
259
323
  if (!response.ok) {
260
- const body = await response.text();
324
+ const body = await readErrorBodyWithLimit(response, options.errorBodyBytes);
261
325
  throw new ProviderHTTPError(`OpenAI request failed with status ${response.status}.`, response.status, {
262
326
  responseBody: body
263
327
  });
264
328
  }
265
- return response.json();
329
+ return options.maxBytes
330
+ ? readJsonWithLimit(response, {
331
+ maxBytes: options.maxBytes,
332
+ provider: options.provider ?? "openai",
333
+ endpoint: options.endpoint,
334
+ abort: options.abort
335
+ })
336
+ : response.json();
266
337
  };
267
338
  const isOpenAIPromptCacheBreakpointPart = (part) => part.type === "provider-data" &&
268
339
  part.provider === "openai" &&
@@ -381,6 +452,25 @@ const localResponsesTools = (tools) => new Map(Object.values(tools ?? {})
381
452
  .filter(isCallableToolDefinition)
382
453
  .map((tool) => [openAILocalResponsesToolType(tool), tool.name])
383
454
  .filter((entry) => Boolean(entry[0])));
455
+ const responsesImageGenerationConfig = (tools, limits) => {
456
+ const definition = Object.values(tools ?? {}).find((tool) => isHostedToolDefinition(tool) && tool.provider === "openai" && tool.type === "image_generation");
457
+ if (!definition || !isHostedToolDefinition(definition)) {
458
+ return undefined;
459
+ }
460
+ const outputFormat = definition.config && typeof definition.config === "object" &&
461
+ ["png", "jpeg", "webp"].includes(String(definition.config.output_format))
462
+ ? definition.config.output_format
463
+ : "png";
464
+ return {
465
+ outputFormat,
466
+ ...(limits
467
+ ? {
468
+ eventMaxBytes: limits.hostedImageEventBytes,
469
+ totalMaxBytes: limits.hostedImageTotalBytes
470
+ }
471
+ : {})
472
+ };
473
+ };
384
474
  const assertResponsesToolsSupported = (modelId, tools) => {
385
475
  const currentCapabilities = modelCapabilities(modelId).agentCapabilities;
386
476
  for (const definition of Object.values(tools ?? {})) {
@@ -439,6 +529,28 @@ const mapTools = (input) => input
439
529
  };
440
530
  })
441
531
  : undefined;
532
+ const mapRealtimeTools = (input) => input
533
+ ? Object.values(input).map((tool) => {
534
+ if (isCallableToolDefinition(tool)) {
535
+ return {
536
+ type: "function",
537
+ name: tool.name,
538
+ description: tool.description,
539
+ parameters: toJSONSchema(tool.schema)
540
+ };
541
+ }
542
+ if (tool.provider && tool.provider !== "openai") {
543
+ throw new UnsupportedFeatureError(`Provider "openai" does not support hosted tools declared for provider "${tool.provider}".`);
544
+ }
545
+ if (tool.type !== "mcp") {
546
+ throw new UnsupportedFeatureError(`Provider "openai" Realtime does not support the hosted tool type "${tool.type}". Use function or MCP tools.`);
547
+ }
548
+ return {
549
+ type: tool.type,
550
+ ...(tool.config && typeof tool.config === "object" ? tool.config : {})
551
+ };
552
+ })
553
+ : undefined;
442
554
  const mapResponsesTools = (input) => input
443
555
  ? Object.values(input).map((tool) => {
444
556
  if (isCallableToolDefinition(tool)) {
@@ -489,11 +601,20 @@ const mapResponsesToolChoice = (toolChoice) => {
489
601
  name: toolChoice.toolName
490
602
  };
491
603
  };
604
+ const mapRealtimeToolChoice = (toolChoice) => {
605
+ if (!toolChoice || typeof toolChoice === "string") {
606
+ return toolChoice;
607
+ }
608
+ return {
609
+ type: "function",
610
+ name: toolChoice.toolName
611
+ };
612
+ };
492
613
  const mapRealtimeProviderOptions = (providerOptions) => {
493
614
  if (!providerOptions) {
494
615
  return {};
495
616
  }
496
- return Object.fromEntries(Object.entries(providerOptions).filter(([key]) => !["headers", "realtime_url", "realtime_query", "expires_after"].includes(key)));
617
+ return Object.fromEntries(Object.entries(providerOptions).filter(([key]) => !["headers", "safety_identifier", "realtime_url", "realtime_query", "expires_after"].includes(key)));
497
618
  };
498
619
  const mapRealtimeAudioFormat = (mediaType, sampleRateHz) => mediaType || sampleRateHz
499
620
  ? {
@@ -503,7 +624,7 @@ const mapRealtimeAudioFormat = (mediaType, sampleRateHz) => mediaType || sampleR
503
624
  : undefined;
504
625
  const mapRealtimeSessionConfig = (config, modelId) => {
505
626
  const mode = inferOpenAIRealtimeMode(modelId ?? "", config.mode);
506
- const tools = mapTools(toToolSet(config.tools));
627
+ const tools = mapRealtimeTools(toToolSet(config.tools));
507
628
  const audio = {
508
629
  input: {
509
630
  format: mapRealtimeAudioFormat(config.inputAudioMediaType, config.inputSampleRateHz),
@@ -533,7 +654,7 @@ const mapRealtimeSessionConfig = (config, modelId) => {
533
654
  instructions: config.translation?.instructions ?? config.instructions,
534
655
  output_modalities: mode === "transcription" ? undefined : config.outputAudioMediaType || config.voice || mode === "translation" ? ["audio"] : ["text"],
535
656
  tools: mode === "conversation" ? tools : undefined,
536
- tool_choice: mode === "conversation" && config.toolChoice ? mapToolChoice(config.toolChoice) : undefined,
657
+ tool_choice: mode === "conversation" && config.toolChoice ? mapRealtimeToolChoice(config.toolChoice) : undefined,
537
658
  reasoning: mode === "conversation" && config.reasoning?.effort
538
659
  ? {
539
660
  effort: config.reasoning.effort
@@ -571,11 +692,124 @@ const openAIRealtimeURL = (baseURL, modelId, mode, providerOptions) => {
571
692
  return url.toString();
572
693
  };
573
694
  const parseRealtimeProviderMetadata = (payload) => payload;
695
+ const parseRealtimeJsonValue = (value) => {
696
+ if (typeof value === "string") {
697
+ try {
698
+ return JSON.parse(value);
699
+ }
700
+ catch {
701
+ return value;
702
+ }
703
+ }
704
+ return value == null ? null : value;
705
+ };
706
+ const openAIRealtimeMcpProviderDataEvent = (payload, data) => [
707
+ {
708
+ type: "realtime-provider-data",
709
+ provider: "openai",
710
+ data: {
711
+ ...data,
712
+ raw_event: payload
713
+ }
714
+ }
715
+ ];
716
+ const openAIRealtimeMcpCommonData = (payload, item) => {
717
+ const itemId = typeof payload.item_id === "string" ? payload.item_id : typeof item?.id === "string" ? item.id : undefined;
718
+ const serverLabel = typeof payload.server_label === "string"
719
+ ? payload.server_label
720
+ : typeof item?.server_label === "string"
721
+ ? item.server_label
722
+ : undefined;
723
+ const name = typeof payload.name === "string" ? payload.name : typeof item?.name === "string" ? item.name : undefined;
724
+ return {
725
+ ...(itemId ? { item_id: itemId } : {}),
726
+ ...(serverLabel ? { server_label: serverLabel } : {}),
727
+ ...(name ? { name } : {})
728
+ };
729
+ };
574
730
  const parseOpenAIRealtimeEvent = (payload) => {
575
731
  const type = String(payload.type ?? "");
576
732
  if (type === "session.created" || type === "session.updated") {
577
733
  return [];
578
734
  }
735
+ if (type === "mcp_list_tools.in_progress" || type === "mcp_list_tools.completed" || type === "mcp_list_tools.failed") {
736
+ const status = type.slice("mcp_list_tools.".length);
737
+ return openAIRealtimeMcpProviderDataEvent(payload, {
738
+ type: "mcp_list_tools",
739
+ status,
740
+ ...openAIRealtimeMcpCommonData(payload),
741
+ ...(payload.error !== undefined ? { error: parseRealtimeJsonValue(payload.error) } : {})
742
+ });
743
+ }
744
+ if (type === "conversation.item.done") {
745
+ const item = payload.item && typeof payload.item === "object" ? payload.item : undefined;
746
+ if (item?.type === "mcp_list_tools") {
747
+ return openAIRealtimeMcpProviderDataEvent(payload, {
748
+ type: "mcp_list_tools",
749
+ status: "completed",
750
+ ...openAIRealtimeMcpCommonData(payload, item),
751
+ ...(item.tools !== undefined ? { tools: parseRealtimeJsonValue(item.tools) } : {})
752
+ });
753
+ }
754
+ if (item?.type === "mcp_approval_request") {
755
+ const approvalRequestId = typeof item.id === "string" ? item.id : undefined;
756
+ return openAIRealtimeMcpProviderDataEvent(payload, {
757
+ type: "mcp_approval_request",
758
+ status: "approval_required",
759
+ ...openAIRealtimeMcpCommonData(payload, item),
760
+ ...(approvalRequestId ? { approval_request_id: approvalRequestId } : {}),
761
+ ...(item.arguments !== undefined ? { arguments: parseRealtimeJsonValue(item.arguments) } : {})
762
+ });
763
+ }
764
+ if (item?.type === "mcp_approval_response") {
765
+ const approve = item.approve === true;
766
+ return openAIRealtimeMcpProviderDataEvent(payload, {
767
+ type: "mcp_approval_response",
768
+ status: approve ? "approved" : "rejected",
769
+ approve,
770
+ ...openAIRealtimeMcpCommonData(payload, item),
771
+ ...(typeof item.approval_request_id === "string" ? { approval_request_id: item.approval_request_id } : {}),
772
+ ...(typeof item.reason === "string" ? { reason: item.reason } : {})
773
+ });
774
+ }
775
+ }
776
+ if (type === "response.mcp_call_arguments.delta" || type === "response.mcp_call_arguments.done") {
777
+ const isDone = type.endsWith(".done");
778
+ return openAIRealtimeMcpProviderDataEvent(payload, {
779
+ type: "mcp_call",
780
+ status: isDone ? "arguments_done" : "arguments_delta",
781
+ ...openAIRealtimeMcpCommonData(payload),
782
+ ...(isDone && payload.arguments !== undefined
783
+ ? { arguments: parseRealtimeJsonValue(payload.arguments) }
784
+ : payload.delta !== undefined
785
+ ? { delta: parseRealtimeJsonValue(payload.delta) }
786
+ : {})
787
+ });
788
+ }
789
+ if (type === "response.mcp_call.in_progress" || type === "response.mcp_call.completed" || type === "response.mcp_call.failed") {
790
+ const status = type.slice("response.mcp_call.".length);
791
+ return openAIRealtimeMcpProviderDataEvent(payload, {
792
+ type: "mcp_call",
793
+ status,
794
+ ...openAIRealtimeMcpCommonData(payload),
795
+ ...(payload.output !== undefined ? { output: parseRealtimeJsonValue(payload.output) } : {}),
796
+ ...(payload.error !== undefined ? { error: parseRealtimeJsonValue(payload.error) } : {})
797
+ });
798
+ }
799
+ if (type === "response.output_item.done") {
800
+ const item = payload.item && typeof payload.item === "object" ? payload.item : undefined;
801
+ if (item?.type === "mcp_call") {
802
+ return openAIRealtimeMcpProviderDataEvent(payload, {
803
+ type: "mcp_call",
804
+ status: item.status === "failed" ? "failed" : "completed",
805
+ ...openAIRealtimeMcpCommonData(payload, item),
806
+ ...(item.arguments !== undefined ? { arguments: parseRealtimeJsonValue(item.arguments) } : {}),
807
+ ...(item.output !== undefined ? { output: parseRealtimeJsonValue(item.output) } : {}),
808
+ ...(item.error !== undefined ? { error: parseRealtimeJsonValue(item.error) } : {}),
809
+ ...(typeof item.approval_request_id === "string" ? { approval_request_id: item.approval_request_id } : {})
810
+ });
811
+ }
812
+ }
579
813
  if (type === "response.text.delta" || type === "response.output_text.delta") {
580
814
  return [
581
815
  {
@@ -905,7 +1139,7 @@ const parseResponsesProviderData = (item) => {
905
1139
  }
906
1140
  const typedItem = item;
907
1141
  if (typeof typedItem.type !== "string" ||
908
- ["message", "function_call"].includes(typedItem.type)) {
1142
+ ["message", "function_call", "image_generation_call"].includes(typedItem.type)) {
909
1143
  return undefined;
910
1144
  }
911
1145
  return item;
@@ -1149,6 +1383,10 @@ const parseResponsesAssistantMessage = (json, multiAgentEnabled = false, localTo
1149
1383
  parts
1150
1384
  };
1151
1385
  };
1386
+ const extractResponsesImageOutputs = (response, outputFormat = "png") => (Array.isArray(response.output) ? response.output : []).flatMap((item) => {
1387
+ const normalized = normalizeOpenAIImageGenerationCall(item, outputFormat);
1388
+ return normalized?.image ? [normalized.image] : [];
1389
+ });
1152
1390
  const normalizeResponsesFinishReason = (status, hasToolCalls, hasRefusal = false) => {
1153
1391
  if (hasToolCalls) {
1154
1392
  return "tool-calls";
@@ -1199,11 +1437,26 @@ const addTokenUsage = (left, right) => {
1199
1437
  totalTokens: sum(left.totalTokens, right.totalTokens)
1200
1438
  };
1201
1439
  };
1202
- const streamResponses = async function* (response, multiAgentEnabled = false, localTools = new Map()) {
1440
+ const streamResponses = async function* (response, multiAgentEnabled = false, localTools = new Map(), imageGeneration) {
1203
1441
  const toolBuffers = new Map();
1204
1442
  const outputAgents = new Map();
1443
+ const hostedImageEventMaxBytes = imageGeneration?.eventMaxBytes ?? DEFAULT_HOSTED_IMAGE_EVENT_BYTES;
1444
+ const hostedImageTotalMaxBytes = imageGeneration?.totalMaxBytes ?? DEFAULT_HOSTED_IMAGE_TOTAL_BYTES;
1445
+ let hostedImageBytes = 0;
1205
1446
  let sawToolCalls = false;
1206
1447
  let sawRefusal = false;
1448
+ const recordHostedImageBytes = (image) => {
1449
+ const receivedBytes = hostedImageBytes + (image.data?.byteLength ?? 0);
1450
+ if (receivedBytes > hostedImageTotalMaxBytes) {
1451
+ throw new ProviderResponseTooLargeError({
1452
+ maxBytes: hostedImageTotalMaxBytes,
1453
+ receivedBytes,
1454
+ provider: "openai",
1455
+ endpoint: "hosted image generation stream"
1456
+ });
1457
+ }
1458
+ hostedImageBytes = receivedBytes;
1459
+ };
1207
1460
  const emitToolCall = (key) => {
1208
1461
  const toolCall = toolBuffers.get(key);
1209
1462
  if (!toolCall || toolCall.emitted || !toolCall.name) {
@@ -1221,12 +1474,31 @@ const streamResponses = async function* (response, multiAgentEnabled = false, lo
1221
1474
  }
1222
1475
  };
1223
1476
  };
1224
- for await (const event of streamSSE(response)) {
1477
+ const maxHostedImageEventChars = Math.min(Number.MAX_SAFE_INTEGER, 4 * Math.ceil(hostedImageEventMaxBytes / 3) + HOSTED_IMAGE_EVENT_JSON_OVERHEAD_CHARS);
1478
+ for await (const event of streamSSE(response, imageGeneration
1479
+ ? { maxEventChars: maxHostedImageEventChars, maxBufferChars: maxHostedImageEventChars }
1480
+ : undefined)) {
1225
1481
  if (event.data === "[DONE]") {
1226
1482
  return;
1227
1483
  }
1228
1484
  const json = JSON.parse(event.data);
1229
1485
  const type = json.type;
1486
+ if (type === "response.image_generation_call.partial_image") {
1487
+ const normalized = normalizeOpenAIImageGenerationPartialImage(json, imageGeneration?.outputFormat ?? "png", hostedImageEventMaxBytes);
1488
+ if (normalized) {
1489
+ recordHostedImageBytes(normalized.image);
1490
+ yield {
1491
+ type: "image-generation",
1492
+ provider: "openai",
1493
+ image: normalized.image,
1494
+ partial: true,
1495
+ id: normalized.callId,
1496
+ index: normalized.partialImageIndex,
1497
+ providerMetadata: normalized.providerMetadata
1498
+ };
1499
+ }
1500
+ continue;
1501
+ }
1230
1502
  if (type === "error") {
1231
1503
  throw new ProviderHTTPError(`OpenAI Responses stream failed: ${json.error?.message ?? json.message ?? "unknown error"}`, 500, { responseBody: JSON.stringify(json) });
1232
1504
  }
@@ -1248,6 +1520,20 @@ const streamResponses = async function* (response, multiAgentEnabled = false, lo
1248
1520
  if (type === "response.output_item.added" || type === "response.output_item.done") {
1249
1521
  const item = json.item;
1250
1522
  let handledLocalToolCall = false;
1523
+ if (item?.type === "image_generation_call" && type === "response.output_item.done") {
1524
+ const normalized = normalizeOpenAIImageGenerationCall(item, imageGeneration?.outputFormat ?? "png", hostedImageEventMaxBytes);
1525
+ if (normalized?.image) {
1526
+ recordHostedImageBytes(normalized.image);
1527
+ yield {
1528
+ type: "image-generation",
1529
+ provider: "openai",
1530
+ image: normalized.image,
1531
+ partial: false,
1532
+ id: normalized.id,
1533
+ providerMetadata: normalized.providerMetadata
1534
+ };
1535
+ }
1536
+ }
1251
1537
  if (typeof json.output_index === "number" && item?.type === "message") {
1252
1538
  outputAgents.set(json.output_index, {
1253
1539
  agentName: item.agent?.agent_name
@@ -1404,6 +1690,14 @@ const streamGenerateResult = async function* (result) {
1404
1690
  }
1405
1691
  }
1406
1692
  }
1693
+ for (const image of result.images ?? []) {
1694
+ yield {
1695
+ type: "image-generation",
1696
+ provider: "openai",
1697
+ image,
1698
+ partial: false
1699
+ };
1700
+ }
1407
1701
  yield {
1408
1702
  type: "finish",
1409
1703
  finishReason: result.finishReason,
@@ -1442,13 +1736,15 @@ class OpenAILanguageModel {
1442
1736
  apiKey;
1443
1737
  baseURL;
1444
1738
  fetcher;
1739
+ responseLimits;
1445
1740
  provider = "openai";
1446
1741
  capabilities;
1447
- constructor(modelId, apiKey, baseURL, fetcher) {
1742
+ constructor(modelId, apiKey, baseURL, fetcher, responseLimits) {
1448
1743
  this.modelId = modelId;
1449
1744
  this.apiKey = apiKey;
1450
1745
  this.baseURL = baseURL;
1451
1746
  this.fetcher = fetcher;
1747
+ this.responseLimits = responseLimits;
1452
1748
  this.capabilities = modelCapabilities(modelId);
1453
1749
  }
1454
1750
  usesResponsesAPI(input, options) {
@@ -1524,6 +1820,7 @@ class OpenAILanguageModel {
1524
1820
  messages: [assistantMessage],
1525
1821
  text: extractMessageText(assistantMessage),
1526
1822
  audio: extractAudioOutputs(assistantMessage),
1823
+ images: extractResponsesImageOutputs(json, responsesImageGenerationConfig(input.tools)?.outputFormat),
1527
1824
  finishReason: normalizeResponsesFinishReason(json.status, hasToolCalls, hasRefusal),
1528
1825
  providerFinishReason: json.status,
1529
1826
  usage: accumulatedUsage,
@@ -1623,9 +1920,10 @@ class OpenAILanguageModel {
1623
1920
  stream: true
1624
1921
  })
1625
1922
  }), input);
1923
+ const imageGeneration = responsesImageGenerationConfig(input.tools, this.responseLimits);
1626
1924
  return (async function* () {
1627
1925
  try {
1628
- yield* streamResponses(response, options.multiAgentEnabled, localResponsesTools(input.tools));
1926
+ yield* streamResponses(response, options.multiAgentEnabled, localResponsesTools(input.tools), imageGeneration);
1629
1927
  }
1630
1928
  finally {
1631
1929
  cleanup();
@@ -1751,17 +2049,25 @@ class OpenAITranscriptionModel {
1751
2049
  apiKey;
1752
2050
  baseURL;
1753
2051
  fetcher;
2052
+ responseLimits;
1754
2053
  provider = "openai";
1755
2054
  capabilities = transcriptionCapabilities;
1756
- constructor(modelId, apiKey, baseURL, fetcher) {
2055
+ constructor(modelId, apiKey, baseURL, fetcher, responseLimits = resolveAudioResponseLimits()) {
1757
2056
  this.modelId = modelId;
1758
2057
  this.apiKey = apiKey;
1759
2058
  this.baseURL = baseURL;
1760
2059
  this.fetcher = fetcher;
2060
+ this.responseLimits = responseLimits;
1761
2061
  }
1762
2062
  async transcribe(input) {
1763
- const { signal, cleanup } = withTimeoutSignal(input);
2063
+ const { signal, cleanup, abort } = withTimeoutSignal(input);
1764
2064
  const form = new FormData();
2065
+ for (const [key, value] of Object.entries(input.providerOptions ?? {})) {
2066
+ if (["model", "file", "prompt", "language"].includes(key)) {
2067
+ continue;
2068
+ }
2069
+ form.set(key, typeof value === "string" ? value : JSON.stringify(value));
2070
+ }
1765
2071
  form.set("model", this.modelId);
1766
2072
  form.set("file", createAudioFile(input.audio));
1767
2073
  if (input.prompt) {
@@ -1770,9 +2076,6 @@ class OpenAITranscriptionModel {
1770
2076
  if (input.language) {
1771
2077
  form.set("language", input.language);
1772
2078
  }
1773
- for (const [key, value] of Object.entries(input.providerOptions ?? {})) {
1774
- form.set(key, typeof value === "string" ? value : JSON.stringify(value));
1775
- }
1776
2079
  try {
1777
2080
  const response = await withRetry(() => this.fetcher(`${this.baseURL}/audio/transcriptions`, {
1778
2081
  method: "POST",
@@ -1780,7 +2083,12 @@ class OpenAITranscriptionModel {
1780
2083
  signal,
1781
2084
  body: form
1782
2085
  }), input);
1783
- const json = await parseJson(response);
2086
+ const json = await parseJson(response, {
2087
+ maxBytes: this.responseLimits.transcriptionBytes,
2088
+ errorBodyBytes: this.responseLimits.errorBodyBytes,
2089
+ endpoint: "audio/transcriptions",
2090
+ abort
2091
+ });
1784
2092
  return {
1785
2093
  text: json.text ?? "",
1786
2094
  rawResponse: json
@@ -1796,16 +2104,18 @@ class OpenAISpeechModel {
1796
2104
  apiKey;
1797
2105
  baseURL;
1798
2106
  fetcher;
2107
+ responseLimits;
1799
2108
  provider = "openai";
1800
2109
  capabilities = speechCapabilities;
1801
- constructor(modelId, apiKey, baseURL, fetcher) {
2110
+ constructor(modelId, apiKey, baseURL, fetcher, responseLimits = resolveAudioResponseLimits()) {
1802
2111
  this.modelId = modelId;
1803
2112
  this.apiKey = apiKey;
1804
2113
  this.baseURL = baseURL;
1805
2114
  this.fetcher = fetcher;
2115
+ this.responseLimits = responseLimits;
1806
2116
  }
1807
2117
  async generateSpeech(input) {
1808
- const { signal, cleanup } = withTimeoutSignal(input);
2118
+ const { signal, cleanup, abort } = withTimeoutSignal(input);
1809
2119
  try {
1810
2120
  const response = await withRetry(() => this.fetcher(`${this.baseURL}/audio/speech`, {
1811
2121
  method: "POST",
@@ -1819,13 +2129,18 @@ class OpenAISpeechModel {
1819
2129
  })
1820
2130
  }), input);
1821
2131
  if (!response.ok) {
1822
- const body = await response.text();
2132
+ const body = await readErrorBodyWithLimit(response, this.responseLimits.errorBodyBytes);
1823
2133
  throw new ProviderHTTPError(`OpenAI request failed with status ${response.status}.`, response.status, {
1824
2134
  responseBody: body
1825
2135
  });
1826
2136
  }
1827
2137
  return {
1828
- audio: new Uint8Array(await response.arrayBuffer()),
2138
+ audio: await readBodyWithLimit(response, {
2139
+ maxBytes: this.responseLimits.speechBytes,
2140
+ provider: "openai",
2141
+ endpoint: "audio/speech",
2142
+ abort
2143
+ }),
1829
2144
  mediaType: response.headers.get("content-type") ?? "audio/mpeg",
1830
2145
  rawResponse: undefined
1831
2146
  };
@@ -1921,11 +2236,10 @@ class OpenAIRealtimeModel {
1921
2236
  }
1922
2237
  async connect(config = {}, options) {
1923
2238
  const initialConfig = this.resolveConfig(config);
1924
- const headers = {
1925
- authorization: `Bearer ${this.apiKey}`
1926
- };
2239
+ const providerOptions = initialConfig.providerOptions;
2240
+ const headers = resolveOpenAIRealtimeHeaders(this.apiKey, providerOptions);
1927
2241
  const connection = await (this.connectionFactory ?? openWebSocketConnection)(this.realtimeURL ??
1928
- openAIRealtimeURL(this.baseURL, this.modelId, inferOpenAIRealtimeMode(this.modelId, initialConfig.mode), initialConfig.providerOptions), headers, options);
2242
+ openAIRealtimeURL(this.baseURL, this.modelId, inferOpenAIRealtimeMode(this.modelId, initialConfig.mode), providerOptions), headers, options);
1929
2243
  const session = new CallbackRealtimeSession({
1930
2244
  provider: this.provider,
1931
2245
  modelId: this.modelId,
@@ -1998,6 +2312,26 @@ class OpenAIRealtimeModel {
1998
2312
  if (mode !== "conversation") {
1999
2313
  throw new UnsupportedFeatureError(`Provider "openai" model "${this.modelId}" does not support realtime tools in ${mode} mode.`);
2000
2314
  }
2315
+ const mcpMetadata = result.providerMetadata?.[openAIRealtimeMcpMetadataKey];
2316
+ if (mcpMetadata && typeof mcpMetadata === "object" && !Array.isArray(mcpMetadata)) {
2317
+ const approval = mcpMetadata;
2318
+ if (approval.type === "mcp_approval_response" &&
2319
+ typeof approval.approval_request_id === "string" &&
2320
+ typeof approval.approve === "boolean") {
2321
+ return [
2322
+ {
2323
+ type: "conversation.item.create",
2324
+ item: {
2325
+ ...(typeof approval.id === "string" ? { id: approval.id } : {}),
2326
+ type: "mcp_approval_response",
2327
+ approval_request_id: approval.approval_request_id,
2328
+ approve: approval.approve,
2329
+ ...(typeof approval.reason === "string" ? { reason: approval.reason } : {})
2330
+ }
2331
+ }
2332
+ ];
2333
+ }
2334
+ }
2001
2335
  const payloads = [
2002
2336
  {
2003
2337
  type: "conversation.item.create",
@@ -2037,7 +2371,7 @@ class OpenAIRealtimeModel {
2037
2371
  delete providerOptions.expires_after;
2038
2372
  const response = await withRetry(() => this.fetcher(this.browserTokenURL ?? `${this.baseURL}/realtime/client_secrets`, {
2039
2373
  method: "POST",
2040
- headers: jsonHeaders(this.apiKey),
2374
+ headers: resolveOpenAIRealtimeHeaders(this.apiKey, providerOptions, true),
2041
2375
  body: JSON.stringify({
2042
2376
  ...(expiresAfter ? { expires_after: expiresAfter } : {}),
2043
2377
  session: mapRealtimeSessionConfig({
@@ -2079,12 +2413,14 @@ export const createOpenAI = (options = {}) => {
2079
2413
  }
2080
2414
  const baseURL = options.baseURL ?? "https://api.openai.com/v1";
2081
2415
  const fetcher = options.fetch ?? globalThis.fetch;
2416
+ const responseLimits = resolveOpenAIResponseLimits(options.responseLimits);
2082
2417
  return createProviderAdapter({
2083
2418
  name: "openai",
2084
- languageModel: (modelId) => new OpenAILanguageModel(modelId, apiKey, baseURL, fetcher),
2419
+ languageModel: (modelId) => new OpenAILanguageModel(modelId, apiKey, baseURL, fetcher, responseLimits),
2085
2420
  embeddingModel: (modelId) => new OpenAIEmbeddingModel(modelId, apiKey, baseURL, fetcher),
2086
- transcriptionModel: (modelId) => new OpenAITranscriptionModel(modelId, apiKey, baseURL, fetcher),
2087
- speechModel: (modelId) => new OpenAISpeechModel(modelId, apiKey, baseURL, fetcher),
2421
+ transcriptionModel: (modelId) => new OpenAITranscriptionModel(modelId, apiKey, baseURL, fetcher, responseLimits),
2422
+ speechModel: (modelId) => new OpenAISpeechModel(modelId, apiKey, baseURL, fetcher, responseLimits),
2423
+ imageGenerationModel: (modelId) => createOpenAIImageGenerationModel({ modelId, apiKey, baseURL, fetch: fetcher }),
2088
2424
  realtimeModel: (modelId) => new OpenAIRealtimeModel(modelId, apiKey, baseURL, fetcher, options.realtimeConnectionFactory, options.realtimeURL, options.browserTokenURL),
2089
2425
  groundedLanguageModel: (modelId) => new OpenAIGroundedLanguageModel(modelId, apiKey, baseURL, fetcher),
2090
2426
  rawFetch: fetcher
@@ -2146,6 +2482,26 @@ export const openAIMcpApprovalResponse = (response) => providerDataPart("openai"
2146
2482
  type: "mcp_approval_response",
2147
2483
  ...response
2148
2484
  });
2485
+ export const openAIRealtimeMcpApprovalResult = (response) => ({
2486
+ toolCallId: response.approvalRequestId,
2487
+ toolName: response.name,
2488
+ output: {
2489
+ type: "mcp_approval_response",
2490
+ approve: response.approve,
2491
+ ...(response.reason ? { reason: response.reason } : {})
2492
+ },
2493
+ isError: false,
2494
+ providerMetadata: {
2495
+ [openAIRealtimeMcpMetadataKey]: {
2496
+ type: "mcp_approval_response",
2497
+ status: response.approve ? "approved" : "rejected",
2498
+ approval_request_id: response.approvalRequestId,
2499
+ approve: response.approve,
2500
+ ...(response.itemId ? { id: response.itemId } : {}),
2501
+ ...(response.reason ? { reason: response.reason } : {})
2502
+ }
2503
+ }
2504
+ });
2149
2505
  export const openAIPromptCacheBreakpoint = () => providerDataPart("openai", {
2150
2506
  type: "prompt_cache_breakpoint",
2151
2507
  mode: "explicit"
@@ -2294,4 +2650,5 @@ export const openAIApplyPatchTool = (config) => ({
2294
2650
  }),
2295
2651
  execute: async ({ operation }) => applyOpenAIPatchOperation(operation, config)
2296
2652
  });
2653
+ export * from "./image-generation.js";
2297
2654
  //# sourceMappingURL=index.js.map