@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",
@@ -111,6 +113,19 @@ function capabilities(model) {
111
113
  const supportsNone = major === 5 && minor >= 1 && !chat || /^gpt-6-(?:sol|luna)(?:-\d{4}-\d{2}-\d{2})?$/i.test(model);
112
114
  return { major, minor, reasoning, supportsNone };
113
115
  }
116
+ function filterOpenAIResponseInclude(model, include, options = {}) {
117
+ const c = capabilities(model);
118
+ let result = include;
119
+ if (options.parameterPolicy !== "passthrough" && c.reasoning && !(options.reasoningEffort === "none" && c.supportsNone) && include.includes("message.output_text.logprobs")) {
120
+ result = include.filter(
121
+ (value) => value !== "message.output_text.logprobs"
122
+ );
123
+ (options.onWarning ?? console.warn)(
124
+ `[llm-sdk] ${model}: omitted message.output_text.logprobs because reasoning is enabled.`
125
+ );
126
+ }
127
+ return result.length ? result : void 0;
128
+ }
114
129
  function requiresOpenAIResponses(model, baseUrl, options = {}) {
115
130
  if (options.parameterPolicy === "passthrough") return false;
116
131
  const c = capabilities(model);
@@ -272,6 +287,7 @@ function toOpenAIResponseFormat(rf) {
272
287
  };
273
288
  }
274
289
  function toOpenAIResponsesTextFormat(rf) {
290
+ if (rf?.type === "json_object") return { type: "json_object" };
275
291
  if (!rf || rf.type !== "json_schema") return void 0;
276
292
  return {
277
293
  type: "json_schema",
@@ -387,6 +403,30 @@ var DEFAULT_MODEL_CONFIG = {
387
403
  maxTokens: 128e3
388
404
  };
389
405
  function openrouter(modelId, options = {}) {
406
+ function reasoning(params) {
407
+ const configured = options.reasoning;
408
+ if (!configured && params.reasoningEffort === void 0) return {};
409
+ const effort = params.reasoningEffort ?? configured?.effort;
410
+ const budget = configured?.maxTokens;
411
+ if (effort !== void 0 && budget !== void 0) {
412
+ throw new Error(
413
+ "OpenRouter reasoning accepts effort or maxTokens, not both"
414
+ );
415
+ }
416
+ if (budget !== void 0 && (!Number.isSafeInteger(budget) || budget <= 0)) {
417
+ throw new Error(
418
+ "OpenRouter reasoning maxTokens must be a positive integer"
419
+ );
420
+ }
421
+ return {
422
+ reasoning: {
423
+ ...effort !== void 0 ? { effort } : {},
424
+ ...budget !== void 0 ? { max_tokens: budget } : {},
425
+ ...configured?.enabled !== void 0 ? { enabled: configured.enabled } : {},
426
+ ...configured?.exclude !== void 0 ? { exclude: configured.exclude } : {}
427
+ }
428
+ };
429
+ }
390
430
  const apiKey = options.apiKey ?? process.env.OPENROUTER_API_KEY;
391
431
  const baseURL = options.baseURL ?? "https://openrouter.ai/api/v1";
392
432
  const headers = {};
@@ -417,7 +457,7 @@ function openrouter(modelId, options = {}) {
417
457
  supportsTools: modelConfig.tools,
418
458
  supportsStreaming: true,
419
459
  supportsJsonMode: modelConfig.jsonMode,
420
- supportsThinking: false,
460
+ supportsThinking: options.reasoning !== void 0,
421
461
  supportsPDF: false,
422
462
  maxTokens: modelConfig.maxTokens,
423
463
  supportedImageTypes: modelConfig.vision ? ["image/png", "image/jpeg", "image/gif", "image/webp"] : []
@@ -429,7 +469,8 @@ function openrouter(modelId, options = {}) {
429
469
  model: modelId,
430
470
  messages,
431
471
  temperature: params.temperature,
432
- max_tokens: params.maxTokens
472
+ max_tokens: params.maxTokens,
473
+ ...reasoning(params)
433
474
  };
434
475
  if (params.tools) {
435
476
  requestBody.tools = params.tools;
@@ -441,7 +482,9 @@ function openrouter(modelId, options = {}) {
441
482
  if (responseFormat) {
442
483
  requestBody.response_format = responseFormat;
443
484
  }
444
- const response = await client2.chat.completions.create(requestBody);
485
+ const response = await client2.chat.completions.create(requestBody, {
486
+ signal: params.signal
487
+ });
445
488
  const choice = response.choices[0];
446
489
  const message = choice.message;
447
490
  const toolCalls = (message.tool_calls ?? []).map(
@@ -453,6 +496,8 @@ function openrouter(modelId, options = {}) {
453
496
  );
454
497
  return {
455
498
  text: message.content ?? "",
499
+ ...reasoningTextFor(message) ? { reasoningText: reasoningTextFor(message) } : {},
500
+ ...reasoningMetadata(message.reasoning_details),
456
501
  toolCalls,
457
502
  finishReason: mapFinishReason(choice.finish_reason),
458
503
  usage: {
@@ -471,7 +516,9 @@ function openrouter(modelId, options = {}) {
471
516
  messages,
472
517
  temperature: params.temperature,
473
518
  max_tokens: params.maxTokens,
474
- stream: true
519
+ ...reasoning(params),
520
+ stream: true,
521
+ stream_options: { include_usage: true }
475
522
  };
476
523
  if (params.tools) {
477
524
  requestBody.tools = params.tools;
@@ -483,70 +530,61 @@ function openrouter(modelId, options = {}) {
483
530
  if (responseFormat) {
484
531
  requestBody.response_format = responseFormat;
485
532
  }
486
- const stream = await client2.chat.completions.create(requestBody);
487
- let currentToolCall = null;
488
- let totalPromptTokens = 0;
489
- let totalCompletionTokens = 0;
533
+ const stream = await client2.chat.completions.create(requestBody, {
534
+ signal: params.signal
535
+ });
536
+ const calls = /* @__PURE__ */ new Map();
537
+ const details = [];
538
+ let finishReason = "unknown";
539
+ let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
490
540
  for await (const chunk of stream) {
491
- if (params.signal?.aborted) {
492
- yield { type: "error", error: new Error("Aborted") };
493
- return;
494
- }
495
- const choice = chunk.choices[0];
541
+ if (params.signal?.aborted)
542
+ throw params.signal.reason ?? new Error("Aborted");
543
+ const choice = chunk.choices?.[0];
496
544
  const delta = choice?.delta;
497
- if (delta?.content) {
498
- yield { type: "text-delta", text: delta.content };
545
+ if (chunk.usage)
546
+ usage = {
547
+ promptTokens: chunk.usage.prompt_tokens ?? 0,
548
+ completionTokens: chunk.usage.completion_tokens ?? 0,
549
+ totalTokens: chunk.usage.total_tokens ?? (chunk.usage.prompt_tokens ?? 0) + (chunk.usage.completion_tokens ?? 0)
550
+ };
551
+ const reasoningText = reasoningTextFor(delta);
552
+ if (typeof reasoningText === "string" && reasoningText) {
553
+ yield { type: "reasoning-delta", text: reasoningText };
499
554
  }
500
- if (delta?.tool_calls) {
501
- for (const tc of delta.tool_calls) {
502
- if (tc.id) {
503
- if (currentToolCall) {
504
- yield {
505
- type: "tool-call",
506
- toolCall: {
507
- id: currentToolCall.id,
508
- name: currentToolCall.name,
509
- args: JSON.parse(currentToolCall.arguments || "{}")
510
- }
511
- };
512
- }
513
- currentToolCall = {
514
- id: tc.id,
515
- name: tc.function?.name ?? "",
516
- arguments: tc.function?.arguments ?? ""
517
- };
518
- } else if (currentToolCall && tc.function?.arguments) {
519
- currentToolCall.arguments += tc.function.arguments;
520
- }
521
- }
555
+ for (const detail of delta?.reasoning_details ?? []) {
556
+ const last = details.at(-1);
557
+ const field = detail.type === "reasoning.text" ? "text" : detail.type === "reasoning.summary" ? "summary" : void 0;
558
+ if (field && last && last.type === detail.type && (!detail.id || !last.id || detail.id === last.id)) {
559
+ last[field] = (last[field] ?? "") + (detail[field] ?? "");
560
+ last.signature ?? (last.signature = detail.signature);
561
+ last.format ?? (last.format = detail.format);
562
+ } else details.push({ ...detail });
522
563
  }
523
- if (choice?.finish_reason) {
524
- if (currentToolCall) {
525
- yield {
526
- type: "tool-call",
527
- toolCall: {
528
- id: currentToolCall.id,
529
- name: currentToolCall.name,
530
- args: JSON.parse(currentToolCall.arguments || "{}")
531
- }
532
- };
533
- currentToolCall = null;
534
- }
535
- if (chunk.usage) {
536
- totalPromptTokens = chunk.usage.prompt_tokens;
537
- totalCompletionTokens = chunk.usage.completion_tokens;
538
- }
539
- yield {
540
- type: "finish",
541
- finishReason: mapFinishReason(choice.finish_reason),
542
- usage: {
543
- promptTokens: totalPromptTokens,
544
- completionTokens: totalCompletionTokens,
545
- totalTokens: totalPromptTokens + totalCompletionTokens
546
- }
547
- };
564
+ if (delta?.content) yield { type: "text-delta", text: delta.content };
565
+ for (const tc of delta?.tool_calls ?? []) {
566
+ const index = tc.index ?? 0;
567
+ const call = calls.get(index) ?? { id: "", name: "", arguments: "" };
568
+ if (tc.id) call.id = tc.id;
569
+ if (tc.function?.name) call.name = tc.function.name;
570
+ if (tc.function?.arguments) call.arguments += tc.function.arguments;
571
+ calls.set(index, call);
548
572
  }
573
+ if (choice?.finish_reason)
574
+ finishReason = mapFinishReason(choice.finish_reason);
549
575
  }
576
+ const parsed = [...calls.values()].map((call) => ({
577
+ id: call.id,
578
+ name: call.name,
579
+ args: JSON.parse(call.arguments || "{}")
580
+ }));
581
+ for (const toolCall of parsed) yield { type: "tool-call", toolCall };
582
+ yield {
583
+ type: "finish",
584
+ finishReason,
585
+ usage,
586
+ ...reasoningMetadata(details)
587
+ };
550
588
  }
551
589
  };
552
590
  }
@@ -582,7 +620,7 @@ function formatMessagesForOpenRouter(messages) {
582
620
  }
583
621
  if (part.type === "image") {
584
622
  const imageData = typeof part.image === "string" ? part.image : Buffer.from(part.image).toString("base64");
585
- const url = imageData.startsWith("data:") ? imageData : `data:${part.mimeType ?? "image/png"};base64,${imageData}`;
623
+ const url = /^(data:|https?:\/\/)/.test(imageData) ? imageData : `data:${part.mimeType ?? "image/png"};base64,${imageData}`;
586
624
  return { type: "image_url", image_url: { url, detail: "auto" } };
587
625
  }
588
626
  return { type: "text", text: "" };
@@ -593,6 +631,9 @@ function formatMessagesForOpenRouter(messages) {
593
631
  role: "assistant",
594
632
  content: msg.content
595
633
  };
634
+ const details = msg.providerMetadata?.openrouter?.reasoningDetails;
635
+ if (Array.isArray(details) && details.length)
636
+ assistantMsg.reasoning_details = details;
596
637
  if (msg.toolCalls && msg.toolCalls.length > 0) {
597
638
  assistantMsg.tool_calls = msg.toolCalls.map((tc) => ({
598
639
  id: tc.id,
@@ -638,6 +679,92 @@ async function searchOpenRouterModels(query, apiKey) {
638
679
  (model) => model.id.toLowerCase().includes(lowerQuery) || model.name.toLowerCase().includes(lowerQuery)
639
680
  );
640
681
  }
682
+ function reasoningMetadata(details) {
683
+ if (!Array.isArray(details) || !details.length) return {};
684
+ const fields = [
685
+ "type",
686
+ "text",
687
+ "summary",
688
+ "data",
689
+ "signature",
690
+ "id",
691
+ "format",
692
+ "index"
693
+ ];
694
+ const reasoningDetails = details.filter((detail) => detail && typeof detail === "object").map(
695
+ (detail) => Object.fromEntries(
696
+ fields.filter((key) => detail[key] !== void 0).map((key) => [key, structuredClone(detail[key])])
697
+ )
698
+ );
699
+ return { providerMetadata: { openrouter: { reasoningDetails } } };
700
+ }
701
+ function reasoningTextFor(message) {
702
+ const text = message?.reasoning ?? message?.reasoning_content;
703
+ if (typeof text === "string" && text) return text;
704
+ if (!Array.isArray(message?.reasoning_details)) return void 0;
705
+ const parts = message.reasoning_details.flatMap((detail) => {
706
+ const value = detail?.type === "reasoning.text" ? detail.text : detail?.type === "reasoning.summary" ? detail.summary : void 0;
707
+ return typeof value === "string" ? [value] : [];
708
+ });
709
+ return parts.length ? parts.join("") : void 0;
710
+ }
711
+
712
+ // src/core/provider-error.ts
713
+ function invalidProviderResponse(message) {
714
+ return Object.assign(new Error(message), {
715
+ name: "InvalidProviderResponseError",
716
+ status: 502,
717
+ code: "INVALID_PROVIDER_RESPONSE"
718
+ });
719
+ }
720
+ function providerErrorEvent(error, code) {
721
+ const details = error ?? {};
722
+ const event = {
723
+ type: "error",
724
+ message: error instanceof Error ? error.message : String(error),
725
+ code,
726
+ ...typeof details.status === "number" ? { status: details.status } : {},
727
+ ...typeof details.code === "string" ? { providerCode: details.code } : {},
728
+ ...typeof details.param === "string" ? { param: details.param } : {},
729
+ ...typeof details.request_id === "string" ? { requestId: details.request_id } : {}
730
+ };
731
+ Object.defineProperty(event, "cause", { value: error, enumerable: false });
732
+ return event;
733
+ }
734
+ function throwIfAborted(signal) {
735
+ if (!signal?.aborted) return;
736
+ const error = new Error("The operation was aborted");
737
+ error.name = "AbortError";
738
+ throw error;
739
+ }
740
+
741
+ // src/core/tool-validation.ts
742
+ function parseToolArguments(value) {
743
+ let args = value;
744
+ if (typeof value === "string") {
745
+ try {
746
+ args = JSON.parse(value || "{}");
747
+ } catch {
748
+ throw invalidProviderResponse("Tool arguments are not valid JSON");
749
+ }
750
+ }
751
+ if (args === null || typeof args !== "object" || Array.isArray(args))
752
+ throw invalidProviderResponse("Tool arguments must be a JSON object");
753
+ return args;
754
+ }
755
+ function validateToolCalls(calls) {
756
+ const ids = /* @__PURE__ */ new Set();
757
+ for (const call of calls) {
758
+ if (!call || typeof call.id !== "string" || !call.id || typeof call.name !== "string" || !call.name || ids.has(call.id))
759
+ throw invalidProviderResponse(
760
+ "Tool calls have missing or duplicate identities"
761
+ );
762
+ ids.add(call.id);
763
+ if (typeof call.args === "string")
764
+ throw invalidProviderResponse("Parsed tool arguments must be an object");
765
+ parseToolArguments(call.args);
766
+ }
767
+ }
641
768
 
642
769
  // src/core/openai-responses.ts
643
770
  function resolveOpenAITransport(defaults, overrides) {
@@ -686,6 +813,54 @@ function responseMetadata(response) {
686
813
  }
687
814
  };
688
815
  }
816
+ function responseReplayMetadata(metadata) {
817
+ const response = metadata?.openai?.response;
818
+ if (!response) return metadata;
819
+ const output = response.output.map((item) => {
820
+ const fields = ["type", "id", "status"];
821
+ switch (item.type) {
822
+ case "reasoning":
823
+ fields.push("encrypted_content");
824
+ break;
825
+ case "message":
826
+ fields.push("role", "phase");
827
+ break;
828
+ case "function_call":
829
+ fields.push("call_id", "name", "namespace", "arguments");
830
+ break;
831
+ case "tool_search_call":
832
+ fields.push("call_id", "execution", "arguments");
833
+ break;
834
+ case "tool_search_output":
835
+ fields.push("call_id", "tools");
836
+ break;
837
+ case "web_search_call":
838
+ fields.push("action");
839
+ break;
840
+ default:
841
+ return stripItemDiagnostics(item);
842
+ }
843
+ const replay = Object.fromEntries(
844
+ fields.filter((key) => item[key] !== void 0).map((key) => [key, item[key]])
845
+ );
846
+ if (item.type === "reasoning") replay.summary = [];
847
+ if (item.type === "message")
848
+ replay.content = Array.isArray(item.content) ? item.content.flatMap(
849
+ (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 }] : []
850
+ ) : [];
851
+ return stripItemDiagnostics(replay);
852
+ });
853
+ return { openai: { response: { output, refusals: [] } } };
854
+ }
855
+ function stripItemDiagnostics(item) {
856
+ const {
857
+ usage: _usage,
858
+ logprobs: _logprobs,
859
+ metadata: _metadata,
860
+ ...state
861
+ } = item;
862
+ return state;
863
+ }
689
864
  function getResponseReplay(metadata) {
690
865
  const response = metadata?.openai?.response;
691
866
  if (response === void 0) return void 0;
@@ -694,29 +869,7 @@ function getResponseReplay(metadata) {
694
869
  "Invalid OpenAI Responses replay metadata: output must contain typed items"
695
870
  );
696
871
  }
697
- return response.output;
698
- }
699
-
700
- // src/core/provider-error.ts
701
- function providerErrorEvent(error, code) {
702
- const details = error ?? {};
703
- const event = {
704
- type: "error",
705
- message: error instanceof Error ? error.message : String(error),
706
- code,
707
- ...typeof details.status === "number" ? { status: details.status } : {},
708
- ...typeof details.code === "string" ? { providerCode: details.code } : {},
709
- ...typeof details.param === "string" ? { param: details.param } : {},
710
- ...typeof details.request_id === "string" ? { requestId: details.request_id } : {}
711
- };
712
- Object.defineProperty(event, "cause", { value: error, enumerable: false });
713
- return event;
714
- }
715
- function throwIfAborted(signal) {
716
- if (!signal?.aborted) return;
717
- const error = new Error("The operation was aborted");
718
- error.name = "AbortError";
719
- throw error;
872
+ return responseReplayMetadata(metadata)?.openai?.response?.output;
720
873
  }
721
874
 
722
875
  // src/core/utils.ts
@@ -726,9 +879,6 @@ function generateId(prefix = "id") {
726
879
  function generateMessageId() {
727
880
  return generateId("msg");
728
881
  }
729
- function generateToolCallId() {
730
- return generateId("call");
731
- }
732
882
 
733
883
  // src/adapters/openai.ts
734
884
  var OpenAIAdapter = class _OpenAIAdapter {
@@ -752,6 +902,8 @@ var OpenAIAdapter = class _OpenAIAdapter {
752
902
  apiKey: this.config.apiKey,
753
903
  baseURL: this.config.baseUrl,
754
904
  organization: this.config.organization,
905
+ project: this.config.project,
906
+ timeout: this.config.timeout,
755
907
  defaultHeaders: this.config.headers
756
908
  });
757
909
  }
@@ -816,11 +968,11 @@ var OpenAIAdapter = class _OpenAIAdapter {
816
968
  }
817
969
  buildResponsesInput(request) {
818
970
  const sourceMessages = request.rawMessages && request.rawMessages.length > 0 ? this.formatRawMessages(request.rawMessages, true) : formatMessagesForOpenAI(request.messages, void 0, true);
819
- const input = [];
971
+ const input2 = [];
820
972
  for (const message of sourceMessages) {
821
973
  if (message.role === "system") {
822
974
  if (message.content && message.content !== request.systemPrompt) {
823
- input.push({
975
+ input2.push({
824
976
  type: "message",
825
977
  role: "developer",
826
978
  content: this.toResponsesContent(message.content, "developer")
@@ -831,12 +983,12 @@ var OpenAIAdapter = class _OpenAIAdapter {
831
983
  if (message.role === "assistant") {
832
984
  const replay = getResponseReplay(message.providerMetadata);
833
985
  if (replay) {
834
- input.push(...replay);
986
+ input2.push(...replay);
835
987
  continue;
836
988
  }
837
989
  const content = this.toResponsesContent(message.content, "assistant");
838
990
  if (typeof content === "string" ? content : Array.isArray(content) && content.length > 0) {
839
- input.push({
991
+ input2.push({
840
992
  type: "message",
841
993
  role: "assistant",
842
994
  content
@@ -844,7 +996,7 @@ var OpenAIAdapter = class _OpenAIAdapter {
844
996
  }
845
997
  const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
846
998
  for (const toolCall of toolCalls) {
847
- input.push({
999
+ input2.push({
848
1000
  type: "function_call",
849
1001
  call_id: toolCall.id,
850
1002
  name: toolCall.function?.name,
@@ -854,7 +1006,7 @@ var OpenAIAdapter = class _OpenAIAdapter {
854
1006
  continue;
855
1007
  }
856
1008
  if (message.role === "tool") {
857
- input.push({
1009
+ input2.push({
858
1010
  type: "function_call_output",
859
1011
  call_id: message.tool_call_id,
860
1012
  output: message.modelOutput ? toolOutputForProvider(message.modelOutput, "responses") : typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? null)
@@ -862,19 +1014,19 @@ var OpenAIAdapter = class _OpenAIAdapter {
862
1014
  continue;
863
1015
  }
864
1016
  const role = message.role === "developer" ? "developer" : "user";
865
- input.push({
1017
+ input2.push({
866
1018
  type: "message",
867
1019
  role,
868
1020
  content: this.toResponsesContent(message.content, role)
869
1021
  });
870
1022
  }
871
- return input;
1023
+ return input2;
872
1024
  }
873
1025
  /** Preserve Copilot screenshot attachments on every OpenAI transport. */
874
1026
  formatRawMessages(messages, preserveProviderMetadata = false) {
875
1027
  return messages.map((message) => {
876
1028
  const { attachments, providerMetadata, modelOutput, ...rest } = message;
877
- let content = message.role === "tool" && !preserveProviderMetadata ? textToolOutput(modelOutput, String(message.content ?? "")) : message.content;
1029
+ let content = message.role === "tool" && !preserveProviderMetadata && modelOutput !== void 0 ? textToolOutput(modelOutput, "") : message.content;
878
1030
  if (preserveProviderMetadata && modelOutput)
879
1031
  rest.modelOutput = modelOutput;
880
1032
  if (message.role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.length > 0 && content === "") {
@@ -977,17 +1129,32 @@ var OpenAIAdapter = class _OpenAIAdapter {
977
1129
  const seenCallIds = /* @__PURE__ */ new Set();
978
1130
  const toolCalls = Array.isArray(response?.output) ? response.output.filter((item) => item?.type === "function_call").map((item) => {
979
1131
  if (typeof item.call_id !== "string" || !item.call_id || typeof item.name !== "string" || !item.name || typeof item.arguments !== "string" || item.status && item.status !== "completed") {
980
- throw new Error("Responses contains an incomplete function call");
1132
+ throw invalidProviderResponse(
1133
+ "Responses contains an incomplete function call"
1134
+ );
981
1135
  }
982
1136
  if (seenCallIds.has(item.call_id))
983
- throw new Error("Responses contains duplicate function call IDs");
1137
+ throw invalidProviderResponse(
1138
+ "Responses contains duplicate function call IDs"
1139
+ );
984
1140
  seenCallIds.add(item.call_id);
985
- const args = JSON.parse(item.arguments);
1141
+ let args;
1142
+ try {
1143
+ args = JSON.parse(item.arguments);
1144
+ } catch {
1145
+ throw invalidProviderResponse(
1146
+ "Responses function arguments must be valid JSON"
1147
+ );
1148
+ }
986
1149
  if (!args || typeof args !== "object" || Array.isArray(args))
987
- throw new Error(
1150
+ throw invalidProviderResponse(
988
1151
  "Responses function arguments must be a JSON object"
989
1152
  );
990
- return { id: item.call_id, name: item.name, args };
1153
+ return {
1154
+ id: item.call_id,
1155
+ name: item.name,
1156
+ args
1157
+ };
991
1158
  }) : [];
992
1159
  return {
993
1160
  content,
@@ -1018,6 +1185,16 @@ var OpenAIAdapter = class _OpenAIAdapter {
1018
1185
  request.config?.responseFormat
1019
1186
  );
1020
1187
  const modelIdForPayload = request.config?.model || this.model;
1188
+ const include = filterOpenAIResponseInclude(
1189
+ modelIdForPayload,
1190
+ [
1191
+ .../* @__PURE__ */ new Set([
1192
+ ...options?.include ?? [],
1193
+ ...options?.store === false ? ["reasoning.encrypted_content"] : []
1194
+ ])
1195
+ ],
1196
+ this.parameterOptions(request)
1197
+ );
1021
1198
  const payload = {
1022
1199
  model: modelIdForPayload,
1023
1200
  instructions: request.systemPrompt,
@@ -1041,14 +1218,7 @@ var OpenAIAdapter = class _OpenAIAdapter {
1041
1218
  ...options?.promptCacheKey !== void 0 ? { prompt_cache_key: options.promptCacheKey } : {},
1042
1219
  ...options?.promptCacheOptions !== void 0 ? { prompt_cache_options: options.promptCacheOptions } : {},
1043
1220
  ...options?.promptCacheRetention !== void 0 ? { prompt_cache_retention: options.promptCacheRetention } : {},
1044
- ...options?.store === false || options?.include ? {
1045
- include: [
1046
- .../* @__PURE__ */ new Set([
1047
- ...options?.include ?? [],
1048
- ...options?.store === false ? ["reasoning.encrypted_content"] : []
1049
- ])
1050
- ]
1051
- } : {},
1221
+ ...include ? { include } : {},
1052
1222
  ...responsesTextFormat ? { text: { format: responsesTextFormat } } : {},
1053
1223
  stream
1054
1224
  };
@@ -1230,7 +1400,9 @@ var OpenAIAdapter = class _OpenAIAdapter {
1230
1400
  const uniqueCalls = /* @__PURE__ */ new Map();
1231
1401
  for (const call of toolCalls.values()) {
1232
1402
  if (!call.id || !call.name)
1233
- throw new Error("Responses stream contains an incomplete tool call");
1403
+ throw invalidProviderResponse(
1404
+ "Responses stream contains an incomplete tool call"
1405
+ );
1234
1406
  uniqueCalls.set(call.id, call);
1235
1407
  }
1236
1408
  toolCalls.clear();
@@ -1417,15 +1589,20 @@ var OpenAIAdapter = class _OpenAIAdapter {
1417
1589
  }
1418
1590
  throwIfAborted(request.signal);
1419
1591
  if (!finishReason)
1420
- throw new Error("OpenAI stream ended without a finish reason");
1592
+ throw invalidProviderResponse(
1593
+ "OpenAI stream ended without a finish reason"
1594
+ );
1421
1595
  if (toolCalls.size && !["tool_calls", "function_call"].includes(finishReason)) {
1422
- throw new Error("OpenAI stream ended with incomplete tool calls");
1423
- }
1424
- for (const call of toolCalls.values()) {
1425
- if (!call.id || !call.name)
1426
- throw new Error("OpenAI stream contains an incomplete tool call");
1427
- JSON.parse(call.arguments || "{}");
1596
+ throw invalidProviderResponse(
1597
+ "OpenAI stream ended with incomplete tool calls"
1598
+ );
1428
1599
  }
1600
+ validateToolCalls(
1601
+ [...toolCalls.values()].map((call) => ({
1602
+ ...call,
1603
+ args: parseToolArguments(call.arguments)
1604
+ }))
1605
+ );
1429
1606
  for (const call of toolCalls.values()) {
1430
1607
  yield {
1431
1608
  type: "action:start",
@@ -1509,20 +1686,20 @@ var OpenAIAdapter = class _OpenAIAdapter {
1509
1686
  logProviderPayload("openai", "response payload", response, request.debug);
1510
1687
  const choice = response.choices?.[0];
1511
1688
  const message = choice?.message;
1689
+ const toolCalls = (message?.tool_calls ?? []).map((toolCall) => ({
1690
+ id: toolCall.id,
1691
+ name: toolCall.function?.name,
1692
+ args: parseToolArguments(toolCall.function?.arguments ?? "{}"),
1693
+ ...toolCall.extra_content ? { extra_content: toolCall.extra_content } : {}
1694
+ }));
1695
+ validateToolCalls(toolCalls);
1696
+ if (toolCalls.length && choice?.finish_reason && !["tool_calls", "function_call"].includes(choice.finish_reason))
1697
+ throw invalidProviderResponse(
1698
+ "OpenAI response ended with incomplete tool calls"
1699
+ );
1512
1700
  return {
1513
1701
  content: message?.content ?? "",
1514
- toolCalls: message?.tool_calls?.map((toolCall) => ({
1515
- id: toolCall.id ?? generateToolCallId(),
1516
- name: toolCall.function?.name ?? "",
1517
- args: (() => {
1518
- try {
1519
- return JSON.parse(toolCall.function?.arguments ?? "{}");
1520
- } catch {
1521
- return {};
1522
- }
1523
- })(),
1524
- ...toolCall.extra_content ? { extra_content: toolCall.extra_content } : {}
1525
- })) ?? [],
1702
+ toolCalls,
1526
1703
  usage: response.usage ? {
1527
1704
  promptTokens: response.usage.prompt_tokens,
1528
1705
  completionTokens: response.usage.completion_tokens,
@@ -1571,6 +1748,737 @@ function createCallableProvider(providerFn, properties) {
1571
1748
  return providerFn;
1572
1749
  }
1573
1750
 
1751
+ // src/core/decisions.ts
1752
+ var decisionErrorMarker = /* @__PURE__ */ Symbol.for("yourgpt.llm-sdk.DecisionAPIError");
1753
+ var _a, _b;
1754
+ var DecisionAPIError = class extends (_b = Error, _a = decisionErrorMarker, _b) {
1755
+ constructor(message, code, status, requestId) {
1756
+ super(message);
1757
+ this.code = code;
1758
+ this.status = status;
1759
+ this.requestId = requestId;
1760
+ this.name = "DecisionAPIError";
1761
+ this[_a] = true;
1762
+ }
1763
+ /** Works across CJS/ESM and separately bundled provider entry points. */
1764
+ static isInstance(error) {
1765
+ return typeof error === "object" && error !== null && /* @__PURE__ */ Symbol.for("yourgpt.llm-sdk.DecisionAPIError") in error;
1766
+ }
1767
+ /** Advisory only: this SDK does not retry Decisions automatically. */
1768
+ get retryable() {
1769
+ return this.code === "network_error" || this.code === "http_error" && (this.status === 408 || this.status === 429 || this.status != null && this.status >= 500);
1770
+ }
1771
+ };
1772
+
1773
+ // src/core/decision-validation.ts
1774
+ function decisionInputError(message) {
1775
+ throw new DecisionAPIError(message, "invalid_input");
1776
+ }
1777
+ function decisionResponseError(message) {
1778
+ throw new DecisionAPIError(message, "invalid_response");
1779
+ }
1780
+ function isDecisionRecord(value) {
1781
+ if (value === null || typeof value !== "object" || Array.isArray(value))
1782
+ return false;
1783
+ const proto = Object.getPrototypeOf(value);
1784
+ return proto === Object.prototype || proto === null;
1785
+ }
1786
+ function json(value, seen = /* @__PURE__ */ new Set(), depth = 0) {
1787
+ if (depth > 64) return false;
1788
+ if (value === null || typeof value === "string" || typeof value === "boolean")
1789
+ return true;
1790
+ if (typeof value === "number") return Number.isFinite(value);
1791
+ if (typeof value !== "object" || !Array.isArray(value) && !isDecisionRecord(value) || seen.has(value))
1792
+ return false;
1793
+ seen.add(value);
1794
+ const valid = Object.getOwnPropertySymbols(value).length === 0 && (Array.isArray(value) ? Array.from(value) : Object.values(value)).every(
1795
+ (item) => json(item, seen, depth + 1)
1796
+ );
1797
+ seen.delete(value);
1798
+ return valid;
1799
+ }
1800
+ function input(value) {
1801
+ return (typeof value === "string" || Array.isArray(value) || isDecisionRecord(value)) && json(value);
1802
+ }
1803
+ function assertDecisionKeys(value, allowed, label) {
1804
+ if (Object.keys(value).some((key) => !allowed.includes(key)))
1805
+ decisionInputError(`${label} contains an unsupported field`);
1806
+ }
1807
+ function validateDecisionInput(options) {
1808
+ if (!isDecisionRecord(options))
1809
+ decisionInputError("Decisions options must be an object");
1810
+ assertDecisionKeys(
1811
+ options,
1812
+ ["state", "questions", "images", "signal", "timeoutMs", "providerOptions"],
1813
+ "Decisions options"
1814
+ );
1815
+ if (!input(options.state))
1816
+ decisionInputError(
1817
+ "state must be finite, acyclic JSON text, object, or array (maximum depth 64)"
1818
+ );
1819
+ if (!isDecisionRecord(options.questions) || Object.keys(options.questions).length === 0)
1820
+ decisionInputError("questions must be a nonempty map");
1821
+ for (const [id, question] of Object.entries(options.questions)) {
1822
+ if (!id.length || !isDecisionRecord(question) || !input(question.instructions))
1823
+ decisionInputError(
1824
+ "Every question needs a nonempty ID and JSON-compatible instructions"
1825
+ );
1826
+ assertDecisionKeys(
1827
+ question,
1828
+ ["type", "instructions", "criteria"],
1829
+ "Question"
1830
+ );
1831
+ if (question.type === "boolean") {
1832
+ if (question.criteria !== void 0 && (!isDecisionRecord(question.criteria) || Object.entries(question.criteria).some(
1833
+ ([key, value]) => !["true", "false"].includes(key) || !input(value)
1834
+ )))
1835
+ decisionInputError("Boolean criteria may describe only true and false");
1836
+ } else if (question.type === "choice") {
1837
+ if (!isDecisionRecord(question.criteria) || Object.keys(question.criteria).length < 2 || Object.keys(question.criteria).length > 255 || Object.entries(question.criteria).some(
1838
+ ([key, value]) => !key.length || value !== null && !input(value)
1839
+ ))
1840
+ decisionInputError(
1841
+ "Choice criteria require 2\u2013255 named options with JSON descriptions or null"
1842
+ );
1843
+ } else if (question.type === "score") {
1844
+ if (!Array.isArray(question.criteria) || question.criteria.length < 2 || !Array.from(question.criteria).every(input))
1845
+ decisionInputError(
1846
+ "Score criteria require at least two ordered JSON descriptions"
1847
+ );
1848
+ } else
1849
+ decisionInputError("Question type must be boolean, choice, or score");
1850
+ }
1851
+ if (options.timeoutMs !== void 0 && (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 1 || options.timeoutMs > 2147483647))
1852
+ decisionInputError("timeoutMs must be a positive 32-bit integer");
1853
+ if (options.providerOptions !== void 0 && (!isDecisionRecord(options.providerOptions) || !json(options.providerOptions)))
1854
+ decisionInputError("providerOptions must be a JSON object");
1855
+ if (options.images !== void 0 && (!Array.isArray(options.images) || !Array.from(options.images).every(
1856
+ (image) => isDecisionRecord(image) && image.data instanceof Uint8Array
1857
+ )))
1858
+ decisionInputError(
1859
+ "Decision images must contain encoded bytes; hosted image URLs are unsupported"
1860
+ );
1861
+ }
1862
+ function probability(value) {
1863
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
1864
+ }
1865
+ function exactKeys(value, keys) {
1866
+ return Object.keys(value).length === keys.length && keys.every((key) => Object.prototype.hasOwnProperty.call(value, key));
1867
+ }
1868
+ function validateDecisionResult(questions, result) {
1869
+ if (!isDecisionRecord(result) || !isDecisionRecord(result.answers) || !exactKeys(result.answers, Object.keys(questions)))
1870
+ decisionResponseError(
1871
+ "Decisions must return exactly one answer per question"
1872
+ );
1873
+ const rounding = result.rounding;
1874
+ let pError = 0, sError = 0;
1875
+ if (rounding !== void 0) {
1876
+ if (!isDecisionRecord(rounding) || ![rounding.probabilityDecimals, rounding.scoreDecimals].every(
1877
+ (v) => typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 15
1878
+ ))
1879
+ decisionResponseError("Invalid decision rounding precision");
1880
+ pError = 0.5 * 10 ** -rounding.probabilityDecimals;
1881
+ sError = 0.5 * 10 ** -rounding.scoreDecimals;
1882
+ }
1883
+ const epsilon = 1e-6;
1884
+ for (const [id, question] of Object.entries(questions)) {
1885
+ const answer = result.answers[id];
1886
+ if (!isDecisionRecord(answer))
1887
+ decisionResponseError("Invalid decision answer");
1888
+ if (answer.type === "refusal") continue;
1889
+ if (answer.type !== question.type)
1890
+ decisionResponseError("Decision answer type does not match its question");
1891
+ if (answer.confidence !== void 0 && !probability(answer.confidence))
1892
+ decisionResponseError(
1893
+ "Confidence must be finite and between zero and one"
1894
+ );
1895
+ if (question.type === "boolean") {
1896
+ if (!probability(answer.probability))
1897
+ decisionResponseError(
1898
+ "Boolean probability must be between zero and one"
1899
+ );
1900
+ continue;
1901
+ }
1902
+ const keys = question.type === "choice" ? Object.keys(question.criteria) : question.criteria.map((_, index) => String(index));
1903
+ if (question.type === "choice" && (typeof answer.choice !== "string" || !keys.includes(answer.choice)))
1904
+ decisionResponseError("Decision selected an unknown choice");
1905
+ if (question.type === "score" && (typeof answer.score !== "number" || !Number.isFinite(answer.score) || answer.score < 0 || answer.score > keys.length - 1))
1906
+ decisionResponseError("Decision score is outside its ordered levels");
1907
+ if (answer.probabilities === void 0) continue;
1908
+ const distribution2 = answer.probabilities;
1909
+ if (!isDecisionRecord(distribution2) || !exactKeys(distribution2, keys) || !Object.values(distribution2).every(probability))
1910
+ decisionResponseError(
1911
+ "Decision distribution must contain every option exactly once with finite probabilities"
1912
+ );
1913
+ const values = Object.values(distribution2);
1914
+ if (Math.abs(values.reduce((a, b) => a + b, 0) - 1) > epsilon + keys.length * pError)
1915
+ decisionResponseError("Decision probabilities do not sum to one");
1916
+ if (question.type === "choice") {
1917
+ if (values.some(
1918
+ (v) => v > distribution2[answer.choice] + epsilon + 2 * pError
1919
+ ))
1920
+ decisionResponseError(
1921
+ "Decision choice is not a highest-probability option"
1922
+ );
1923
+ } else {
1924
+ const mean = keys.reduce(
1925
+ (sum, key) => sum + Number(key) * distribution2[key],
1926
+ 0
1927
+ );
1928
+ const tolerance = epsilon + sError + pError * keys.reduce((sum, key) => sum + Number(key), 0);
1929
+ if (Math.abs(answer.score - mean) > tolerance)
1930
+ decisionResponseError(
1931
+ "Decision score does not match its probability-weighted levels"
1932
+ );
1933
+ }
1934
+ }
1935
+ }
1936
+
1937
+ // src/providers/decisions/http.ts
1938
+ async function postDecision(url, headers, body, options, fetchFn, maxBytes) {
1939
+ const encoded = JSON.stringify(body);
1940
+ if (Buffer.byteLength(encoded, "utf8") > maxBytes)
1941
+ decisionInputError("Decision request exceeds the provider's byte limit");
1942
+ options.signal?.throwIfAborted();
1943
+ const controller = new AbortController();
1944
+ const relay = () => controller.abort(options.signal?.reason);
1945
+ options.signal?.addEventListener("abort", relay, { once: true });
1946
+ const timer = setTimeout(
1947
+ () => controller.abort(
1948
+ new DOMException("Decision request timed out", "TimeoutError")
1949
+ ),
1950
+ options.timeoutMs ?? 6e4
1951
+ );
1952
+ let rejectAbort = () => {
1953
+ };
1954
+ const aborted = new Promise((_, reject) => {
1955
+ rejectAbort = () => reject(controller.signal.reason);
1956
+ controller.signal.addEventListener("abort", rejectAbort, { once: true });
1957
+ });
1958
+ const perform = async () => {
1959
+ let response;
1960
+ try {
1961
+ response = await fetchFn(url, {
1962
+ method: "POST",
1963
+ headers,
1964
+ body: encoded,
1965
+ signal: controller.signal,
1966
+ redirect: "error"
1967
+ });
1968
+ } catch {
1969
+ controller.signal.throwIfAborted();
1970
+ throw new DecisionAPIError(
1971
+ "Decision request failed before receiving a response",
1972
+ "network_error"
1973
+ );
1974
+ }
1975
+ const requestId = response.headers.get("x-request-id") ?? response.headers.get("cf-ray") ?? void 0;
1976
+ if (controller.signal.aborted) {
1977
+ void response.body?.cancel().catch(() => {
1978
+ });
1979
+ controller.signal.throwIfAborted();
1980
+ }
1981
+ if (!response.ok) {
1982
+ void response.body?.cancel().catch(() => {
1983
+ });
1984
+ throw new DecisionAPIError(
1985
+ `Decision API returned HTTP ${response.status}`,
1986
+ "http_error",
1987
+ response.status,
1988
+ requestId
1989
+ );
1990
+ }
1991
+ if (!response.body)
1992
+ decisionResponseError("Decision API returned an empty body");
1993
+ const reader = response.body.getReader();
1994
+ const cancel = () => {
1995
+ void reader.cancel().catch(() => {
1996
+ });
1997
+ };
1998
+ controller.signal.addEventListener("abort", cancel, { once: true });
1999
+ const chunks = [];
2000
+ let size = 0;
2001
+ try {
2002
+ while (true) {
2003
+ controller.signal.throwIfAborted();
2004
+ const { value: value2, done } = await reader.read();
2005
+ if (done) break;
2006
+ size += value2.byteLength;
2007
+ if (size > 8 * 1024 * 1024) {
2008
+ cancel();
2009
+ decisionResponseError("Decision API response exceeds 8 MiB");
2010
+ }
2011
+ chunks.push(value2);
2012
+ }
2013
+ } catch (error) {
2014
+ controller.signal.throwIfAborted();
2015
+ if (error instanceof DecisionAPIError) throw error;
2016
+ throw new DecisionAPIError(
2017
+ "Decision response could not be read",
2018
+ "network_error",
2019
+ response.status,
2020
+ requestId
2021
+ );
2022
+ } finally {
2023
+ controller.signal.removeEventListener("abort", cancel);
2024
+ reader.releaseLock();
2025
+ }
2026
+ controller.signal.throwIfAborted();
2027
+ let value;
2028
+ try {
2029
+ value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
2030
+ } catch {
2031
+ decisionResponseError("Decision API did not return valid JSON");
2032
+ }
2033
+ return { value, status: response.status, requestId };
2034
+ };
2035
+ try {
2036
+ return await Promise.race([perform(), aborted]);
2037
+ } finally {
2038
+ clearTimeout(timer);
2039
+ options.signal?.removeEventListener("abort", relay);
2040
+ controller.signal.removeEventListener("abort", rejectAbort);
2041
+ }
2042
+ }
2043
+
2044
+ // src/providers/decisions/images.ts
2045
+ function unsupportedDecision(message) {
2046
+ throw new DecisionAPIError(message, "unsupported_option");
2047
+ }
2048
+ function prepareDecisionImages(images, provider) {
2049
+ if (provider === "openrouter" && images.length)
2050
+ unsupportedDecision(
2051
+ "OpenRouter's native Decisions schema does not support image inputs"
2052
+ );
2053
+ if (provider === "cloudflare" && images.length > 4)
2054
+ decisionInputError("Cloudflare Decisions supports at most four images");
2055
+ if (provider === "openai" && images.length > 128)
2056
+ decisionInputError("OpenAI Decisions supports at most 128 images");
2057
+ let total = 0;
2058
+ return images.map((image) => {
2059
+ assertDecisionKeys(
2060
+ image,
2061
+ ["data", "mediaType", "detail"],
2062
+ "Decision image"
2063
+ );
2064
+ if (image.detail !== void 0) {
2065
+ if (provider !== "openai")
2066
+ unsupportedDecision(
2067
+ "Image detail is only supported by OpenAI Decisions"
2068
+ );
2069
+ if (!["auto", "low", "high", "original"].includes(image.detail))
2070
+ decisionInputError("Invalid image detail");
2071
+ }
2072
+ const data = Buffer.from(image.data);
2073
+ const signature = data.subarray(0, 12).toString("latin1");
2074
+ 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;
2075
+ if (!mediaType || image.mediaType !== void 0 && image.mediaType !== mediaType)
2076
+ decisionInputError(
2077
+ "Decision image bytes must match a supported encoded image format"
2078
+ );
2079
+ if (provider === "typesafe" && !["image/png", "image/jpeg"].includes(mediaType))
2080
+ unsupportedDecision("Jev images must be PNG or JPEG");
2081
+ if (provider === "cloudflare" && mediaType === "image/gif")
2082
+ unsupportedDecision("Cloudflare Decisions does not accept GIF images");
2083
+ const limit = provider === "cloudflare" ? 4 : provider === "typesafe" ? 16 : 20;
2084
+ if (data.length > limit * 1024 * 1024)
2085
+ decisionInputError("Decision image exceeds this provider's file limit");
2086
+ total += data.length;
2087
+ if (provider === "cloudflare" && total > 8 * 1024 * 1024)
2088
+ decisionInputError("Cloudflare Decisions images exceed 8 MiB total");
2089
+ const base64 = data.toString("base64");
2090
+ return {
2091
+ url: `data:${mediaType};base64,${base64}`,
2092
+ base64,
2093
+ mediaType,
2094
+ detail: image.detail
2095
+ };
2096
+ });
2097
+ }
2098
+
2099
+ // src/providers/decisions/response.ts
2100
+ function distribution(value, score) {
2101
+ if (!Array.isArray(value))
2102
+ decisionResponseError(
2103
+ "OpenAI Decisions omitted its probability distribution"
2104
+ );
2105
+ const entries = [];
2106
+ const seen = /* @__PURE__ */ new Set();
2107
+ for (const item of value) {
2108
+ if (!isDecisionRecord(item) || (score ? typeof item.value !== "number" || !Number.isInteger(item.value) || item.value < 0 : typeof item.value !== "string"))
2109
+ decisionResponseError("Invalid decision probability option");
2110
+ const key = String(item.value);
2111
+ if (seen.has(key))
2112
+ decisionResponseError("Duplicate option in decision probabilities");
2113
+ seen.add(key);
2114
+ entries.push([key, item.probability]);
2115
+ }
2116
+ return Object.fromEntries(entries);
2117
+ }
2118
+ function parseDecisionResponse(raw, provider, questions, status, requestId) {
2119
+ if (!isDecisionRecord(raw))
2120
+ decisionResponseError("Decision response must be an object");
2121
+ let payload = raw;
2122
+ if (provider === "cloudflare") {
2123
+ if (raw.success === false)
2124
+ throw new DecisionAPIError(
2125
+ "Cloudflare reported an unsuccessful decision request",
2126
+ "http_error",
2127
+ status,
2128
+ requestId
2129
+ );
2130
+ if (raw.success !== true || !isDecisionRecord(raw.result))
2131
+ decisionResponseError(
2132
+ "Cloudflare Decisions returned an invalid result envelope"
2133
+ );
2134
+ payload = raw.result;
2135
+ }
2136
+ if (typeof payload.model !== "string" || !payload.model)
2137
+ decisionResponseError("Decision response omitted its model identity");
2138
+ const entries = [];
2139
+ if (provider === "openai") {
2140
+ if (!Array.isArray(payload.answers))
2141
+ decisionResponseError("OpenAI Decisions answers must be an array");
2142
+ const seen = /* @__PURE__ */ new Set();
2143
+ for (const answer of payload.answers) {
2144
+ if (!isDecisionRecord(answer))
2145
+ decisionResponseError("Invalid OpenAI decision answer");
2146
+ if (answer.type === "refusal" && answer.name == null)
2147
+ throw new DecisionAPIError(
2148
+ "OpenAI refused an unnamed decision question",
2149
+ "refusal",
2150
+ status,
2151
+ requestId
2152
+ );
2153
+ if (typeof answer.name !== "string" || seen.has(answer.name))
2154
+ decisionResponseError(
2155
+ "OpenAI decision answers have missing or duplicate question identities"
2156
+ );
2157
+ seen.add(answer.name);
2158
+ entries.push([answer.name, answer]);
2159
+ }
2160
+ } else {
2161
+ if (!isDecisionRecord(payload.answers))
2162
+ decisionResponseError("Decisions answers must be a named map");
2163
+ entries.push(...Object.entries(payload.answers));
2164
+ }
2165
+ const answers = Object.fromEntries(
2166
+ entries.map(([id, value]) => {
2167
+ if (!isDecisionRecord(value))
2168
+ decisionResponseError("Invalid decision answer");
2169
+ if (value.type === "refusal" && provider === "openai")
2170
+ return [id, { type: "refusal" }];
2171
+ const booleanType = provider === "openai" ? "predicate" : "noul";
2172
+ if (value.type === booleanType)
2173
+ return [
2174
+ id,
2175
+ {
2176
+ type: "boolean",
2177
+ probability: provider === "openai" ? value.probability : value.noul
2178
+ }
2179
+ ];
2180
+ if (value.type !== "choice" && value.type !== "score")
2181
+ decisionResponseError(
2182
+ "Provider returned an unsupported decision answer type"
2183
+ );
2184
+ if (provider !== "openrouter" && value.probabilities == null)
2185
+ decisionResponseError(
2186
+ "Provider omitted a required decision distribution"
2187
+ );
2188
+ const probabilities = provider === "openai" ? distribution(value.probabilities, value.type === "score") : value.probabilities;
2189
+ const fields = {
2190
+ ...probabilities !== void 0 && {
2191
+ probabilities
2192
+ },
2193
+ ...value.confidence != null && {
2194
+ confidence: value.confidence
2195
+ }
2196
+ };
2197
+ return [
2198
+ id,
2199
+ value.type === "choice" ? { type: "choice", choice: value.choice, ...fields } : { type: "score", score: value.score, ...fields }
2200
+ ];
2201
+ })
2202
+ );
2203
+ const result = {
2204
+ answers,
2205
+ response: {
2206
+ provider,
2207
+ modelId: payload.model,
2208
+ status,
2209
+ requestId: requestId ?? (typeof payload.id === "string" ? payload.id : void 0),
2210
+ body: raw
2211
+ }
2212
+ };
2213
+ const typesafe = provider === "typesafe" || provider === "openrouter" && (payload.model.startsWith("typesafe/") || payload.provider === "TypeSafe");
2214
+ if (typesafe) result.rounding = { probabilityDecimals: 2, scoreDecimals: 2 };
2215
+ if (payload.usage != null) {
2216
+ if (!isDecisionRecord(payload.usage))
2217
+ decisionResponseError("Invalid decision usage");
2218
+ const usage = payload.usage;
2219
+ const details = isDecisionRecord(usage.input_tokens_details) ? usage.input_tokens_details : {};
2220
+ result.usage = {};
2221
+ const mapped = {
2222
+ inputTokens: usage.input_tokens,
2223
+ outputTokens: usage.output_tokens,
2224
+ totalTokens: usage.total_tokens,
2225
+ cachedInputTokens: details.cached_tokens,
2226
+ cacheWriteTokens: details.cache_write_tokens,
2227
+ costUsd: usage.cost
2228
+ };
2229
+ for (const [key, value] of Object.entries(mapped)) {
2230
+ if (value == null) continue;
2231
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || key !== "costUsd" && !Number.isInteger(value))
2232
+ decisionResponseError("Invalid decision usage count");
2233
+ result.usage[key] = value;
2234
+ }
2235
+ }
2236
+ validateDecisionResult(questions, result);
2237
+ return result;
2238
+ }
2239
+
2240
+ // src/providers/decisions/model.ts
2241
+ function asText(value) {
2242
+ return typeof value === "string" ? value : JSON.stringify(value);
2243
+ }
2244
+ function requestOptions(options, provider) {
2245
+ for (const key of Object.keys(options.providerOptions ?? {})) {
2246
+ if (key !== provider)
2247
+ unsupportedDecision(
2248
+ "Decision providerOptions must match the selected provider"
2249
+ );
2250
+ }
2251
+ const opts = options.providerOptions?.[provider] ?? {};
2252
+ if (!isDecisionRecord(opts))
2253
+ decisionInputError("Decision provider options must be an object");
2254
+ const allowed = provider === "openai" ? ["safetyIdentifier", "promptCacheKey"] : provider === "typesafe" ? ["longContext"] : provider === "openrouter" ? ["sessionId", "provider"] : [];
2255
+ assertDecisionKeys(opts, allowed, "Decision provider options");
2256
+ for (const [key, limit] of [
2257
+ ["safetyIdentifier", 128],
2258
+ ["promptCacheKey", 64],
2259
+ ["sessionId", 256]
2260
+ ]) {
2261
+ if (opts[key] !== void 0 && (typeof opts[key] !== "string" || opts[key].length > limit))
2262
+ decisionInputError("Invalid decision identifier or cache key");
2263
+ }
2264
+ if (opts.longContext !== void 0 && typeof opts.longContext !== "boolean")
2265
+ decisionInputError("longContext must be boolean");
2266
+ if (opts.provider !== void 0) {
2267
+ if (!isDecisionRecord(opts.provider))
2268
+ decisionInputError("OpenRouter routing preferences must be an object");
2269
+ assertDecisionKeys(
2270
+ opts.provider,
2271
+ ["order", "allowFallbacks"],
2272
+ "OpenRouter routing preferences"
2273
+ );
2274
+ if (opts.provider.allowFallbacks !== void 0 && typeof opts.provider.allowFallbacks !== "boolean")
2275
+ decisionInputError("allowFallbacks must be boolean");
2276
+ if (opts.provider.order !== void 0 && (!Array.isArray(opts.provider.order) || !opts.provider.order.every(
2277
+ (value) => typeof value === "string" && value.length
2278
+ )))
2279
+ decisionInputError("Provider order must be an array of provider names");
2280
+ }
2281
+ return opts;
2282
+ }
2283
+ function nativeDecisionModel(modelId, config) {
2284
+ if (typeof modelId !== "string" || !modelId.trim())
2285
+ decisionInputError("Decision model ID is required");
2286
+ const provider = config.provider;
2287
+ return {
2288
+ provider,
2289
+ modelId,
2290
+ async doDecide(options) {
2291
+ validateDecisionInput(options);
2292
+ options.signal?.throwIfAborted();
2293
+ const opts = requestOptions(options, provider);
2294
+ const questions = Object.entries(options.questions);
2295
+ if (provider === "openai" && questions.length > 200)
2296
+ decisionInputError("OpenAI Decisions supports at most 200 questions");
2297
+ if (provider === "cloudflare" && (questions.length > 64 || questions.some(([id]) => !/^[A-Za-z0-9_.-]{1,100}$/.test(id))))
2298
+ decisionInputError(
2299
+ "Cloudflare supports 1\u201364 questions with IDs of 1\u2013100 letters, digits, underscores, dots, or hyphens"
2300
+ );
2301
+ for (const [, question] of questions) {
2302
+ if (provider !== "openai" && question.type === "score" && question.criteria.length > 10)
2303
+ decisionInputError(
2304
+ "System One adapters support at most ten score levels"
2305
+ );
2306
+ if (provider === "cloudflare" && typeof question.instructions === "string" && !question.instructions.trim())
2307
+ decisionInputError("Cloudflare requires nonempty instructions");
2308
+ if (provider === "openrouter" && question.type === "boolean" && question.criteria !== void 0 && (question.criteria.true === void 0 || question.criteria.false === void 0))
2309
+ decisionInputError(
2310
+ "OpenRouter boolean criteria must describe both true and false when supplied"
2311
+ );
2312
+ }
2313
+ const images = prepareDecisionImages(options.images ?? [], provider);
2314
+ const headers = Object.fromEntries(
2315
+ Object.entries(config.headers ?? {}).filter(
2316
+ ([name]) => !["authorization", "content-type"].includes(name.toLowerCase())
2317
+ )
2318
+ );
2319
+ headers["Content-Type"] = "application/json";
2320
+ const key = config.apiKey ?? process.env[config.keyEnv];
2321
+ if (!key?.trim())
2322
+ decisionInputError(`Missing ${config.keyEnv} for Decisions`);
2323
+ headers.Authorization = `Bearer ${key}`;
2324
+ let body;
2325
+ if (provider === "openai") {
2326
+ body = {
2327
+ model: modelId,
2328
+ input: [
2329
+ {
2330
+ role: "user",
2331
+ content: [
2332
+ { type: "input_text", text: asText(options.state) },
2333
+ ...images.map((image) => ({
2334
+ type: "input_image",
2335
+ image_url: image.url,
2336
+ ...image.detail !== void 0 && { detail: image.detail }
2337
+ }))
2338
+ ]
2339
+ }
2340
+ ],
2341
+ questions: questions.map(([name, question]) => {
2342
+ const instructions = asText(question.instructions);
2343
+ if (question.type === "boolean")
2344
+ return {
2345
+ type: "predicate",
2346
+ name,
2347
+ instructions: [
2348
+ instructions,
2349
+ ...Object.entries(question.criteria ?? {}).map(
2350
+ ([key2, value]) => `Criteria for ${key2}:
2351
+ ${asText(value)}`
2352
+ )
2353
+ ].join("\n\n")
2354
+ };
2355
+ if (question.type === "choice")
2356
+ return {
2357
+ type: "choice",
2358
+ name,
2359
+ instructions,
2360
+ choices: Object.entries(question.criteria).map(
2361
+ ([value, description]) => ({
2362
+ value,
2363
+ ...description !== null && {
2364
+ description: asText(description)
2365
+ }
2366
+ })
2367
+ )
2368
+ };
2369
+ return {
2370
+ type: "score",
2371
+ name,
2372
+ instructions,
2373
+ levels: question.criteria.map((description, index) => ({
2374
+ label: String(index),
2375
+ description: asText(description)
2376
+ }))
2377
+ };
2378
+ }),
2379
+ ...opts.safetyIdentifier !== void 0 && {
2380
+ safety_identifier: opts.safetyIdentifier
2381
+ },
2382
+ ...opts.promptCacheKey !== void 0 && {
2383
+ prompt_cache_key: opts.promptCacheKey
2384
+ }
2385
+ };
2386
+ } else {
2387
+ body = {
2388
+ model: config.bodyModel ?? modelId,
2389
+ state: options.state,
2390
+ questions: Object.fromEntries(
2391
+ questions.map(([id, question]) => [
2392
+ id,
2393
+ {
2394
+ ...question,
2395
+ type: question.type === "boolean" ? "noul" : question.type
2396
+ }
2397
+ ])
2398
+ )
2399
+ };
2400
+ if (provider === "typesafe") {
2401
+ if (opts.longContext)
2402
+ headers["TypeSafe-Danger-Zone-Long-Context"] = "true";
2403
+ if (images.length) {
2404
+ const refs = images.map(() => randomUUID());
2405
+ body.state = { state: options.state, images: refs };
2406
+ body.assets = Object.fromEntries(
2407
+ images.map((image, index) => [
2408
+ refs[index],
2409
+ {
2410
+ type: "image",
2411
+ data_base64: image.base64,
2412
+ format: image.mediaType === "image/png" ? "png" : "jpeg"
2413
+ }
2414
+ ])
2415
+ );
2416
+ headers["TypeSafe-Danger-Zone-Images"] = "true";
2417
+ }
2418
+ } else if (provider === "cloudflare" && images.length) {
2419
+ body.images = images.map((image) => ({
2420
+ content_type: image.mediaType,
2421
+ base64: image.base64
2422
+ }));
2423
+ } else if (provider === "openrouter") {
2424
+ if (opts.sessionId !== void 0) body.session_id = opts.sessionId;
2425
+ if (isDecisionRecord(opts.provider))
2426
+ body.provider = {
2427
+ ...opts.provider.order !== void 0 && {
2428
+ order: opts.provider.order
2429
+ },
2430
+ ...opts.provider.allowFallbacks !== void 0 && {
2431
+ allow_fallbacks: opts.provider.allowFallbacks
2432
+ }
2433
+ };
2434
+ }
2435
+ }
2436
+ const base = config.baseUrl ?? config.defaultBaseUrl;
2437
+ let url;
2438
+ try {
2439
+ url = new URL(`${base.replace(/\/+$/, "")}${config.path}`);
2440
+ } catch {
2441
+ decisionInputError("Invalid Decisions base URL");
2442
+ }
2443
+ if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash)
2444
+ decisionInputError(
2445
+ "Decisions base URL must be an HTTP(S) API root without credentials, query, or fragment"
2446
+ );
2447
+ const response = await postDecision(
2448
+ url.href,
2449
+ headers,
2450
+ body,
2451
+ options,
2452
+ config.fetch ?? globalThis.fetch,
2453
+ (provider === "cloudflare" ? 13 : 32) * 1024 * 1024
2454
+ );
2455
+ return parseDecisionResponse(
2456
+ response.value,
2457
+ provider,
2458
+ options.questions,
2459
+ response.status,
2460
+ response.requestId
2461
+ );
2462
+ }
2463
+ };
2464
+ }
2465
+
2466
+ // src/providers/openrouter/decisions.ts
2467
+ function openrouterDecision(modelId = "typesafe/jev-1.13", options = {}) {
2468
+ return nativeDecisionModel(modelId, {
2469
+ ...options,
2470
+ headers: {
2471
+ ...options.headers,
2472
+ ...options.siteUrl && { "HTTP-Referer": options.siteUrl },
2473
+ ...options.appName && { "X-Title": options.appName }
2474
+ },
2475
+ provider: "openrouter",
2476
+ keyEnv: "OPENROUTER_API_KEY",
2477
+ defaultBaseUrl: "https://openrouter.ai/api/alpha",
2478
+ path: "/decisions"
2479
+ });
2480
+ }
2481
+
1574
2482
  // src/providers/openrouter/index.ts
1575
2483
  var DEFAULT_CAPABILITIES = {
1576
2484
  vision: true,
@@ -1612,6 +2520,6 @@ function createOpenRouter(config = {}) {
1612
2520
  }
1613
2521
  var createOpenRouterProvider = createOpenRouter;
1614
2522
 
1615
- export { createOpenRouter, openrouter as createOpenRouterModel, createOpenRouterProvider, fetchOpenRouterModels, openrouter, searchOpenRouterModels };
2523
+ export { createOpenRouter, openrouter as createOpenRouterModel, createOpenRouterProvider, fetchOpenRouterModels, openrouter, openrouterDecision, searchOpenRouterModels };
1616
2524
  //# sourceMappingURL=index.mjs.map
1617
2525
  //# sourceMappingURL=index.mjs.map