@zhivex-ai/openai 0.16.3 → 0.18.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
@@ -4,15 +4,87 @@ import { computerInputSchema, computerScreenshotSchema } from "./computer.js";
4
4
  export { openAIComputerTool, OpenAIComputerExecutionError } from "./computer.js";
5
5
  import { jsonHeaders, parseJson, RESERVED_REQUEST_HEADERS } from "./http.js";
6
6
  import { OpenAIRealtimeModel, openAIRealtimeMcpMetadataKey, resolveOpenAIRealtimeHeaders } from "./realtime.js";
7
- export { openAIRealtimeMcpMetadataKey } from "./realtime.js";
7
+ export { OpenAIRealtimeModel, openAIRealtimeMcpMetadataKey } from "./realtime.js";
8
8
  import { OpenAIEmbeddingModel, OpenAITranscriptionModel, OpenAISpeechModel, toUint8Array } from "./resource-models.js";
9
+ export { OpenAISpeechModel } from "./resource-models.js";
9
10
  import { OpenAIAgentsClient } from "./agents.js";
10
11
  export { OpenAIAgentsClient } from "./agents.js";
11
12
  import { toJSONSchema, z } from "zod";
12
13
  import { OpenAILiveModel, isOpenAILiveModel } from "./live.js";
14
+ export { OpenAILiveModel } from "./live.js";
13
15
  import { createOpenAIImageGenerationModel, normalizeOpenAIImageGenerationCall, normalizeOpenAIImageGenerationPartialImage } from "./image-generation.js";
14
16
  import { groundedCapabilities, supportsOpenAIModernResponses, assertOpenAIModelRequestSupported, resolveOpenAIModelProfile, modelCapabilities } from "./capabilities.js";
15
- import { imageInputToDataUrl, ConfigurationError, ProviderHTTPError, ProviderResponseTooLargeError, ProviderToolCallError, assertTrustedEndpoint, resolveAudioResponseLimits, openWebSocketConnection, hostedTool, providerDataPart, UnsupportedFeatureError, createProviderAdapter, isCallableToolDefinition, isHostedToolDefinition, normalizeFinishReason, streamSSE, streamChatCompletions, toolResultPayload, withRetry, withResponseRetry, withTimeoutSignal } from "@zhivex-ai/core/provider";
17
+ import { imageInputToDataUrl, ConfigurationError, ProviderHTTPError, ProviderResponseTooLargeError, ProviderToolCallError, ValidationError, assertTrustedEndpoint, resolveAudioResponseLimits, openWebSocketConnection, hostedTool, providerDataPart, UnsupportedFeatureError, createProviderAdapter, isCallableToolDefinition, isHostedToolDefinition, normalizeFinishReason, streamSSE, streamChatCompletions, toolResultPayload, withRetry, withResponseRetry, withTimeoutSignal } from "@zhivex-ai/core/provider";
18
+ const TOOL_INPUT_JSON_SCHEMA = Symbol.for("@zhivex-ai/core/json-schema");
19
+ /** Zod inputs stay on toJSONSchema so this package still loads against the oldest core in its peer range. */
20
+ const toolInputSchema = (schema) => {
21
+ if (schema !== null && typeof schema === "object" && schema[TOOL_INPUT_JSON_SCHEMA] === true) {
22
+ return { ...schema.jsonSchema };
23
+ }
24
+ return toJSONSchema(schema);
25
+ };
26
+ const openAISamplingWarnings = new Set();
27
+ const warnOpenAISampling = (option, detail) => {
28
+ const key = `openai:${option}:${detail}`;
29
+ if (openAISamplingWarnings.has(key))
30
+ return;
31
+ openAISamplingWarnings.add(key);
32
+ console.warn(`Provider "openai" does not support ${option}. ${detail} The option was ignored.`);
33
+ };
34
+ const openAIChatCacheTtlWarnings = new Set();
35
+ const warnOpenAIChatPromptCacheTtl = () => {
36
+ const key = 'openai:prompt_cache_options.ttl:12h:chat';
37
+ if (openAIChatCacheTtlWarnings.has(key))
38
+ return;
39
+ openAIChatCacheTtlWarnings.add(key);
40
+ console.warn('Provider "openai" does not support prompt_cache_options.ttl "12h" on Chat Completions. The option was ignored.');
41
+ };
42
+ /** Chat Completions support for a 12-hour cache TTL is unconfirmed, so that value is ignored. */
43
+ const openAIChatBodyOptions = (options) => {
44
+ const cache = options.prompt_cache_options;
45
+ if (cache?.ttl !== "12h")
46
+ return options;
47
+ warnOpenAIChatPromptCacheTtl();
48
+ const rest = { ...cache };
49
+ delete rest.ttl;
50
+ const next = { ...options };
51
+ if (Object.keys(rest).length === 0)
52
+ delete next.prompt_cache_options;
53
+ else
54
+ next.prompt_cache_options = rest;
55
+ return next;
56
+ };
57
+ /** Model profiles that already reject sampling. `topP` still throws in assertOpenAIModelRequestSupported. */
58
+ const openAISamplingRejected = (modelId, input, bodyOptions) => {
59
+ const profile = resolveOpenAIModelProfile(modelId);
60
+ if (profile.sampling === "allowed")
61
+ return null;
62
+ const effort = input.reasoning?.effort ?? bodyOptions.reasoning?.effort ?? bodyOptions.reasoning_effort;
63
+ return profile.sampling === "unsupported" || effort !== "none" ? profile.label : null;
64
+ };
65
+ const openAIChatSampling = (modelId, input, bodyOptions) => {
66
+ const rejected = openAISamplingRejected(modelId, input, bodyOptions);
67
+ if (rejected) {
68
+ if (input.seed !== undefined)
69
+ warnOpenAISampling("seed", `${rejected} does not support seed.`);
70
+ if (input.stopSequences !== undefined) {
71
+ warnOpenAISampling("stopSequences", `${rejected} does not support stop sequences.`);
72
+ }
73
+ }
74
+ return {
75
+ ...(input.topP !== undefined ? { top_p: input.topP } : {}),
76
+ ...(!rejected && input.seed !== undefined ? { seed: input.seed } : {}),
77
+ ...(!rejected && input.stopSequences !== undefined ? { stop: [...input.stopSequences] } : {})
78
+ };
79
+ };
80
+ const openAIResponsesSampling = (input) => {
81
+ if (input.seed !== undefined)
82
+ warnOpenAISampling("seed", "The Responses API does not accept seed.");
83
+ if (input.stopSequences !== undefined) {
84
+ warnOpenAISampling("stopSequences", "The Responses API does not accept stop sequences.");
85
+ }
86
+ return input.topP !== undefined ? { top_p: input.topP } : {};
87
+ };
16
88
  const MIB = 1024 * 1024;
17
89
  const DEFAULT_HOSTED_IMAGE_EVENT_BYTES = 32 * MIB;
18
90
  const DEFAULT_HOSTED_IMAGE_TOTAL_BYTES = 128 * MIB;
@@ -182,6 +254,14 @@ const isOpenAIPromptCacheBreakpointPart = (part) => part.type === "provider-data
182
254
  part.data !== null &&
183
255
  typeof part.data === "object" &&
184
256
  part.data.type === "prompt_cache_breakpoint";
257
+ const explicitPromptCacheBreakpoint = (breakpoint) => {
258
+ const record = breakpoint;
259
+ const ttl = record.ttl;
260
+ if (ttl !== undefined && ttl !== "30m" && ttl !== "12h") {
261
+ throw new ValidationError('OpenAI prompt cache breakpoint ttl must be "30m" or "12h".');
262
+ }
263
+ return ttl === "30m" || ttl === "12h" ? { mode: "explicit", ttl } : { mode: "explicit" };
264
+ };
185
265
  const promptCacheBreakpointForContentPart = (part) => {
186
266
  const metadata = part
187
267
  .providerMetadata;
@@ -190,7 +270,7 @@ const promptCacheBreakpointForContentPart = (part) => {
190
270
  : metadata;
191
271
  const breakpoint = openAIMetadata?.prompt_cache_breakpoint;
192
272
  return breakpoint && typeof breakpoint === "object" && !Array.isArray(breakpoint)
193
- ? { prompt_cache_breakpoint: { mode: "explicit" } }
273
+ ? { prompt_cache_breakpoint: explicitPromptCacheBreakpoint(breakpoint) }
194
274
  : {};
195
275
  };
196
276
  const imageDetailForPart = (part) => {
@@ -201,12 +281,12 @@ const imageDetailForPart = (part) => {
201
281
  : undefined);
202
282
  return detail === "auto" || detail === "low" || detail === "high" || detail === "original" ? detail : undefined;
203
283
  };
204
- const markLastContentBlockForPromptCaching = (content) => {
284
+ const markLastContentBlockForPromptCaching = (content, breakpoint) => {
205
285
  const last = content.at(-1);
206
286
  if (!last) {
207
287
  throw new ConfigurationError("OpenAI prompt cache breakpoints must follow a cacheable content part.");
208
288
  }
209
- last.prompt_cache_breakpoint = { mode: "explicit" };
289
+ last.prompt_cache_breakpoint = explicitPromptCacheBreakpoint(breakpoint);
210
290
  };
211
291
  const mapContentParts = (message) => {
212
292
  const hasRichContent = message.parts.some((part) => part.type === "image" ||
@@ -251,7 +331,7 @@ const mapContentParts = (message) => {
251
331
  });
252
332
  }
253
333
  else if (isOpenAIPromptCacheBreakpointPart(part)) {
254
- markLastContentBlockForPromptCaching(content);
334
+ markLastContentBlockForPromptCaching(content, part.data);
255
335
  }
256
336
  }
257
337
  return content;
@@ -358,7 +438,7 @@ const mapTools = (input) => input
358
438
  function: {
359
439
  name: tool.name,
360
440
  description: tool.description,
361
- parameters: toJSONSchema(tool.schema)
441
+ parameters: toolInputSchema(tool.schema)
362
442
  }
363
443
  };
364
444
  }
@@ -385,7 +465,7 @@ const mapResponsesTools = (input) => input
385
465
  type: "function",
386
466
  name: tool.name,
387
467
  description: tool.description,
388
- parameters: toJSONSchema(tool.schema),
468
+ parameters: toolInputSchema(tool.schema),
389
469
  ...openAIResponsesFunctionConfig(tool)
390
470
  };
391
471
  }
@@ -798,7 +878,7 @@ const toResponsesInput = (messages, format, history = messages) => {
798
878
  break;
799
879
  case "provider-data":
800
880
  if (isOpenAIPromptCacheBreakpointPart(part)) {
801
- markLastContentBlockForPromptCaching(content);
881
+ markLastContentBlockForPromptCaching(content, part.data);
802
882
  }
803
883
  break;
804
884
  }
@@ -966,36 +1046,90 @@ const attachTerminalUsage = (error, raw) => new ProviderToolCallError({
966
1046
  cause: error.cause,
967
1047
  usage: mapResponsesUsage(raw)
968
1048
  });
969
- const mapResponsesUsage = (usage) => usage
970
- ? {
1049
+ const mapOpenAICacheWriteTokensDetails = (details) => {
1050
+ if (!details || typeof details !== "object" || Array.isArray(details))
1051
+ return undefined;
1052
+ const record = details;
1053
+ const read = (key) => typeof record[key] === "number" ? record[key] : undefined;
1054
+ const ttl30mTokens = read("ttl_30m_tokens");
1055
+ const ttl12hTokens = read("ttl_12h_tokens");
1056
+ if (ttl30mTokens === undefined && ttl12hTokens === undefined)
1057
+ return undefined;
1058
+ return {
1059
+ ...(ttl30mTokens !== undefined ? { ttl30mTokens } : {}),
1060
+ ...(ttl12hTokens !== undefined ? { ttl12hTokens } : {})
1061
+ };
1062
+ };
1063
+ const mapResponsesUsage = (usage) => {
1064
+ if (!usage)
1065
+ return undefined;
1066
+ const cacheWriteTokensDetails = mapOpenAICacheWriteTokensDetails(usage.input_tokens_details?.cache_write_tokens_details);
1067
+ return {
971
1068
  inputTokens: usage.input_tokens,
972
1069
  cachedInputTokens: usage.input_tokens_details?.cached_tokens,
973
1070
  cacheWriteTokens: usage.input_tokens_details?.cache_write_tokens,
1071
+ ...(cacheWriteTokensDetails ? { cacheWriteTokensDetails } : {}),
974
1072
  outputTokens: usage.output_tokens,
975
1073
  reasoningTokens: usage.output_tokens_details?.reasoning_tokens,
976
1074
  totalTokens: usage.total_tokens
977
- }
978
- : undefined;
979
- const mapChatUsage = (usage) => usage
980
- ? {
1075
+ };
1076
+ };
1077
+ const mapChatUsage = (usage) => {
1078
+ if (!usage)
1079
+ return undefined;
1080
+ const cacheWriteTokensDetails = mapOpenAICacheWriteTokensDetails(usage.prompt_tokens_details?.cache_write_tokens_details);
1081
+ return {
981
1082
  inputTokens: usage.prompt_tokens,
982
1083
  cachedInputTokens: usage.prompt_tokens_details?.cached_tokens,
983
1084
  cacheWriteTokens: usage.prompt_tokens_details?.cache_write_tokens,
1085
+ ...(cacheWriteTokensDetails ? { cacheWriteTokensDetails } : {}),
984
1086
  outputTokens: usage.completion_tokens,
985
1087
  reasoningTokens: usage.completion_tokens_details?.reasoning_tokens,
986
1088
  totalTokens: usage.total_tokens
987
- }
988
- : undefined;
1089
+ };
1090
+ };
1091
+ const openAIUsageCounters = (usage) => [
1092
+ usage.inputTokens,
1093
+ usage.outputTokens,
1094
+ usage.totalTokens,
1095
+ usage.cachedInputTokens,
1096
+ usage.cacheWriteTokens,
1097
+ usage.reasoningTokens,
1098
+ usage.cacheWriteTokensDetails?.ttl30mTokens,
1099
+ usage.cacheWriteTokensDetails?.ttl12hTokens
1100
+ ];
1101
+ const openAIUsageCounterInvalid = (usage) => openAIUsageCounters(usage ?? {}).some((value) => value !== undefined && (!Number.isSafeInteger(value) || value < 0));
989
1102
  const addTokenUsage = (left, right) => {
990
1103
  if (!left)
991
1104
  return right;
992
1105
  if (!right)
993
1106
  return left;
994
1107
  const sum = (a, b) => a === undefined || b === undefined ? undefined : a + b;
1108
+ const consistentDetails = (writes, details) => {
1109
+ if (!details)
1110
+ return (writes ?? 0) === 0;
1111
+ return writes !== undefined && (details.ttl30mTokens ?? 0) + (details.ttl12hTokens ?? 0) === writes;
1112
+ };
1113
+ const cacheWriteTokensDetails = consistentDetails(left.cacheWriteTokens, left.cacheWriteTokensDetails) &&
1114
+ consistentDetails(right.cacheWriteTokens, right.cacheWriteTokensDetails) &&
1115
+ (left.cacheWriteTokensDetails || right.cacheWriteTokensDetails)
1116
+ ? {
1117
+ ...(left.cacheWriteTokensDetails?.ttl30mTokens !== undefined || right.cacheWriteTokensDetails?.ttl30mTokens !== undefined
1118
+ ? { ttl30mTokens: (left.cacheWriteTokensDetails?.ttl30mTokens ?? 0) + (right.cacheWriteTokensDetails?.ttl30mTokens ?? 0) }
1119
+ : {}),
1120
+ ...(left.cacheWriteTokensDetails?.ttl12hTokens !== undefined || right.cacheWriteTokensDetails?.ttl12hTokens !== undefined
1121
+ ? { ttl12hTokens: (left.cacheWriteTokensDetails?.ttl12hTokens ?? 0) + (right.cacheWriteTokensDetails?.ttl12hTokens ?? 0) }
1122
+ : {})
1123
+ }
1124
+ : undefined;
995
1125
  return {
996
1126
  inputTokens: sum(left.inputTokens, right.inputTokens),
997
1127
  cachedInputTokens: sum(left.cachedInputTokens, right.cachedInputTokens),
998
1128
  cacheWriteTokens: sum(left.cacheWriteTokens, right.cacheWriteTokens),
1129
+ ...(cacheWriteTokensDetails &&
1130
+ (cacheWriteTokensDetails.ttl30mTokens !== undefined || cacheWriteTokensDetails.ttl12hTokens !== undefined)
1131
+ ? { cacheWriteTokensDetails }
1132
+ : {}),
999
1133
  outputTokens: sum(left.outputTokens, right.outputTokens),
1000
1134
  reasoningTokens: sum(left.reasoningTokens, right.reasoningTokens),
1001
1135
  totalTokens: sum(left.totalTokens, right.totalTokens)
@@ -1493,6 +1627,17 @@ class OpenAILanguageModel {
1493
1627
  if (!this.capabilities.vision && input.messages.some(message => message.parts.some(part => part.type === "image"))) {
1494
1628
  throw new UnsupportedFeatureError(`OpenAI model "${this.modelId}" has no declared vision support.`);
1495
1629
  }
1630
+ if (!this.capabilities.audioInput && input.messages.some(message => message.parts.some(part => part.type === "audio"))) {
1631
+ throw new UnsupportedFeatureError(`OpenAI model "${this.modelId}" has no declared audio input support.`);
1632
+ }
1633
+ // No OpenAI language model advertises video input unless inputMediaTypes says so.
1634
+ const acceptsVideo = this.capabilities.inputMediaTypes?.some((type) => {
1635
+ const normalized = type.toLowerCase();
1636
+ return normalized === "*/*" || normalized === "video/*" || normalized.startsWith("video/");
1637
+ }) === true;
1638
+ if (!acceptsVideo && input.messages.some(message => message.parts.some(part => part.type === "file" && part.mediaType.toLowerCase().startsWith("video/")))) {
1639
+ throw new UnsupportedFeatureError(`OpenAI model "${this.modelId}" has no declared video input support.`);
1640
+ }
1496
1641
  const rawFormat = options.bodyOptions.response_format;
1497
1642
  const rawText = options.bodyOptions.text;
1498
1643
  const formatTypes = [rawFormat?.type, rawText?.format?.type];
@@ -1569,17 +1714,18 @@ class OpenAILanguageModel {
1569
1714
  ...(input.toolChoice ? { tool_choice: mapResponsesToolChoice(input.toolChoice) } : {}),
1570
1715
  text: mapResponsesStructuredOutput(input) ?? responseBodyOptions.text,
1571
1716
  temperature: input.temperature,
1717
+ ...openAIResponsesSampling(input),
1572
1718
  max_output_tokens: ptc ? remainingOutputTokens : input.maxTokens,
1573
1719
  ...mapResponsesReasoning(input, responseBodyOptions.reasoning)
1574
1720
  })
1575
1721
  }), { ...input, abortSignal: signal, ...(ptc ? { maxRetries: 0 } : {}) }, "OpenAI");
1576
1722
  const json = await parseJson(response);
1577
1723
  const reportedUsage = mapResponsesUsage(json.usage);
1578
- if (ptc && (!reportedUsage || ![reportedUsage.inputTokens, reportedUsage.outputTokens, reportedUsage.totalTokens].every(value => typeof value === "number" && Number.isSafeInteger(value) && value >= 0) || reportedUsage.totalTokens < reportedUsage.inputTokens + reportedUsage.outputTokens || Object.values(reportedUsage).some(value => value !== undefined && (!Number.isSafeInteger(value) || value < 0)))) {
1724
+ if (ptc && (!reportedUsage || ![reportedUsage.inputTokens, reportedUsage.outputTokens, reportedUsage.totalTokens].every(value => typeof value === "number" && Number.isSafeInteger(value) && value >= 0) || reportedUsage.totalTokens < reportedUsage.inputTokens + reportedUsage.outputTokens || openAIUsageCounterInvalid(reportedUsage))) {
1579
1725
  throw failure("OPENAI_PTC_USAGE_UNKNOWN");
1580
1726
  }
1581
1727
  const nextUsage = addTokenUsage(accumulatedUsage, reportedUsage);
1582
- if (ptc && Object.values(nextUsage ?? {}).some(value => value !== undefined && !Number.isSafeInteger(value)))
1728
+ if (ptc && openAIUsageCounterInvalid(nextUsage))
1583
1729
  throw failure("OPENAI_PTC_USAGE_UNKNOWN");
1584
1730
  accumulatedUsage = nextUsage;
1585
1731
  if (ptc)
@@ -1658,13 +1804,14 @@ class OpenAILanguageModel {
1658
1804
  headers: options.headers,
1659
1805
  signal,
1660
1806
  body: JSON.stringify({
1661
- ...options.bodyOptions,
1807
+ ...openAIChatBodyOptions(options.bodyOptions),
1662
1808
  model: this.modelId,
1663
1809
  messages: mapMessages(input.messages, input.toolResultFormat),
1664
1810
  tools: mapTools(input.tools),
1665
1811
  ...(input.toolChoice ? { tool_choice: mapToolChoice(input.toolChoice) } : {}),
1666
1812
  response_format: mapStructuredOutput(input) ?? options.bodyOptions.response_format,
1667
1813
  temperature: input.temperature,
1814
+ ...openAIChatSampling(this.modelId, input, options.bodyOptions),
1668
1815
  ...(input.reasoning ? {} : { max_tokens: input.maxTokens }),
1669
1816
  ...mapChatReasoning(input),
1670
1817
  stream: false
@@ -1722,6 +1869,7 @@ class OpenAILanguageModel {
1722
1869
  ...(input.toolChoice ? { tool_choice: mapResponsesToolChoice(input.toolChoice) } : {}),
1723
1870
  text: mapResponsesStructuredOutput(input) ?? responseBodyOptions.text,
1724
1871
  temperature: input.temperature,
1872
+ ...openAIResponsesSampling(input),
1725
1873
  max_output_tokens: input.maxTokens,
1726
1874
  ...mapResponsesReasoning(input, responseBodyOptions.reasoning),
1727
1875
  stream: true
@@ -1744,13 +1892,14 @@ class OpenAILanguageModel {
1744
1892
  headers: options.headers,
1745
1893
  signal,
1746
1894
  body: JSON.stringify({
1747
- ...options.bodyOptions,
1895
+ ...openAIChatBodyOptions(options.bodyOptions),
1748
1896
  model: this.modelId,
1749
1897
  messages: mapMessages(input.messages, input.toolResultFormat),
1750
1898
  tools: mapTools(input.tools),
1751
1899
  ...(input.toolChoice ? { tool_choice: mapToolChoice(input.toolChoice) } : {}),
1752
1900
  response_format: mapStructuredOutput(input) ?? options.bodyOptions.response_format,
1753
1901
  temperature: input.temperature,
1902
+ ...openAIChatSampling(this.modelId, input, options.bodyOptions),
1754
1903
  ...(input.reasoning ? {} : { max_tokens: input.maxTokens }),
1755
1904
  ...mapChatReasoning(input),
1756
1905
  stream: true,
@@ -1846,6 +1995,7 @@ export const createOpenAI = (options = {}) => {
1846
1995
  const responseLimits = resolveOpenAIResponseLimits(options.responseLimits);
1847
1996
  return createProviderAdapter({
1848
1997
  name: "openai",
1998
+ decisionModel: (modelId = "gpt-6-luna") => createOpenAIDecisionModel(modelId, { apiKey, baseURL, fetch: fetcher, allowUnsafeEndpoints: options.allowUnsafeEndpoints }),
1849
1999
  experimentalDecisionModel: (modelId = "gpt-6-luna") => createOpenAIDecisionModel(modelId, { apiKey, baseURL, fetch: fetcher, allowUnsafeEndpoints: options.allowUnsafeEndpoints }),
1850
2000
  languageModel: (modelId) => {
1851
2001
  if (modelId.startsWith("gpt-live-"))
@@ -1949,9 +2099,10 @@ export const openAIRealtimeMcpApprovalResult = (response) => ({
1949
2099
  }
1950
2100
  }
1951
2101
  });
1952
- export const openAIPromptCacheBreakpoint = () => providerDataPart("openai", {
2102
+ export const openAIPromptCacheBreakpoint = (breakpoint) => providerDataPart("openai", {
1953
2103
  type: "prompt_cache_breakpoint",
1954
- mode: "explicit"
2104
+ mode: "explicit",
2105
+ ...(breakpoint?.ttl ? { ttl: breakpoint.ttl } : {})
1955
2106
  });
1956
2107
  export const openAIComputerUseTool = (config) => hostedTool({
1957
2108
  name: "computer",