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