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