@yourgpt/llm-sdk 2.6.0-beta.2 → 2.6.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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -17
  3. package/dist/adapters/index.d.mts +11 -13
  4. package/dist/adapters/index.d.ts +11 -13
  5. package/dist/adapters/index.js +406 -114
  6. package/dist/adapters/index.mjs +406 -114
  7. package/dist/audio-K-Z3-Bce.d.mts +191 -0
  8. package/dist/audio-K-Z3-Bce.d.ts +191 -0
  9. package/dist/{base-BICaQaRS.d.mts → base-6LzczuCg.d.ts} +8 -1
  10. package/dist/{base-CpRPB8u8.d.ts → base-DySbTP8C.d.mts} +8 -1
  11. package/dist/decisions-CJzOt87t.d.mts +137 -0
  12. package/dist/decisions-CJzOt87t.d.ts +137 -0
  13. package/dist/fallback/index.d.mts +4 -4
  14. package/dist/fallback/index.d.ts +4 -4
  15. package/dist/http-BQxUT5A9.d.mts +9 -0
  16. package/dist/http-BQxUT5A9.d.ts +9 -0
  17. package/dist/index.d.mts +17 -9
  18. package/dist/index.d.ts +17 -9
  19. package/dist/index.js +698 -205
  20. package/dist/index.mjs +695 -206
  21. package/dist/providers/anthropic/index.d.mts +7 -4
  22. package/dist/providers/anthropic/index.d.ts +7 -4
  23. package/dist/providers/anthropic/index.js +389 -115
  24. package/dist/providers/anthropic/index.mjs +389 -115
  25. package/dist/providers/azure/index.d.mts +3 -3
  26. package/dist/providers/azure/index.d.ts +3 -3
  27. package/dist/providers/cloudflare/index.d.mts +11 -0
  28. package/dist/providers/cloudflare/index.d.ts +11 -0
  29. package/dist/providers/cloudflare/index.js +743 -0
  30. package/dist/providers/cloudflare/index.mjs +741 -0
  31. package/dist/providers/fireworks/index.d.mts +3 -3
  32. package/dist/providers/fireworks/index.d.ts +3 -3
  33. package/dist/providers/fireworks/index.js +181 -60
  34. package/dist/providers/fireworks/index.mjs +181 -60
  35. package/dist/providers/google/index.d.mts +3 -3
  36. package/dist/providers/google/index.d.ts +3 -3
  37. package/dist/providers/google/index.js +181 -60
  38. package/dist/providers/google/index.mjs +181 -60
  39. package/dist/providers/ollama/index.d.mts +4 -4
  40. package/dist/providers/ollama/index.d.ts +4 -4
  41. package/dist/providers/openai/index.d.mts +38 -6
  42. package/dist/providers/openai/index.d.ts +38 -6
  43. package/dist/providers/openai/index.js +1451 -85
  44. package/dist/providers/openai/index.mjs +1449 -86
  45. package/dist/providers/openrouter/index.d.mts +21 -4
  46. package/dist/providers/openrouter/index.d.ts +21 -4
  47. package/dist/providers/openrouter/index.js +1040 -131
  48. package/dist/providers/openrouter/index.mjs +1040 -132
  49. package/dist/providers/togetherai/index.d.mts +3 -3
  50. package/dist/providers/togetherai/index.d.ts +3 -3
  51. package/dist/providers/togetherai/index.js +181 -60
  52. package/dist/providers/togetherai/index.mjs +181 -60
  53. package/dist/providers/typesafe/index.d.mts +8 -0
  54. package/dist/providers/typesafe/index.d.ts +8 -0
  55. package/dist/providers/typesafe/index.js +735 -0
  56. package/dist/providers/typesafe/index.mjs +733 -0
  57. package/dist/providers/xai/index.d.mts +3 -3
  58. package/dist/providers/xai/index.d.ts +3 -3
  59. package/dist/providers/xai/index.js +181 -60
  60. package/dist/providers/xai/index.mjs +181 -60
  61. package/dist/{types-DCFqkJAy.d.ts → types-B0y6VZSQ.d.ts} +1 -1
  62. package/dist/{types-DHQEMFHA.d.mts → types-BXQMtaCX.d.ts} +17 -3
  63. package/dist/{types-C3RgRbEC.d.mts → types-BY6BVhFg.d.mts} +1 -1
  64. package/dist/{types-Dikj3En4.d.ts → types-C7_qVSSd.d.mts} +17 -3
  65. package/dist/{types-Dg1Bcg1k.d.mts → types-D7lzMMLu.d.mts} +13 -1
  66. package/dist/{types-Dg1Bcg1k.d.ts → types-D7lzMMLu.d.ts} +13 -1
  67. package/dist/yourgpt/index.d.mts +1 -1
  68. package/dist/yourgpt/index.d.ts +1 -1
  69. package/docs/decisions.md +174 -0
  70. package/docs/media.md +201 -0
  71. package/docs/multimodal-and-audio.md +9 -2
  72. package/package.json +34 -12
  73. package/dist/audio-BBSwnjo8.d.mts +0 -92
  74. package/dist/audio-BBSwnjo8.d.ts +0 -92
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ var crypto = require('crypto');
4
+
3
5
  // src/core/tool-output.ts
4
6
  var imageTypes = /* @__PURE__ */ new Set([
5
7
  "image/png",
@@ -103,6 +105,72 @@ function textToolOutput(value, fallback) {
103
105
  return toolOutputForProvider(value, "unsupported").map((part) => part.text).join("\n");
104
106
  }
105
107
 
108
+ // src/core/provider-error.ts
109
+ function invalidProviderResponse(message) {
110
+ return Object.assign(new Error(message), {
111
+ name: "InvalidProviderResponseError",
112
+ status: 502,
113
+ code: "INVALID_PROVIDER_RESPONSE"
114
+ });
115
+ }
116
+ function providerErrorEvent(error, code) {
117
+ const details = error ?? {};
118
+ const event = {
119
+ type: "error",
120
+ message: error instanceof Error ? error.message : String(error),
121
+ code,
122
+ ...typeof details.status === "number" ? { status: details.status } : {},
123
+ ...typeof details.code === "string" ? { providerCode: details.code } : {},
124
+ ...typeof details.param === "string" ? { param: details.param } : {},
125
+ ...typeof details.request_id === "string" ? { requestId: details.request_id } : {}
126
+ };
127
+ Object.defineProperty(event, "cause", { value: error, enumerable: false });
128
+ return event;
129
+ }
130
+ function errorFromEvent(event) {
131
+ if (event.cause instanceof Error) return event.cause;
132
+ return Object.assign(new Error(event.message), {
133
+ status: event.status,
134
+ code: event.providerCode ?? event.code,
135
+ param: event.param,
136
+ request_id: event.requestId
137
+ });
138
+ }
139
+ function throwIfAborted(signal) {
140
+ if (!signal?.aborted) return;
141
+ const error = new Error("The operation was aborted");
142
+ error.name = "AbortError";
143
+ throw error;
144
+ }
145
+
146
+ // src/core/tool-validation.ts
147
+ function parseToolArguments(value) {
148
+ let args = value;
149
+ if (typeof value === "string") {
150
+ try {
151
+ args = JSON.parse(value || "{}");
152
+ } catch {
153
+ throw invalidProviderResponse("Tool arguments are not valid JSON");
154
+ }
155
+ }
156
+ if (args === null || typeof args !== "object" || Array.isArray(args))
157
+ throw invalidProviderResponse("Tool arguments must be a JSON object");
158
+ return args;
159
+ }
160
+ function validateToolCalls(calls) {
161
+ const ids = /* @__PURE__ */ new Set();
162
+ for (const call of calls) {
163
+ if (!call || typeof call.id !== "string" || !call.id || typeof call.name !== "string" || !call.name || ids.has(call.id))
164
+ throw invalidProviderResponse(
165
+ "Tool calls have missing or duplicate identities"
166
+ );
167
+ ids.add(call.id);
168
+ if (typeof call.args === "string")
169
+ throw invalidProviderResponse("Parsed tool arguments must be an object");
170
+ parseToolArguments(call.args);
171
+ }
172
+ }
173
+
106
174
  // src/core/openai-parameters.ts
107
175
  function capabilities(model) {
108
176
  const gpt = /^gpt-(\d+)(?:\.(\d+))?(?:-(.+))?$/i.exec(model);
@@ -113,6 +181,19 @@ function capabilities(model) {
113
181
  const supportsNone = major === 5 && minor >= 1 && !chat || /^gpt-6-(?:sol|luna)(?:-\d{4}-\d{2}-\d{2})?$/i.test(model);
114
182
  return { major, minor, reasoning, supportsNone };
115
183
  }
184
+ function filterOpenAIResponseInclude(model, include, options = {}) {
185
+ const c = capabilities(model);
186
+ let result = include;
187
+ if (options.parameterPolicy !== "passthrough" && c.reasoning && !(options.reasoningEffort === "none" && c.supportsNone) && include.includes("message.output_text.logprobs")) {
188
+ result = include.filter(
189
+ (value) => value !== "message.output_text.logprobs"
190
+ );
191
+ (options.onWarning ?? console.warn)(
192
+ `[llm-sdk] ${model}: omitted message.output_text.logprobs because reasoning is enabled.`
193
+ );
194
+ }
195
+ return result.length ? result : void 0;
196
+ }
116
197
  function requiresOpenAIResponses(model, baseUrl, options = {}) {
117
198
  if (options.parameterPolicy === "passthrough") return false;
118
199
  const c = capabilities(model);
@@ -201,6 +282,54 @@ function responseMetadata(response) {
201
282
  }
202
283
  };
203
284
  }
285
+ function responseReplayMetadata(metadata) {
286
+ const response = metadata?.openai?.response;
287
+ if (!response) return metadata;
288
+ const output = response.output.map((item) => {
289
+ const fields = ["type", "id", "status"];
290
+ switch (item.type) {
291
+ case "reasoning":
292
+ fields.push("encrypted_content");
293
+ break;
294
+ case "message":
295
+ fields.push("role", "phase");
296
+ break;
297
+ case "function_call":
298
+ fields.push("call_id", "name", "namespace", "arguments");
299
+ break;
300
+ case "tool_search_call":
301
+ fields.push("call_id", "execution", "arguments");
302
+ break;
303
+ case "tool_search_output":
304
+ fields.push("call_id", "tools");
305
+ break;
306
+ case "web_search_call":
307
+ fields.push("action");
308
+ break;
309
+ default:
310
+ return stripItemDiagnostics(item);
311
+ }
312
+ const replay = Object.fromEntries(
313
+ fields.filter((key) => item[key] !== void 0).map((key) => [key, item[key]])
314
+ );
315
+ if (item.type === "reasoning") replay.summary = [];
316
+ if (item.type === "message")
317
+ replay.content = Array.isArray(item.content) ? item.content.flatMap(
318
+ (part) => part?.type === "output_text" && typeof part.text === "string" ? [{ type: "output_text", text: part.text, annotations: [] }] : part?.type === "refusal" && typeof part.refusal === "string" ? [{ type: "refusal", refusal: part.refusal }] : []
319
+ ) : [];
320
+ return stripItemDiagnostics(replay);
321
+ });
322
+ return { openai: { response: { output, refusals: [] } } };
323
+ }
324
+ function stripItemDiagnostics(item) {
325
+ const {
326
+ usage: _usage,
327
+ logprobs: _logprobs,
328
+ metadata: _metadata,
329
+ ...state
330
+ } = item;
331
+ return state;
332
+ }
204
333
  function getResponseReplay(metadata) {
205
334
  const response = metadata?.openai?.response;
206
335
  if (response === void 0) return void 0;
@@ -209,38 +338,7 @@ function getResponseReplay(metadata) {
209
338
  "Invalid OpenAI Responses replay metadata: output must contain typed items"
210
339
  );
211
340
  }
212
- return response.output;
213
- }
214
-
215
- // src/core/provider-error.ts
216
- function providerErrorEvent(error, code) {
217
- const details = error ?? {};
218
- const event = {
219
- type: "error",
220
- message: error instanceof Error ? error.message : String(error),
221
- code,
222
- ...typeof details.status === "number" ? { status: details.status } : {},
223
- ...typeof details.code === "string" ? { providerCode: details.code } : {},
224
- ...typeof details.param === "string" ? { param: details.param } : {},
225
- ...typeof details.request_id === "string" ? { requestId: details.request_id } : {}
226
- };
227
- Object.defineProperty(event, "cause", { value: error, enumerable: false });
228
- return event;
229
- }
230
- function errorFromEvent(event) {
231
- if (event.cause instanceof Error) return event.cause;
232
- return Object.assign(new Error(event.message), {
233
- status: event.status,
234
- code: event.providerCode ?? event.code,
235
- param: event.param,
236
- request_id: event.requestId
237
- });
238
- }
239
- function throwIfAborted(signal) {
240
- if (!signal?.aborted) return;
241
- const error = new Error("The operation was aborted");
242
- error.name = "AbortError";
243
- throw error;
341
+ return responseReplayMetadata(metadata)?.openai?.response?.output;
244
342
  }
245
343
 
246
344
  // src/core/utils.ts
@@ -250,9 +348,6 @@ function generateId(prefix = "id") {
250
348
  function generateMessageId() {
251
349
  return generateId("msg");
252
350
  }
253
- function generateToolCallId() {
254
- return generateId("call");
255
- }
256
351
 
257
352
  // src/adapters/base.ts
258
353
  function stringifyForDebug(value) {
@@ -374,6 +469,7 @@ function toOpenAIResponseFormat(rf) {
374
469
  };
375
470
  }
376
471
  function toOpenAIResponsesTextFormat(rf) {
472
+ if (rf?.type === "json_object") return { type: "json_object" };
377
473
  if (!rf || rf.type !== "json_schema") return void 0;
378
474
  return {
379
475
  type: "json_schema",
@@ -503,6 +599,8 @@ var OpenAIAdapter = class _OpenAIAdapter {
503
599
  apiKey: this.config.apiKey,
504
600
  baseURL: this.config.baseUrl,
505
601
  organization: this.config.organization,
602
+ project: this.config.project,
603
+ timeout: this.config.timeout,
506
604
  defaultHeaders: this.config.headers
507
605
  });
508
606
  }
@@ -567,11 +665,11 @@ var OpenAIAdapter = class _OpenAIAdapter {
567
665
  }
568
666
  buildResponsesInput(request) {
569
667
  const sourceMessages = request.rawMessages && request.rawMessages.length > 0 ? this.formatRawMessages(request.rawMessages, true) : formatMessagesForOpenAI(request.messages, void 0, true);
570
- const input = [];
668
+ const input2 = [];
571
669
  for (const message of sourceMessages) {
572
670
  if (message.role === "system") {
573
671
  if (message.content && message.content !== request.systemPrompt) {
574
- input.push({
672
+ input2.push({
575
673
  type: "message",
576
674
  role: "developer",
577
675
  content: this.toResponsesContent(message.content, "developer")
@@ -582,12 +680,12 @@ var OpenAIAdapter = class _OpenAIAdapter {
582
680
  if (message.role === "assistant") {
583
681
  const replay = getResponseReplay(message.providerMetadata);
584
682
  if (replay) {
585
- input.push(...replay);
683
+ input2.push(...replay);
586
684
  continue;
587
685
  }
588
686
  const content = this.toResponsesContent(message.content, "assistant");
589
687
  if (typeof content === "string" ? content : Array.isArray(content) && content.length > 0) {
590
- input.push({
688
+ input2.push({
591
689
  type: "message",
592
690
  role: "assistant",
593
691
  content
@@ -595,7 +693,7 @@ var OpenAIAdapter = class _OpenAIAdapter {
595
693
  }
596
694
  const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
597
695
  for (const toolCall of toolCalls) {
598
- input.push({
696
+ input2.push({
599
697
  type: "function_call",
600
698
  call_id: toolCall.id,
601
699
  name: toolCall.function?.name,
@@ -605,7 +703,7 @@ var OpenAIAdapter = class _OpenAIAdapter {
605
703
  continue;
606
704
  }
607
705
  if (message.role === "tool") {
608
- input.push({
706
+ input2.push({
609
707
  type: "function_call_output",
610
708
  call_id: message.tool_call_id,
611
709
  output: message.modelOutput ? toolOutputForProvider(message.modelOutput, "responses") : typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? null)
@@ -613,19 +711,19 @@ var OpenAIAdapter = class _OpenAIAdapter {
613
711
  continue;
614
712
  }
615
713
  const role = message.role === "developer" ? "developer" : "user";
616
- input.push({
714
+ input2.push({
617
715
  type: "message",
618
716
  role,
619
717
  content: this.toResponsesContent(message.content, role)
620
718
  });
621
719
  }
622
- return input;
720
+ return input2;
623
721
  }
624
722
  /** Preserve Copilot screenshot attachments on every OpenAI transport. */
625
723
  formatRawMessages(messages, preserveProviderMetadata = false) {
626
724
  return messages.map((message) => {
627
725
  const { attachments, providerMetadata, modelOutput, ...rest } = message;
628
- let content = message.role === "tool" && !preserveProviderMetadata ? textToolOutput(modelOutput, String(message.content ?? "")) : message.content;
726
+ let content = message.role === "tool" && !preserveProviderMetadata && modelOutput !== void 0 ? textToolOutput(modelOutput, "") : message.content;
629
727
  if (preserveProviderMetadata && modelOutput)
630
728
  rest.modelOutput = modelOutput;
631
729
  if (message.role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.length > 0 && content === "") {
@@ -728,17 +826,32 @@ var OpenAIAdapter = class _OpenAIAdapter {
728
826
  const seenCallIds = /* @__PURE__ */ new Set();
729
827
  const toolCalls = Array.isArray(response?.output) ? response.output.filter((item) => item?.type === "function_call").map((item) => {
730
828
  if (typeof item.call_id !== "string" || !item.call_id || typeof item.name !== "string" || !item.name || typeof item.arguments !== "string" || item.status && item.status !== "completed") {
731
- throw new Error("Responses contains an incomplete function call");
829
+ throw invalidProviderResponse(
830
+ "Responses contains an incomplete function call"
831
+ );
732
832
  }
733
833
  if (seenCallIds.has(item.call_id))
734
- throw new Error("Responses contains duplicate function call IDs");
834
+ throw invalidProviderResponse(
835
+ "Responses contains duplicate function call IDs"
836
+ );
735
837
  seenCallIds.add(item.call_id);
736
- const args = JSON.parse(item.arguments);
838
+ let args;
839
+ try {
840
+ args = JSON.parse(item.arguments);
841
+ } catch {
842
+ throw invalidProviderResponse(
843
+ "Responses function arguments must be valid JSON"
844
+ );
845
+ }
737
846
  if (!args || typeof args !== "object" || Array.isArray(args))
738
- throw new Error(
847
+ throw invalidProviderResponse(
739
848
  "Responses function arguments must be a JSON object"
740
849
  );
741
- return { id: item.call_id, name: item.name, args };
850
+ return {
851
+ id: item.call_id,
852
+ name: item.name,
853
+ args
854
+ };
742
855
  }) : [];
743
856
  return {
744
857
  content,
@@ -769,6 +882,16 @@ var OpenAIAdapter = class _OpenAIAdapter {
769
882
  request.config?.responseFormat
770
883
  );
771
884
  const modelIdForPayload = request.config?.model || this.model;
885
+ const include = filterOpenAIResponseInclude(
886
+ modelIdForPayload,
887
+ [
888
+ .../* @__PURE__ */ new Set([
889
+ ...options?.include ?? [],
890
+ ...options?.store === false ? ["reasoning.encrypted_content"] : []
891
+ ])
892
+ ],
893
+ this.parameterOptions(request)
894
+ );
772
895
  const payload = {
773
896
  model: modelIdForPayload,
774
897
  instructions: request.systemPrompt,
@@ -792,14 +915,7 @@ var OpenAIAdapter = class _OpenAIAdapter {
792
915
  ...options?.promptCacheKey !== void 0 ? { prompt_cache_key: options.promptCacheKey } : {},
793
916
  ...options?.promptCacheOptions !== void 0 ? { prompt_cache_options: options.promptCacheOptions } : {},
794
917
  ...options?.promptCacheRetention !== void 0 ? { prompt_cache_retention: options.promptCacheRetention } : {},
795
- ...options?.store === false || options?.include ? {
796
- include: [
797
- .../* @__PURE__ */ new Set([
798
- ...options?.include ?? [],
799
- ...options?.store === false ? ["reasoning.encrypted_content"] : []
800
- ])
801
- ]
802
- } : {},
918
+ ...include ? { include } : {},
803
919
  ...responsesTextFormat ? { text: { format: responsesTextFormat } } : {},
804
920
  stream
805
921
  };
@@ -981,7 +1097,9 @@ var OpenAIAdapter = class _OpenAIAdapter {
981
1097
  const uniqueCalls = /* @__PURE__ */ new Map();
982
1098
  for (const call of toolCalls.values()) {
983
1099
  if (!call.id || !call.name)
984
- throw new Error("Responses stream contains an incomplete tool call");
1100
+ throw invalidProviderResponse(
1101
+ "Responses stream contains an incomplete tool call"
1102
+ );
985
1103
  uniqueCalls.set(call.id, call);
986
1104
  }
987
1105
  toolCalls.clear();
@@ -1168,15 +1286,20 @@ var OpenAIAdapter = class _OpenAIAdapter {
1168
1286
  }
1169
1287
  throwIfAborted(request.signal);
1170
1288
  if (!finishReason)
1171
- throw new Error("OpenAI stream ended without a finish reason");
1289
+ throw invalidProviderResponse(
1290
+ "OpenAI stream ended without a finish reason"
1291
+ );
1172
1292
  if (toolCalls.size && !["tool_calls", "function_call"].includes(finishReason)) {
1173
- throw new Error("OpenAI stream ended with incomplete tool calls");
1174
- }
1175
- for (const call of toolCalls.values()) {
1176
- if (!call.id || !call.name)
1177
- throw new Error("OpenAI stream contains an incomplete tool call");
1178
- JSON.parse(call.arguments || "{}");
1293
+ throw invalidProviderResponse(
1294
+ "OpenAI stream ended with incomplete tool calls"
1295
+ );
1179
1296
  }
1297
+ validateToolCalls(
1298
+ [...toolCalls.values()].map((call) => ({
1299
+ ...call,
1300
+ args: parseToolArguments(call.arguments)
1301
+ }))
1302
+ );
1180
1303
  for (const call of toolCalls.values()) {
1181
1304
  yield {
1182
1305
  type: "action:start",
@@ -1260,20 +1383,20 @@ var OpenAIAdapter = class _OpenAIAdapter {
1260
1383
  logProviderPayload("openai", "response payload", response, request.debug);
1261
1384
  const choice = response.choices?.[0];
1262
1385
  const message = choice?.message;
1386
+ const toolCalls = (message?.tool_calls ?? []).map((toolCall) => ({
1387
+ id: toolCall.id,
1388
+ name: toolCall.function?.name,
1389
+ args: parseToolArguments(toolCall.function?.arguments ?? "{}"),
1390
+ ...toolCall.extra_content ? { extra_content: toolCall.extra_content } : {}
1391
+ }));
1392
+ validateToolCalls(toolCalls);
1393
+ if (toolCalls.length && choice?.finish_reason && !["tool_calls", "function_call"].includes(choice.finish_reason))
1394
+ throw invalidProviderResponse(
1395
+ "OpenAI response ended with incomplete tool calls"
1396
+ );
1263
1397
  return {
1264
1398
  content: message?.content ?? "",
1265
- toolCalls: message?.tool_calls?.map((toolCall) => ({
1266
- id: toolCall.id ?? generateToolCallId(),
1267
- name: toolCall.function?.name ?? "",
1268
- args: (() => {
1269
- try {
1270
- return JSON.parse(toolCall.function?.arguments ?? "{}");
1271
- } catch {
1272
- return {};
1273
- }
1274
- })(),
1275
- ...toolCall.extra_content ? { extra_content: toolCall.extra_content } : {}
1276
- })) ?? [],
1399
+ toolCalls,
1277
1400
  usage: response.usage ? {
1278
1401
  promptTokens: response.usage.prompt_tokens,
1279
1402
  completionTokens: response.usage.completion_tokens,
@@ -1494,9 +1617,14 @@ function openai(modelId, options = {}) {
1494
1617
  (tc) => ({
1495
1618
  id: tc.id,
1496
1619
  name: tc.function.name,
1497
- args: JSON.parse(tc.function.arguments || "{}")
1620
+ args: parseToolArguments(tc.function.arguments)
1498
1621
  })
1499
1622
  );
1623
+ validateToolCalls(toolCalls);
1624
+ if (toolCalls.length && choice.finish_reason && !["tool_calls", "function_call"].includes(choice.finish_reason))
1625
+ throw invalidProviderResponse(
1626
+ "OpenAI response ended with incomplete tool calls"
1627
+ );
1500
1628
  return {
1501
1629
  text: message.content ?? "",
1502
1630
  toolCalls,
@@ -1615,18 +1743,25 @@ function openai(modelId, options = {}) {
1615
1743
  }
1616
1744
  throwIfAborted(params.signal);
1617
1745
  if (!finishReason)
1618
- throw new Error("OpenAI stream ended without a finish reason");
1746
+ throw invalidProviderResponse(
1747
+ "OpenAI stream ended without a finish reason"
1748
+ );
1619
1749
  const calls = [...toolCalls.values()].map((call) => {
1620
1750
  if (!call.id || !call.name)
1621
- throw new Error("OpenAI stream contains an incomplete tool call");
1751
+ throw invalidProviderResponse(
1752
+ "OpenAI stream contains an incomplete tool call"
1753
+ );
1622
1754
  return {
1623
1755
  id: call.id,
1624
1756
  name: call.name,
1625
- args: JSON.parse(call.arguments || "{}")
1757
+ args: parseToolArguments(call.arguments)
1626
1758
  };
1627
1759
  });
1628
1760
  if (calls.length && finishReason !== "tool-calls")
1629
- throw new Error("OpenAI stream ended with incomplete tool calls");
1761
+ throw invalidProviderResponse(
1762
+ "OpenAI stream ended with incomplete tool calls"
1763
+ );
1764
+ validateToolCalls(calls);
1630
1765
  for (const toolCall of calls) yield { type: "tool-call", toolCall };
1631
1766
  yield { type: "finish", finishReason, usage };
1632
1767
  }
@@ -1711,6 +1846,739 @@ function formatMessagesForOpenAI2(messages, responses = false) {
1711
1846
  });
1712
1847
  }
1713
1848
 
1849
+ // src/core/decisions.ts
1850
+ var decisionErrorMarker = /* @__PURE__ */ Symbol.for("yourgpt.llm-sdk.DecisionAPIError");
1851
+ var _a, _b;
1852
+ var DecisionAPIError = class extends (_b = Error, _a = decisionErrorMarker, _b) {
1853
+ constructor(message, code, status, requestId) {
1854
+ super(message);
1855
+ this.code = code;
1856
+ this.status = status;
1857
+ this.requestId = requestId;
1858
+ this.name = "DecisionAPIError";
1859
+ this[_a] = true;
1860
+ }
1861
+ /** Works across CJS/ESM and separately bundled provider entry points. */
1862
+ static isInstance(error) {
1863
+ return typeof error === "object" && error !== null && /* @__PURE__ */ Symbol.for("yourgpt.llm-sdk.DecisionAPIError") in error;
1864
+ }
1865
+ /** Advisory only: this SDK does not retry Decisions automatically. */
1866
+ get retryable() {
1867
+ return this.code === "network_error" || this.code === "http_error" && (this.status === 408 || this.status === 429 || this.status != null && this.status >= 500);
1868
+ }
1869
+ };
1870
+
1871
+ // src/core/decision-validation.ts
1872
+ function decisionInputError(message) {
1873
+ throw new DecisionAPIError(message, "invalid_input");
1874
+ }
1875
+ function decisionResponseError(message) {
1876
+ throw new DecisionAPIError(message, "invalid_response");
1877
+ }
1878
+ function isDecisionRecord(value) {
1879
+ if (value === null || typeof value !== "object" || Array.isArray(value))
1880
+ return false;
1881
+ const proto = Object.getPrototypeOf(value);
1882
+ return proto === Object.prototype || proto === null;
1883
+ }
1884
+ function json(value, seen = /* @__PURE__ */ new Set(), depth = 0) {
1885
+ if (depth > 64) return false;
1886
+ if (value === null || typeof value === "string" || typeof value === "boolean")
1887
+ return true;
1888
+ if (typeof value === "number") return Number.isFinite(value);
1889
+ if (typeof value !== "object" || !Array.isArray(value) && !isDecisionRecord(value) || seen.has(value))
1890
+ return false;
1891
+ seen.add(value);
1892
+ const valid = Object.getOwnPropertySymbols(value).length === 0 && (Array.isArray(value) ? Array.from(value) : Object.values(value)).every(
1893
+ (item) => json(item, seen, depth + 1)
1894
+ );
1895
+ seen.delete(value);
1896
+ return valid;
1897
+ }
1898
+ function input(value) {
1899
+ return (typeof value === "string" || Array.isArray(value) || isDecisionRecord(value)) && json(value);
1900
+ }
1901
+ function assertDecisionKeys(value, allowed, label) {
1902
+ if (Object.keys(value).some((key) => !allowed.includes(key)))
1903
+ decisionInputError(`${label} contains an unsupported field`);
1904
+ }
1905
+ function validateDecisionInput(options) {
1906
+ if (!isDecisionRecord(options))
1907
+ decisionInputError("Decisions options must be an object");
1908
+ assertDecisionKeys(
1909
+ options,
1910
+ ["state", "questions", "images", "signal", "timeoutMs", "providerOptions"],
1911
+ "Decisions options"
1912
+ );
1913
+ if (!input(options.state))
1914
+ decisionInputError(
1915
+ "state must be finite, acyclic JSON text, object, or array (maximum depth 64)"
1916
+ );
1917
+ if (!isDecisionRecord(options.questions) || Object.keys(options.questions).length === 0)
1918
+ decisionInputError("questions must be a nonempty map");
1919
+ for (const [id, question] of Object.entries(options.questions)) {
1920
+ if (!id.length || !isDecisionRecord(question) || !input(question.instructions))
1921
+ decisionInputError(
1922
+ "Every question needs a nonempty ID and JSON-compatible instructions"
1923
+ );
1924
+ assertDecisionKeys(
1925
+ question,
1926
+ ["type", "instructions", "criteria"],
1927
+ "Question"
1928
+ );
1929
+ if (question.type === "boolean") {
1930
+ if (question.criteria !== void 0 && (!isDecisionRecord(question.criteria) || Object.entries(question.criteria).some(
1931
+ ([key, value]) => !["true", "false"].includes(key) || !input(value)
1932
+ )))
1933
+ decisionInputError("Boolean criteria may describe only true and false");
1934
+ } else if (question.type === "choice") {
1935
+ if (!isDecisionRecord(question.criteria) || Object.keys(question.criteria).length < 2 || Object.keys(question.criteria).length > 255 || Object.entries(question.criteria).some(
1936
+ ([key, value]) => !key.length || value !== null && !input(value)
1937
+ ))
1938
+ decisionInputError(
1939
+ "Choice criteria require 2\u2013255 named options with JSON descriptions or null"
1940
+ );
1941
+ } else if (question.type === "score") {
1942
+ if (!Array.isArray(question.criteria) || question.criteria.length < 2 || !Array.from(question.criteria).every(input))
1943
+ decisionInputError(
1944
+ "Score criteria require at least two ordered JSON descriptions"
1945
+ );
1946
+ } else
1947
+ decisionInputError("Question type must be boolean, choice, or score");
1948
+ }
1949
+ if (options.timeoutMs !== void 0 && (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 1 || options.timeoutMs > 2147483647))
1950
+ decisionInputError("timeoutMs must be a positive 32-bit integer");
1951
+ if (options.providerOptions !== void 0 && (!isDecisionRecord(options.providerOptions) || !json(options.providerOptions)))
1952
+ decisionInputError("providerOptions must be a JSON object");
1953
+ if (options.images !== void 0 && (!Array.isArray(options.images) || !Array.from(options.images).every(
1954
+ (image) => isDecisionRecord(image) && image.data instanceof Uint8Array
1955
+ )))
1956
+ decisionInputError(
1957
+ "Decision images must contain encoded bytes; hosted image URLs are unsupported"
1958
+ );
1959
+ }
1960
+ function probability(value) {
1961
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
1962
+ }
1963
+ function exactKeys(value, keys) {
1964
+ return Object.keys(value).length === keys.length && keys.every((key) => Object.prototype.hasOwnProperty.call(value, key));
1965
+ }
1966
+ function validateDecisionResult(questions, result) {
1967
+ if (!isDecisionRecord(result) || !isDecisionRecord(result.answers) || !exactKeys(result.answers, Object.keys(questions)))
1968
+ decisionResponseError(
1969
+ "Decisions must return exactly one answer per question"
1970
+ );
1971
+ const rounding = result.rounding;
1972
+ let pError = 0, sError = 0;
1973
+ if (rounding !== void 0) {
1974
+ if (!isDecisionRecord(rounding) || ![rounding.probabilityDecimals, rounding.scoreDecimals].every(
1975
+ (v) => typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 15
1976
+ ))
1977
+ decisionResponseError("Invalid decision rounding precision");
1978
+ pError = 0.5 * 10 ** -rounding.probabilityDecimals;
1979
+ sError = 0.5 * 10 ** -rounding.scoreDecimals;
1980
+ }
1981
+ const epsilon = 1e-6;
1982
+ for (const [id, question] of Object.entries(questions)) {
1983
+ const answer = result.answers[id];
1984
+ if (!isDecisionRecord(answer))
1985
+ decisionResponseError("Invalid decision answer");
1986
+ if (answer.type === "refusal") continue;
1987
+ if (answer.type !== question.type)
1988
+ decisionResponseError("Decision answer type does not match its question");
1989
+ if (answer.confidence !== void 0 && !probability(answer.confidence))
1990
+ decisionResponseError(
1991
+ "Confidence must be finite and between zero and one"
1992
+ );
1993
+ if (question.type === "boolean") {
1994
+ if (!probability(answer.probability))
1995
+ decisionResponseError(
1996
+ "Boolean probability must be between zero and one"
1997
+ );
1998
+ continue;
1999
+ }
2000
+ const keys = question.type === "choice" ? Object.keys(question.criteria) : question.criteria.map((_, index) => String(index));
2001
+ if (question.type === "choice" && (typeof answer.choice !== "string" || !keys.includes(answer.choice)))
2002
+ decisionResponseError("Decision selected an unknown choice");
2003
+ if (question.type === "score" && (typeof answer.score !== "number" || !Number.isFinite(answer.score) || answer.score < 0 || answer.score > keys.length - 1))
2004
+ decisionResponseError("Decision score is outside its ordered levels");
2005
+ if (answer.probabilities === void 0) continue;
2006
+ const distribution2 = answer.probabilities;
2007
+ if (!isDecisionRecord(distribution2) || !exactKeys(distribution2, keys) || !Object.values(distribution2).every(probability))
2008
+ decisionResponseError(
2009
+ "Decision distribution must contain every option exactly once with finite probabilities"
2010
+ );
2011
+ const values = Object.values(distribution2);
2012
+ if (Math.abs(values.reduce((a, b) => a + b, 0) - 1) > epsilon + keys.length * pError)
2013
+ decisionResponseError("Decision probabilities do not sum to one");
2014
+ if (question.type === "choice") {
2015
+ if (values.some(
2016
+ (v) => v > distribution2[answer.choice] + epsilon + 2 * pError
2017
+ ))
2018
+ decisionResponseError(
2019
+ "Decision choice is not a highest-probability option"
2020
+ );
2021
+ } else {
2022
+ const mean = keys.reduce(
2023
+ (sum, key) => sum + Number(key) * distribution2[key],
2024
+ 0
2025
+ );
2026
+ const tolerance = epsilon + sError + pError * keys.reduce((sum, key) => sum + Number(key), 0);
2027
+ if (Math.abs(answer.score - mean) > tolerance)
2028
+ decisionResponseError(
2029
+ "Decision score does not match its probability-weighted levels"
2030
+ );
2031
+ }
2032
+ }
2033
+ }
2034
+
2035
+ // src/providers/decisions/http.ts
2036
+ async function postDecision(url, headers, body, options, fetchFn, maxBytes) {
2037
+ const encoded = JSON.stringify(body);
2038
+ if (Buffer.byteLength(encoded, "utf8") > maxBytes)
2039
+ decisionInputError("Decision request exceeds the provider's byte limit");
2040
+ options.signal?.throwIfAborted();
2041
+ const controller = new AbortController();
2042
+ const relay = () => controller.abort(options.signal?.reason);
2043
+ options.signal?.addEventListener("abort", relay, { once: true });
2044
+ const timer = setTimeout(
2045
+ () => controller.abort(
2046
+ new DOMException("Decision request timed out", "TimeoutError")
2047
+ ),
2048
+ options.timeoutMs ?? 6e4
2049
+ );
2050
+ let rejectAbort = () => {
2051
+ };
2052
+ const aborted = new Promise((_, reject) => {
2053
+ rejectAbort = () => reject(controller.signal.reason);
2054
+ controller.signal.addEventListener("abort", rejectAbort, { once: true });
2055
+ });
2056
+ const perform = async () => {
2057
+ let response;
2058
+ try {
2059
+ response = await fetchFn(url, {
2060
+ method: "POST",
2061
+ headers,
2062
+ body: encoded,
2063
+ signal: controller.signal,
2064
+ redirect: "error"
2065
+ });
2066
+ } catch {
2067
+ controller.signal.throwIfAborted();
2068
+ throw new DecisionAPIError(
2069
+ "Decision request failed before receiving a response",
2070
+ "network_error"
2071
+ );
2072
+ }
2073
+ const requestId = response.headers.get("x-request-id") ?? response.headers.get("cf-ray") ?? void 0;
2074
+ if (controller.signal.aborted) {
2075
+ void response.body?.cancel().catch(() => {
2076
+ });
2077
+ controller.signal.throwIfAborted();
2078
+ }
2079
+ if (!response.ok) {
2080
+ void response.body?.cancel().catch(() => {
2081
+ });
2082
+ throw new DecisionAPIError(
2083
+ `Decision API returned HTTP ${response.status}`,
2084
+ "http_error",
2085
+ response.status,
2086
+ requestId
2087
+ );
2088
+ }
2089
+ if (!response.body)
2090
+ decisionResponseError("Decision API returned an empty body");
2091
+ const reader = response.body.getReader();
2092
+ const cancel = () => {
2093
+ void reader.cancel().catch(() => {
2094
+ });
2095
+ };
2096
+ controller.signal.addEventListener("abort", cancel, { once: true });
2097
+ const chunks = [];
2098
+ let size = 0;
2099
+ try {
2100
+ while (true) {
2101
+ controller.signal.throwIfAborted();
2102
+ const { value: value2, done } = await reader.read();
2103
+ if (done) break;
2104
+ size += value2.byteLength;
2105
+ if (size > 8 * 1024 * 1024) {
2106
+ cancel();
2107
+ decisionResponseError("Decision API response exceeds 8 MiB");
2108
+ }
2109
+ chunks.push(value2);
2110
+ }
2111
+ } catch (error) {
2112
+ controller.signal.throwIfAborted();
2113
+ if (error instanceof DecisionAPIError) throw error;
2114
+ throw new DecisionAPIError(
2115
+ "Decision response could not be read",
2116
+ "network_error",
2117
+ response.status,
2118
+ requestId
2119
+ );
2120
+ } finally {
2121
+ controller.signal.removeEventListener("abort", cancel);
2122
+ reader.releaseLock();
2123
+ }
2124
+ controller.signal.throwIfAborted();
2125
+ let value;
2126
+ try {
2127
+ value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
2128
+ } catch {
2129
+ decisionResponseError("Decision API did not return valid JSON");
2130
+ }
2131
+ return { value, status: response.status, requestId };
2132
+ };
2133
+ try {
2134
+ return await Promise.race([perform(), aborted]);
2135
+ } finally {
2136
+ clearTimeout(timer);
2137
+ options.signal?.removeEventListener("abort", relay);
2138
+ controller.signal.removeEventListener("abort", rejectAbort);
2139
+ }
2140
+ }
2141
+
2142
+ // src/providers/decisions/images.ts
2143
+ function unsupportedDecision(message) {
2144
+ throw new DecisionAPIError(message, "unsupported_option");
2145
+ }
2146
+ function prepareDecisionImages(images, provider) {
2147
+ if (provider === "openrouter" && images.length)
2148
+ unsupportedDecision(
2149
+ "OpenRouter's native Decisions schema does not support image inputs"
2150
+ );
2151
+ if (provider === "cloudflare" && images.length > 4)
2152
+ decisionInputError("Cloudflare Decisions supports at most four images");
2153
+ if (provider === "openai" && images.length > 128)
2154
+ decisionInputError("OpenAI Decisions supports at most 128 images");
2155
+ let total = 0;
2156
+ return images.map((image) => {
2157
+ assertDecisionKeys(
2158
+ image,
2159
+ ["data", "mediaType", "detail"],
2160
+ "Decision image"
2161
+ );
2162
+ if (image.detail !== void 0) {
2163
+ if (provider !== "openai")
2164
+ unsupportedDecision(
2165
+ "Image detail is only supported by OpenAI Decisions"
2166
+ );
2167
+ if (!["auto", "low", "high", "original"].includes(image.detail))
2168
+ decisionInputError("Invalid image detail");
2169
+ }
2170
+ const data = Buffer.from(image.data);
2171
+ const signature = data.subarray(0, 12).toString("latin1");
2172
+ const mediaType = signature.startsWith("\x89PNG\r\n\n") ? "image/png" : data[0] === 255 && data[1] === 216 && data[2] === 255 ? "image/jpeg" : signature.startsWith("RIFF") && signature.slice(8) === "WEBP" ? "image/webp" : /^GIF8[79]a/.test(signature) ? "image/gif" : void 0;
2173
+ if (!mediaType || image.mediaType !== void 0 && image.mediaType !== mediaType)
2174
+ decisionInputError(
2175
+ "Decision image bytes must match a supported encoded image format"
2176
+ );
2177
+ if (provider === "typesafe" && !["image/png", "image/jpeg"].includes(mediaType))
2178
+ unsupportedDecision("Jev images must be PNG or JPEG");
2179
+ if (provider === "cloudflare" && mediaType === "image/gif")
2180
+ unsupportedDecision("Cloudflare Decisions does not accept GIF images");
2181
+ const limit = provider === "cloudflare" ? 4 : provider === "typesafe" ? 16 : 20;
2182
+ if (data.length > limit * 1024 * 1024)
2183
+ decisionInputError("Decision image exceeds this provider's file limit");
2184
+ total += data.length;
2185
+ if (provider === "cloudflare" && total > 8 * 1024 * 1024)
2186
+ decisionInputError("Cloudflare Decisions images exceed 8 MiB total");
2187
+ const base64 = data.toString("base64");
2188
+ return {
2189
+ url: `data:${mediaType};base64,${base64}`,
2190
+ base64,
2191
+ mediaType,
2192
+ detail: image.detail
2193
+ };
2194
+ });
2195
+ }
2196
+
2197
+ // src/providers/decisions/response.ts
2198
+ function distribution(value, score) {
2199
+ if (!Array.isArray(value))
2200
+ decisionResponseError(
2201
+ "OpenAI Decisions omitted its probability distribution"
2202
+ );
2203
+ const entries = [];
2204
+ const seen = /* @__PURE__ */ new Set();
2205
+ for (const item of value) {
2206
+ if (!isDecisionRecord(item) || (score ? typeof item.value !== "number" || !Number.isInteger(item.value) || item.value < 0 : typeof item.value !== "string"))
2207
+ decisionResponseError("Invalid decision probability option");
2208
+ const key = String(item.value);
2209
+ if (seen.has(key))
2210
+ decisionResponseError("Duplicate option in decision probabilities");
2211
+ seen.add(key);
2212
+ entries.push([key, item.probability]);
2213
+ }
2214
+ return Object.fromEntries(entries);
2215
+ }
2216
+ function parseDecisionResponse(raw, provider, questions, status, requestId) {
2217
+ if (!isDecisionRecord(raw))
2218
+ decisionResponseError("Decision response must be an object");
2219
+ let payload = raw;
2220
+ if (provider === "cloudflare") {
2221
+ if (raw.success === false)
2222
+ throw new DecisionAPIError(
2223
+ "Cloudflare reported an unsuccessful decision request",
2224
+ "http_error",
2225
+ status,
2226
+ requestId
2227
+ );
2228
+ if (raw.success !== true || !isDecisionRecord(raw.result))
2229
+ decisionResponseError(
2230
+ "Cloudflare Decisions returned an invalid result envelope"
2231
+ );
2232
+ payload = raw.result;
2233
+ }
2234
+ if (typeof payload.model !== "string" || !payload.model)
2235
+ decisionResponseError("Decision response omitted its model identity");
2236
+ const entries = [];
2237
+ if (provider === "openai") {
2238
+ if (!Array.isArray(payload.answers))
2239
+ decisionResponseError("OpenAI Decisions answers must be an array");
2240
+ const seen = /* @__PURE__ */ new Set();
2241
+ for (const answer of payload.answers) {
2242
+ if (!isDecisionRecord(answer))
2243
+ decisionResponseError("Invalid OpenAI decision answer");
2244
+ if (answer.type === "refusal" && answer.name == null)
2245
+ throw new DecisionAPIError(
2246
+ "OpenAI refused an unnamed decision question",
2247
+ "refusal",
2248
+ status,
2249
+ requestId
2250
+ );
2251
+ if (typeof answer.name !== "string" || seen.has(answer.name))
2252
+ decisionResponseError(
2253
+ "OpenAI decision answers have missing or duplicate question identities"
2254
+ );
2255
+ seen.add(answer.name);
2256
+ entries.push([answer.name, answer]);
2257
+ }
2258
+ } else {
2259
+ if (!isDecisionRecord(payload.answers))
2260
+ decisionResponseError("Decisions answers must be a named map");
2261
+ entries.push(...Object.entries(payload.answers));
2262
+ }
2263
+ const answers = Object.fromEntries(
2264
+ entries.map(([id, value]) => {
2265
+ if (!isDecisionRecord(value))
2266
+ decisionResponseError("Invalid decision answer");
2267
+ if (value.type === "refusal" && provider === "openai")
2268
+ return [id, { type: "refusal" }];
2269
+ const booleanType = provider === "openai" ? "predicate" : "noul";
2270
+ if (value.type === booleanType)
2271
+ return [
2272
+ id,
2273
+ {
2274
+ type: "boolean",
2275
+ probability: provider === "openai" ? value.probability : value.noul
2276
+ }
2277
+ ];
2278
+ if (value.type !== "choice" && value.type !== "score")
2279
+ decisionResponseError(
2280
+ "Provider returned an unsupported decision answer type"
2281
+ );
2282
+ if (provider !== "openrouter" && value.probabilities == null)
2283
+ decisionResponseError(
2284
+ "Provider omitted a required decision distribution"
2285
+ );
2286
+ const probabilities = provider === "openai" ? distribution(value.probabilities, value.type === "score") : value.probabilities;
2287
+ const fields = {
2288
+ ...probabilities !== void 0 && {
2289
+ probabilities
2290
+ },
2291
+ ...value.confidence != null && {
2292
+ confidence: value.confidence
2293
+ }
2294
+ };
2295
+ return [
2296
+ id,
2297
+ value.type === "choice" ? { type: "choice", choice: value.choice, ...fields } : { type: "score", score: value.score, ...fields }
2298
+ ];
2299
+ })
2300
+ );
2301
+ const result = {
2302
+ answers,
2303
+ response: {
2304
+ provider,
2305
+ modelId: payload.model,
2306
+ status,
2307
+ requestId: requestId ?? (typeof payload.id === "string" ? payload.id : void 0),
2308
+ body: raw
2309
+ }
2310
+ };
2311
+ const typesafe = provider === "typesafe" || provider === "openrouter" && (payload.model.startsWith("typesafe/") || payload.provider === "TypeSafe");
2312
+ if (typesafe) result.rounding = { probabilityDecimals: 2, scoreDecimals: 2 };
2313
+ if (payload.usage != null) {
2314
+ if (!isDecisionRecord(payload.usage))
2315
+ decisionResponseError("Invalid decision usage");
2316
+ const usage = payload.usage;
2317
+ const details = isDecisionRecord(usage.input_tokens_details) ? usage.input_tokens_details : {};
2318
+ result.usage = {};
2319
+ const mapped = {
2320
+ inputTokens: usage.input_tokens,
2321
+ outputTokens: usage.output_tokens,
2322
+ totalTokens: usage.total_tokens,
2323
+ cachedInputTokens: details.cached_tokens,
2324
+ cacheWriteTokens: details.cache_write_tokens,
2325
+ costUsd: usage.cost
2326
+ };
2327
+ for (const [key, value] of Object.entries(mapped)) {
2328
+ if (value == null) continue;
2329
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || key !== "costUsd" && !Number.isInteger(value))
2330
+ decisionResponseError("Invalid decision usage count");
2331
+ result.usage[key] = value;
2332
+ }
2333
+ }
2334
+ validateDecisionResult(questions, result);
2335
+ return result;
2336
+ }
2337
+
2338
+ // src/providers/decisions/model.ts
2339
+ function asText(value) {
2340
+ return typeof value === "string" ? value : JSON.stringify(value);
2341
+ }
2342
+ function requestOptions(options, provider) {
2343
+ for (const key of Object.keys(options.providerOptions ?? {})) {
2344
+ if (key !== provider)
2345
+ unsupportedDecision(
2346
+ "Decision providerOptions must match the selected provider"
2347
+ );
2348
+ }
2349
+ const opts = options.providerOptions?.[provider] ?? {};
2350
+ if (!isDecisionRecord(opts))
2351
+ decisionInputError("Decision provider options must be an object");
2352
+ const allowed = provider === "openai" ? ["safetyIdentifier", "promptCacheKey"] : provider === "typesafe" ? ["longContext"] : provider === "openrouter" ? ["sessionId", "provider"] : [];
2353
+ assertDecisionKeys(opts, allowed, "Decision provider options");
2354
+ for (const [key, limit] of [
2355
+ ["safetyIdentifier", 128],
2356
+ ["promptCacheKey", 64],
2357
+ ["sessionId", 256]
2358
+ ]) {
2359
+ if (opts[key] !== void 0 && (typeof opts[key] !== "string" || opts[key].length > limit))
2360
+ decisionInputError("Invalid decision identifier or cache key");
2361
+ }
2362
+ if (opts.longContext !== void 0 && typeof opts.longContext !== "boolean")
2363
+ decisionInputError("longContext must be boolean");
2364
+ if (opts.provider !== void 0) {
2365
+ if (!isDecisionRecord(opts.provider))
2366
+ decisionInputError("OpenRouter routing preferences must be an object");
2367
+ assertDecisionKeys(
2368
+ opts.provider,
2369
+ ["order", "allowFallbacks"],
2370
+ "OpenRouter routing preferences"
2371
+ );
2372
+ if (opts.provider.allowFallbacks !== void 0 && typeof opts.provider.allowFallbacks !== "boolean")
2373
+ decisionInputError("allowFallbacks must be boolean");
2374
+ if (opts.provider.order !== void 0 && (!Array.isArray(opts.provider.order) || !opts.provider.order.every(
2375
+ (value) => typeof value === "string" && value.length
2376
+ )))
2377
+ decisionInputError("Provider order must be an array of provider names");
2378
+ }
2379
+ return opts;
2380
+ }
2381
+ function nativeDecisionModel(modelId, config) {
2382
+ if (typeof modelId !== "string" || !modelId.trim())
2383
+ decisionInputError("Decision model ID is required");
2384
+ const provider = config.provider;
2385
+ return {
2386
+ provider,
2387
+ modelId,
2388
+ async doDecide(options) {
2389
+ validateDecisionInput(options);
2390
+ options.signal?.throwIfAborted();
2391
+ const opts = requestOptions(options, provider);
2392
+ const questions = Object.entries(options.questions);
2393
+ if (provider === "openai" && questions.length > 200)
2394
+ decisionInputError("OpenAI Decisions supports at most 200 questions");
2395
+ if (provider === "cloudflare" && (questions.length > 64 || questions.some(([id]) => !/^[A-Za-z0-9_.-]{1,100}$/.test(id))))
2396
+ decisionInputError(
2397
+ "Cloudflare supports 1\u201364 questions with IDs of 1\u2013100 letters, digits, underscores, dots, or hyphens"
2398
+ );
2399
+ for (const [, question] of questions) {
2400
+ if (provider !== "openai" && question.type === "score" && question.criteria.length > 10)
2401
+ decisionInputError(
2402
+ "System One adapters support at most ten score levels"
2403
+ );
2404
+ if (provider === "cloudflare" && typeof question.instructions === "string" && !question.instructions.trim())
2405
+ decisionInputError("Cloudflare requires nonempty instructions");
2406
+ if (provider === "openrouter" && question.type === "boolean" && question.criteria !== void 0 && (question.criteria.true === void 0 || question.criteria.false === void 0))
2407
+ decisionInputError(
2408
+ "OpenRouter boolean criteria must describe both true and false when supplied"
2409
+ );
2410
+ }
2411
+ const images = prepareDecisionImages(options.images ?? [], provider);
2412
+ const headers = Object.fromEntries(
2413
+ Object.entries(config.headers ?? {}).filter(
2414
+ ([name]) => !["authorization", "content-type"].includes(name.toLowerCase())
2415
+ )
2416
+ );
2417
+ headers["Content-Type"] = "application/json";
2418
+ const key = config.apiKey ?? process.env[config.keyEnv];
2419
+ if (!key?.trim())
2420
+ decisionInputError(`Missing ${config.keyEnv} for Decisions`);
2421
+ headers.Authorization = `Bearer ${key}`;
2422
+ let body;
2423
+ if (provider === "openai") {
2424
+ body = {
2425
+ model: modelId,
2426
+ input: [
2427
+ {
2428
+ role: "user",
2429
+ content: [
2430
+ { type: "input_text", text: asText(options.state) },
2431
+ ...images.map((image) => ({
2432
+ type: "input_image",
2433
+ image_url: image.url,
2434
+ ...image.detail !== void 0 && { detail: image.detail }
2435
+ }))
2436
+ ]
2437
+ }
2438
+ ],
2439
+ questions: questions.map(([name, question]) => {
2440
+ const instructions = asText(question.instructions);
2441
+ if (question.type === "boolean")
2442
+ return {
2443
+ type: "predicate",
2444
+ name,
2445
+ instructions: [
2446
+ instructions,
2447
+ ...Object.entries(question.criteria ?? {}).map(
2448
+ ([key2, value]) => `Criteria for ${key2}:
2449
+ ${asText(value)}`
2450
+ )
2451
+ ].join("\n\n")
2452
+ };
2453
+ if (question.type === "choice")
2454
+ return {
2455
+ type: "choice",
2456
+ name,
2457
+ instructions,
2458
+ choices: Object.entries(question.criteria).map(
2459
+ ([value, description]) => ({
2460
+ value,
2461
+ ...description !== null && {
2462
+ description: asText(description)
2463
+ }
2464
+ })
2465
+ )
2466
+ };
2467
+ return {
2468
+ type: "score",
2469
+ name,
2470
+ instructions,
2471
+ levels: question.criteria.map((description, index) => ({
2472
+ label: String(index),
2473
+ description: asText(description)
2474
+ }))
2475
+ };
2476
+ }),
2477
+ ...opts.safetyIdentifier !== void 0 && {
2478
+ safety_identifier: opts.safetyIdentifier
2479
+ },
2480
+ ...opts.promptCacheKey !== void 0 && {
2481
+ prompt_cache_key: opts.promptCacheKey
2482
+ }
2483
+ };
2484
+ } else {
2485
+ body = {
2486
+ model: config.bodyModel ?? modelId,
2487
+ state: options.state,
2488
+ questions: Object.fromEntries(
2489
+ questions.map(([id, question]) => [
2490
+ id,
2491
+ {
2492
+ ...question,
2493
+ type: question.type === "boolean" ? "noul" : question.type
2494
+ }
2495
+ ])
2496
+ )
2497
+ };
2498
+ if (provider === "typesafe") {
2499
+ if (opts.longContext)
2500
+ headers["TypeSafe-Danger-Zone-Long-Context"] = "true";
2501
+ if (images.length) {
2502
+ const refs = images.map(() => crypto.randomUUID());
2503
+ body.state = { state: options.state, images: refs };
2504
+ body.assets = Object.fromEntries(
2505
+ images.map((image, index) => [
2506
+ refs[index],
2507
+ {
2508
+ type: "image",
2509
+ data_base64: image.base64,
2510
+ format: image.mediaType === "image/png" ? "png" : "jpeg"
2511
+ }
2512
+ ])
2513
+ );
2514
+ headers["TypeSafe-Danger-Zone-Images"] = "true";
2515
+ }
2516
+ } else if (provider === "cloudflare" && images.length) {
2517
+ body.images = images.map((image) => ({
2518
+ content_type: image.mediaType,
2519
+ base64: image.base64
2520
+ }));
2521
+ } else if (provider === "openrouter") {
2522
+ if (opts.sessionId !== void 0) body.session_id = opts.sessionId;
2523
+ if (isDecisionRecord(opts.provider))
2524
+ body.provider = {
2525
+ ...opts.provider.order !== void 0 && {
2526
+ order: opts.provider.order
2527
+ },
2528
+ ...opts.provider.allowFallbacks !== void 0 && {
2529
+ allow_fallbacks: opts.provider.allowFallbacks
2530
+ }
2531
+ };
2532
+ }
2533
+ }
2534
+ const base = config.baseUrl ?? config.defaultBaseUrl;
2535
+ let url;
2536
+ try {
2537
+ url = new URL(`${base.replace(/\/+$/, "")}${config.path}`);
2538
+ } catch {
2539
+ decisionInputError("Invalid Decisions base URL");
2540
+ }
2541
+ if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash)
2542
+ decisionInputError(
2543
+ "Decisions base URL must be an HTTP(S) API root without credentials, query, or fragment"
2544
+ );
2545
+ const response = await postDecision(
2546
+ url.href,
2547
+ headers,
2548
+ body,
2549
+ options,
2550
+ config.fetch ?? globalThis.fetch,
2551
+ (provider === "cloudflare" ? 13 : 32) * 1024 * 1024
2552
+ );
2553
+ return parseDecisionResponse(
2554
+ response.value,
2555
+ provider,
2556
+ options.questions,
2557
+ response.status,
2558
+ response.requestId
2559
+ );
2560
+ }
2561
+ };
2562
+ }
2563
+
2564
+ // src/providers/openai/decisions.ts
2565
+ function openaiDecision(modelId = "gpt-6-luna", options = {}) {
2566
+ return nativeDecisionModel(modelId, {
2567
+ ...options,
2568
+ headers: {
2569
+ ...options.headers,
2570
+ ...options.organization && {
2571
+ "OpenAI-Organization": options.organization
2572
+ },
2573
+ ...options.project && { "OpenAI-Project": options.project }
2574
+ },
2575
+ provider: "openai",
2576
+ keyEnv: "OPENAI_API_KEY",
2577
+ defaultBaseUrl: "https://api.openai.com/v1",
2578
+ path: "/decisions"
2579
+ });
2580
+ }
2581
+
1714
2582
  // src/providers/types.ts
1715
2583
  function createCallableProvider(providerFn, properties) {
1716
2584
  Object.defineProperty(providerFn, "name", {
@@ -1729,17 +2597,17 @@ function createCallableProvider(providerFn, properties) {
1729
2597
 
1730
2598
  // src/core/audio.ts
1731
2599
  var audioErrorMarker = /* @__PURE__ */ Symbol.for("@yourgpt/llm-sdk/AudioAPIError");
1732
- var _a;
2600
+ var _a2;
1733
2601
  var AudioAPIError = class extends Error {
1734
2602
  constructor(message, code, status, requestId) {
1735
2603
  super(message);
1736
2604
  this.code = code;
1737
2605
  this.status = status;
1738
2606
  this.requestId = requestId;
1739
- this[_a] = true;
2607
+ this[_a2] = true;
1740
2608
  this.name = "AudioAPIError";
1741
2609
  }
1742
- static [(_a = audioErrorMarker, Symbol.hasInstance)](value) {
2610
+ static [(_a2 = audioErrorMarker, Symbol.hasInstance)](value) {
1743
2611
  return !!value && typeof value === "object" && value[audioErrorMarker] === true;
1744
2612
  }
1745
2613
  };
@@ -2115,6 +2983,497 @@ function openaiTranscription(modelId = "gpt-4o-mini-transcribe", config = {}) {
2115
2983
  };
2116
2984
  }
2117
2985
 
2986
+ // src/core/image.ts
2987
+ var marker = /* @__PURE__ */ Symbol.for("@yourgpt/llm-sdk/MediaAPIError");
2988
+ var _a3;
2989
+ var MediaAPIError = class extends Error {
2990
+ constructor(message, code, status, requestId) {
2991
+ super(message);
2992
+ this.code = code;
2993
+ this.status = status;
2994
+ this.requestId = requestId;
2995
+ this[_a3] = true;
2996
+ this.name = "MediaAPIError";
2997
+ }
2998
+ static [(_a3 = marker, Symbol.hasInstance)](value) {
2999
+ return !!value && typeof value === "object" && value[marker] === true;
3000
+ }
3001
+ };
3002
+
3003
+ // src/providers/openai/media-http.ts
3004
+ function invalidMedia(message) {
3005
+ throw new MediaAPIError(message, "invalid_input");
3006
+ }
3007
+ async function postMedia(config, path, body, options = {}) {
3008
+ options.signal?.throwIfAborted();
3009
+ const timeout = options.timeoutMs ?? 12e4;
3010
+ if (!Number.isFinite(timeout) || timeout <= 0 || timeout > 6e5)
3011
+ invalidMedia("timeoutMs must be between 1 and 600000");
3012
+ const key = config.apiKey ?? process.env.OPENAI_API_KEY;
3013
+ if (!key) invalidMedia("An OpenAI API key is required on the server");
3014
+ const headers = { Authorization: `Bearer ${key}` };
3015
+ if (typeof body === "string") headers["Content-Type"] = "application/json";
3016
+ if (config.organization) headers["OpenAI-Organization"] = config.organization;
3017
+ if (config.project) headers["OpenAI-Project"] = config.project;
3018
+ if (options.safetyIdentifier)
3019
+ headers["OpenAI-Safety-Identifier"] = options.safetyIdentifier;
3020
+ const controller = new AbortController();
3021
+ const relay = () => controller.abort(options.signal?.reason);
3022
+ options.signal?.addEventListener("abort", relay, { once: true });
3023
+ const timer = setTimeout(
3024
+ () => controller.abort(
3025
+ new DOMException("Media request timed out", "TimeoutError")
3026
+ ),
3027
+ timeout
3028
+ );
3029
+ let rejectAbort = () => {
3030
+ };
3031
+ const aborted = new Promise((_, reject) => {
3032
+ rejectAbort = () => reject(controller.signal.reason);
3033
+ controller.signal.addEventListener("abort", rejectAbort, { once: true });
3034
+ });
3035
+ const perform = async () => {
3036
+ let response;
3037
+ try {
3038
+ response = await (config.fetch ?? globalThis.fetch)(
3039
+ `${(config.baseUrl ?? "https://api.openai.com/v1").replace(/\/$/, "")}${path}`,
3040
+ {
3041
+ method: "POST",
3042
+ headers,
3043
+ body,
3044
+ signal: controller.signal,
3045
+ redirect: "error"
3046
+ }
3047
+ );
3048
+ } catch {
3049
+ controller.signal.throwIfAborted();
3050
+ throw new MediaAPIError(
3051
+ "OpenAI media connection failed",
3052
+ "network_error"
3053
+ );
3054
+ }
3055
+ const requestId = response.headers.get("x-request-id") ?? void 0;
3056
+ if (controller.signal.aborted || !response.ok) {
3057
+ void response.body?.cancel().catch(() => {
3058
+ });
3059
+ controller.signal.throwIfAborted();
3060
+ throw new MediaAPIError(
3061
+ `OpenAI media returned HTTP ${response.status}`,
3062
+ "http_error",
3063
+ response.status,
3064
+ requestId
3065
+ );
3066
+ }
3067
+ const limit = options.maxResponseBytes ?? 1024 * 1024;
3068
+ if (Number(response.headers.get("content-length")) > limit) {
3069
+ void response.body?.cancel().catch(() => {
3070
+ });
3071
+ throw new MediaAPIError(
3072
+ "Media response exceeds size limit",
3073
+ "invalid_response"
3074
+ );
3075
+ }
3076
+ const reader = response.body?.getReader();
3077
+ if (!reader)
3078
+ throw new MediaAPIError("Media response is empty", "invalid_response");
3079
+ const cancel = () => {
3080
+ void reader.cancel().catch(() => {
3081
+ });
3082
+ };
3083
+ controller.signal.addEventListener("abort", cancel, { once: true });
3084
+ const chunks = [];
3085
+ let size = 0;
3086
+ try {
3087
+ while (true) {
3088
+ controller.signal.throwIfAborted();
3089
+ const { done, value } = await reader.read();
3090
+ if (done) break;
3091
+ size += value.byteLength;
3092
+ if (size > limit) {
3093
+ cancel();
3094
+ throw new MediaAPIError(
3095
+ "Media response exceeds size limit",
3096
+ "invalid_response"
3097
+ );
3098
+ }
3099
+ chunks.push(value);
3100
+ }
3101
+ } catch (error) {
3102
+ controller.signal.throwIfAborted();
3103
+ if (error instanceof MediaAPIError) throw error;
3104
+ throw new MediaAPIError(
3105
+ "Media response could not be read",
3106
+ "network_error"
3107
+ );
3108
+ } finally {
3109
+ controller.signal.removeEventListener("abort", cancel);
3110
+ reader.releaseLock();
3111
+ }
3112
+ controller.signal.throwIfAborted();
3113
+ return {
3114
+ text: Buffer.concat(chunks).toString("utf8"),
3115
+ status: response.status,
3116
+ requestId
3117
+ };
3118
+ };
3119
+ try {
3120
+ return await Promise.race([perform(), aborted]);
3121
+ } finally {
3122
+ clearTimeout(timer);
3123
+ options.signal?.removeEventListener("abort", relay);
3124
+ controller.signal.removeEventListener("abort", rejectAbort);
3125
+ }
3126
+ }
3127
+ function mediaJSON(text) {
3128
+ try {
3129
+ const data = JSON.parse(text);
3130
+ if (data && typeof data === "object" && !Array.isArray(data)) return data;
3131
+ } catch {
3132
+ }
3133
+ throw new MediaAPIError(
3134
+ "Media API returned invalid JSON",
3135
+ "invalid_response"
3136
+ );
3137
+ }
3138
+
3139
+ // src/providers/openai/image.ts
3140
+ var mimes = {
3141
+ png: "image/png",
3142
+ jpeg: "image/jpeg",
3143
+ webp: "image/webp"
3144
+ };
3145
+ function imageFormat(data) {
3146
+ if (Buffer.from(data.subarray(0, 8)).equals(
3147
+ Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])
3148
+ ))
3149
+ return "image/png";
3150
+ if (data[0] === 255 && data[1] === 216 && data[2] === 255)
3151
+ return "image/jpeg";
3152
+ if (Buffer.from(data.subarray(0, 4)).toString() === "RIFF" && Buffer.from(data.subarray(8, 12)).toString() === "WEBP")
3153
+ return "image/webp";
3154
+ }
3155
+ async function fileBlob(file, mask = false) {
3156
+ if (!file || !Object.values(mimes).includes(file.mediaType) || mask && file.mediaType !== "image/png")
3157
+ invalidMedia("Images must be PNG, JPEG or WebP; masks must be PNG");
3158
+ const size = file.data instanceof Blob ? file.data.size : file.data?.byteLength;
3159
+ if (!size || size >= 5e7)
3160
+ invalidMedia("Each input image must be smaller than 50 MB");
3161
+ const bytes = file.data instanceof Blob ? new Uint8Array(await file.data.arrayBuffer()) : file.data instanceof Uint8Array ? file.data : file.data instanceof ArrayBuffer ? new Uint8Array(file.data) : invalidMedia("Image data must be bytes or a Blob");
3162
+ if (imageFormat(bytes) !== file.mediaType)
3163
+ invalidMedia("Image bytes do not match the media type");
3164
+ return new Blob([new Uint8Array(bytes)], { type: file.mediaType });
3165
+ }
3166
+ function optionsFor(model, options) {
3167
+ const allowed = [
3168
+ "quality",
3169
+ "background",
3170
+ "outputFormat",
3171
+ "outputCompression",
3172
+ "moderation",
3173
+ "inputFidelity",
3174
+ "safetyIdentifier"
3175
+ ];
3176
+ for (const [provider, value] of Object.entries(
3177
+ options.providerOptions ?? {}
3178
+ )) {
3179
+ if (provider !== "openai" || !value || typeof value !== "object" || Object.keys(value).some((k) => !allowed.includes(k)))
3180
+ throw new MediaAPIError(
3181
+ "Unsupported image provider option",
3182
+ "unsupported_option"
3183
+ );
3184
+ }
3185
+ const o = options.providerOptions?.openai ?? {};
3186
+ for (const [key, values] of Object.entries({
3187
+ quality: ["auto", "low", "medium", "high", "xhigh", "max"],
3188
+ background: ["auto", "opaque", "transparent"],
3189
+ outputFormat: ["png", "jpeg", "webp"],
3190
+ moderation: ["auto", "low"],
3191
+ inputFidelity: ["high", "low"]
3192
+ })) {
3193
+ const value = o[key];
3194
+ if (value !== void 0 && !values.includes(value))
3195
+ invalidMedia(`Invalid image ${key}`);
3196
+ }
3197
+ if ((o.quality === "xhigh" || o.quality === "max") && !model.startsWith("gpt-image-2.5"))
3198
+ invalidMedia("xhigh/max quality requires a GPT Image 2.5 model");
3199
+ if (o.background === "transparent" && o.outputFormat === "jpeg")
3200
+ invalidMedia("JPEG does not support transparency");
3201
+ if (o.outputCompression !== void 0 && (!Number.isInteger(o.outputCompression) || o.outputCompression < 0 || o.outputCompression > 100 || !["jpeg", "webp"].includes(o.outputFormat ?? "png")))
3202
+ invalidMedia("Compression must be 0\u2013100 and requires JPEG or WebP");
3203
+ if (o.inputFidelity !== void 0 && !options.images)
3204
+ invalidMedia("inputFidelity requires image inputs");
3205
+ if (o.moderation !== void 0 && options.images)
3206
+ throw new MediaAPIError(
3207
+ "moderation is supported for generation only",
3208
+ "unsupported_option"
3209
+ );
3210
+ if (o.safetyIdentifier !== void 0 && (typeof o.safetyIdentifier !== "string" || !o.safetyIdentifier.length || o.safetyIdentifier.length > 128))
3211
+ invalidMedia("safetyIdentifier must contain 1\u2013128 characters");
3212
+ return o;
3213
+ }
3214
+ function openaiImage(modelId = "gpt-image-2.5-flare", config = {}) {
3215
+ if (!/^(gpt-image-|chatgpt-image-)/.test(modelId))
3216
+ invalidMedia(
3217
+ "Use a GPT Image model; retired DALL-E models are not supported"
3218
+ );
3219
+ return {
3220
+ provider: "openai",
3221
+ modelId,
3222
+ async doGenerate(options) {
3223
+ options.signal?.throwIfAborted();
3224
+ if (typeof options.prompt !== "string" || !options.prompt.trim() || options.prompt.length > 32e3)
3225
+ invalidMedia("Image prompt must contain 1\u201332000 characters");
3226
+ const n = options.n ?? 1;
3227
+ if (!Number.isInteger(n) || n < 1 || n > 10)
3228
+ invalidMedia("n must be an integer from 1 to 10");
3229
+ if (options.size !== void 0 && options.size !== "auto") {
3230
+ const match = /^(\d+)x(\d+)$/.exec(options.size);
3231
+ if (!match) invalidMedia("Image size must be auto or WIDTHxHEIGHT");
3232
+ const w = Number(match[1]), h = Number(match[2]);
3233
+ if (modelId.startsWith("gpt-image-2")) {
3234
+ if (w % 16 || h % 16 || w > 3840 || h > 3840 || w / h > 3 || h / w > 3 || w * h < 655360 || w * h > 8294400)
3235
+ invalidMedia(
3236
+ "Image size exceeds GPT Image dimensions or pixel limits"
3237
+ );
3238
+ } else if (!["1024x1024", "1536x1024", "1024x1536"].includes(options.size))
3239
+ invalidMedia("This model requires a standard GPT Image size");
3240
+ }
3241
+ if (options.images && (!Array.isArray(options.images) || !options.images.length || options.images.length > 16))
3242
+ invalidMedia("Editing requires 1\u201316 input images");
3243
+ if (options.mask && !options.images)
3244
+ invalidMedia("A mask requires image inputs");
3245
+ const o = optionsFor(modelId, options);
3246
+ const format = o.outputFormat ?? "png";
3247
+ const fields = {
3248
+ model: modelId,
3249
+ prompt: options.prompt,
3250
+ n,
3251
+ size: options.size,
3252
+ quality: o.quality,
3253
+ background: o.background,
3254
+ output_format: format,
3255
+ output_compression: o.outputCompression,
3256
+ moderation: o.moderation,
3257
+ input_fidelity: o.inputFidelity,
3258
+ safety_identifier: o.safetyIdentifier
3259
+ };
3260
+ let body = JSON.stringify(fields);
3261
+ if (options.images) {
3262
+ const form = new FormData();
3263
+ for (const [key, value] of Object.entries(fields))
3264
+ if (value !== void 0) form.set(key, String(value));
3265
+ let total = 0;
3266
+ for (const [i, input2] of options.images.entries()) {
3267
+ const blob = await fileBlob(input2);
3268
+ total += blob.size;
3269
+ if (total > 1e8)
3270
+ invalidMedia("Total image input exceeds the SDK's 100 MB limit");
3271
+ form.append(
3272
+ "image[]",
3273
+ blob,
3274
+ `image-${i}.${input2.mediaType.split("/")[1]}`
3275
+ );
3276
+ }
3277
+ if (options.mask) {
3278
+ const mask = await fileBlob(options.mask, true);
3279
+ total += mask.size;
3280
+ if (total > 1e8)
3281
+ invalidMedia("Total image input exceeds the SDK's 100 MB limit");
3282
+ form.set("mask", mask, "mask.png");
3283
+ }
3284
+ body = form;
3285
+ }
3286
+ const response = await postMedia(
3287
+ config,
3288
+ options.images ? "/images/edits" : "/images/generations",
3289
+ body,
3290
+ { ...options, maxResponseBytes: 128 * 1024 * 1024 }
3291
+ );
3292
+ const data = mediaJSON(response.text);
3293
+ if (!Array.isArray(data.data) || data.data.length !== n)
3294
+ throw new MediaAPIError(
3295
+ "Images API returned an unexpected image count",
3296
+ "invalid_response"
3297
+ );
3298
+ const images = data.data.map((item) => {
3299
+ const encoded = item?.b64_json;
3300
+ if (typeof encoded !== "string" || !encoded.length || encoded.length % 4 || !/^[A-Za-z0-9+/]+={0,2}$/.test(encoded))
3301
+ throw new MediaAPIError(
3302
+ "Images API returned invalid base64",
3303
+ "invalid_response"
3304
+ );
3305
+ const bytes = new Uint8Array(Buffer.from(encoded, "base64"));
3306
+ if (imageFormat(bytes) !== mimes[format])
3307
+ throw new MediaAPIError(
3308
+ "Images API returned an unexpected image format",
3309
+ "invalid_response"
3310
+ );
3311
+ return {
3312
+ uint8Array: bytes,
3313
+ get base64() {
3314
+ return Buffer.from(bytes).toString("base64");
3315
+ },
3316
+ mediaType: mimes[format],
3317
+ ...typeof item.revised_prompt === "string" ? { revisedPrompt: item.revised_prompt } : {}
3318
+ };
3319
+ });
3320
+ return {
3321
+ image: images[0],
3322
+ images,
3323
+ usage: data.usage,
3324
+ response: {
3325
+ provider: "openai",
3326
+ modelId,
3327
+ status: response.status,
3328
+ requestId: response.requestId
3329
+ },
3330
+ providerMetadata: {
3331
+ created: data.created,
3332
+ size: data.size,
3333
+ quality: data.quality,
3334
+ background: data.background
3335
+ }
3336
+ };
3337
+ }
3338
+ };
3339
+ }
3340
+
3341
+ // src/providers/openai/realtime.ts
3342
+ function buildSession(model, o) {
3343
+ if (o.instructions !== void 0 && (typeof o.instructions !== "string" || o.instructions.length > 1e5))
3344
+ invalidMedia(
3345
+ "Realtime instructions must be a string up to 100000 characters"
3346
+ );
3347
+ for (const value of [o.voice, o.transcriptionModel])
3348
+ if (value !== void 0 && (typeof value !== "string" || !value.trim() || value.length > 100))
3349
+ invalidMedia("Invalid realtime voice or transcription model");
3350
+ if (o.language !== void 0 && !/^[a-z]{2}$/.test(o.language))
3351
+ invalidMedia("Realtime language must be a two-letter code");
3352
+ if (o.language && !o.transcriptionModel)
3353
+ invalidMedia("language requires a transcription model");
3354
+ if (o.outputModality !== void 0 && !["audio", "text"].includes(o.outputModality))
3355
+ invalidMedia("Invalid realtime output modality");
3356
+ if (o.maxOutputTokens !== void 0 && o.maxOutputTokens !== "inf" && (!Number.isInteger(o.maxOutputTokens) || o.maxOutputTokens < 1 || o.maxOutputTokens > 4096))
3357
+ invalidMedia("maxOutputTokens must be 1\u20134096 or inf");
3358
+ if (o.safetyIdentifier !== void 0 && (typeof o.safetyIdentifier !== "string" || !/^[\x21-\x7e]{1,128}$/.test(o.safetyIdentifier)))
3359
+ invalidMedia(
3360
+ "safetyIdentifier must contain 1\u2013128 printable ASCII characters"
3361
+ );
3362
+ const names = /* @__PURE__ */ new Set();
3363
+ if (o.tools && (!Array.isArray(o.tools) || o.tools.length > 128))
3364
+ invalidMedia("Realtime supports up to 128 tools");
3365
+ const tools = o.tools?.map((tool) => {
3366
+ if (!tool || !/^[a-zA-Z0-9_-]{1,64}$/.test(tool.name) || names.has(tool.name) || !tool.parameters || tool.parameters.type !== "object")
3367
+ invalidMedia(
3368
+ "Realtime tools require unique names and object parameter schemas"
3369
+ );
3370
+ names.add(tool.name);
3371
+ return {
3372
+ type: "function",
3373
+ name: tool.name,
3374
+ description: tool.description,
3375
+ parameters: tool.parameters
3376
+ };
3377
+ });
3378
+ let turn;
3379
+ const td = o.turnDetection;
3380
+ if (td === null) turn = null;
3381
+ else if (td?.type === "semantic_vad") {
3382
+ if (td.eagerness !== void 0 && !["auto", "low", "medium", "high"].includes(td.eagerness))
3383
+ invalidMedia("Invalid semantic VAD eagerness");
3384
+ turn = {
3385
+ type: td.type,
3386
+ eagerness: td.eagerness,
3387
+ create_response: true,
3388
+ interrupt_response: true
3389
+ };
3390
+ } else {
3391
+ if (td && td.type !== "server_vad")
3392
+ invalidMedia("Invalid realtime turn detection");
3393
+ if (td?.threshold !== void 0 && (!Number.isFinite(td.threshold) || td.threshold < 0 || td.threshold > 1))
3394
+ invalidMedia("VAD threshold must be 0\u20131");
3395
+ for (const value of [td?.silenceDurationMs, td?.prefixPaddingMs])
3396
+ if (value !== void 0 && (!Number.isInteger(value) || value < 0 || value > 1e4))
3397
+ invalidMedia("VAD durations must be 0\u201310000 milliseconds");
3398
+ turn = {
3399
+ type: "server_vad",
3400
+ threshold: td?.threshold,
3401
+ silence_duration_ms: td?.silenceDurationMs,
3402
+ prefix_padding_ms: td?.prefixPaddingMs,
3403
+ create_response: true,
3404
+ interrupt_response: true
3405
+ };
3406
+ }
3407
+ return {
3408
+ type: "realtime",
3409
+ model,
3410
+ instructions: o.instructions,
3411
+ output_modalities: [o.outputModality ?? "audio"],
3412
+ max_output_tokens: o.maxOutputTokens,
3413
+ audio: {
3414
+ input: {
3415
+ turn_detection: turn,
3416
+ ...o.transcriptionModel ? {
3417
+ transcription: {
3418
+ model: o.transcriptionModel,
3419
+ language: o.language
3420
+ }
3421
+ } : {}
3422
+ },
3423
+ output: { voice: o.voice ?? "marin" }
3424
+ },
3425
+ tools
3426
+ };
3427
+ }
3428
+ function openaiRealtime(modelId = "gpt-realtime-2.1", config = {}) {
3429
+ if (!modelId.startsWith("gpt-realtime"))
3430
+ invalidMedia("openaiRealtime requires a gpt-realtime model");
3431
+ return {
3432
+ provider: "openai",
3433
+ modelId,
3434
+ async createClientSecret(options = {}) {
3435
+ const ttl = options.expiresAfterSeconds ?? 60;
3436
+ if (!Number.isInteger(ttl) || ttl < 10 || ttl > 7200)
3437
+ invalidMedia("Client secret lifetime must be 10\u20137200 seconds");
3438
+ const session = buildSession(modelId, options);
3439
+ const response = await postMedia(
3440
+ config,
3441
+ "/realtime/client_secrets",
3442
+ JSON.stringify({
3443
+ session,
3444
+ expires_after: { anchor: "created_at", seconds: ttl }
3445
+ }),
3446
+ { ...options, timeoutMs: options.timeoutMs ?? 3e4 }
3447
+ );
3448
+ const data = mediaJSON(response.text);
3449
+ if (typeof data.value !== "string" || !data.value.startsWith("ek_") || !Number.isFinite(data.expires_at) || data.expires_at <= Date.now() / 1e3)
3450
+ throw new MediaAPIError(
3451
+ "Realtime returned an invalid or expired client secret",
3452
+ "invalid_response"
3453
+ );
3454
+ return { value: data.value, expiresAt: data.expires_at, modelId };
3455
+ },
3456
+ async createCall(options) {
3457
+ if (typeof options.sdp !== "string" || !options.sdp.startsWith("v=0") || options.sdp.length > 65536)
3458
+ invalidMedia("A valid SDP offer up to 64 KiB is required");
3459
+ const form = new FormData();
3460
+ form.set("sdp", options.sdp);
3461
+ form.set("session", JSON.stringify(buildSession(modelId, options)));
3462
+ const response = await postMedia(config, "/realtime/calls", form, {
3463
+ ...options,
3464
+ timeoutMs: options.timeoutMs ?? 3e4,
3465
+ maxResponseBytes: 65536
3466
+ });
3467
+ if (!response.text.startsWith("v=0"))
3468
+ throw new MediaAPIError(
3469
+ "Realtime returned an invalid SDP answer",
3470
+ "invalid_response"
3471
+ );
3472
+ return { sdp: response.text };
3473
+ }
3474
+ };
3475
+ }
3476
+
2118
3477
  // src/providers/openai/index.ts
2119
3478
  var OPENAI_MODELS2 = {
2120
3479
  // GPT-4o series
@@ -2230,6 +3589,10 @@ function createOpenAI(config = {}) {
2230
3589
  apiKey,
2231
3590
  model: modelId,
2232
3591
  baseUrl: config.baseUrl,
3592
+ organization: config.organization,
3593
+ project: config.project,
3594
+ timeout: config.timeout,
3595
+ headers: config.headers,
2233
3596
  reasoningEffort: config.reasoningEffort,
2234
3597
  api: config.api,
2235
3598
  responses: config.responses,
@@ -2266,6 +3629,9 @@ exports.createOpenAI = createOpenAI;
2266
3629
  exports.createOpenAIModel = openai;
2267
3630
  exports.createOpenAIProvider = createOpenAIProvider;
2268
3631
  exports.openai = openai;
3632
+ exports.openaiDecision = openaiDecision;
3633
+ exports.openaiImage = openaiImage;
3634
+ exports.openaiRealtime = openaiRealtime;
2269
3635
  exports.openaiSpeech = openaiSpeech;
2270
3636
  exports.openaiTranscription = openaiTranscription;
2271
3637
  //# sourceMappingURL=index.js.map