@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.
- package/LICENSE +21 -0
- package/README.md +87 -17
- package/dist/adapters/index.d.mts +11 -13
- package/dist/adapters/index.d.ts +11 -13
- package/dist/adapters/index.js +406 -114
- package/dist/adapters/index.mjs +406 -114
- package/dist/audio-K-Z3-Bce.d.mts +191 -0
- package/dist/audio-K-Z3-Bce.d.ts +191 -0
- package/dist/{base-BICaQaRS.d.mts → base-6LzczuCg.d.ts} +8 -1
- package/dist/{base-CpRPB8u8.d.ts → base-DySbTP8C.d.mts} +8 -1
- package/dist/decisions-CJzOt87t.d.mts +137 -0
- package/dist/decisions-CJzOt87t.d.ts +137 -0
- package/dist/fallback/index.d.mts +4 -4
- package/dist/fallback/index.d.ts +4 -4
- package/dist/http-BQxUT5A9.d.mts +9 -0
- package/dist/http-BQxUT5A9.d.ts +9 -0
- package/dist/index.d.mts +17 -9
- package/dist/index.d.ts +17 -9
- package/dist/index.js +698 -205
- package/dist/index.mjs +695 -206
- package/dist/providers/anthropic/index.d.mts +7 -4
- package/dist/providers/anthropic/index.d.ts +7 -4
- package/dist/providers/anthropic/index.js +389 -115
- package/dist/providers/anthropic/index.mjs +389 -115
- package/dist/providers/azure/index.d.mts +3 -3
- package/dist/providers/azure/index.d.ts +3 -3
- package/dist/providers/cloudflare/index.d.mts +11 -0
- package/dist/providers/cloudflare/index.d.ts +11 -0
- package/dist/providers/cloudflare/index.js +743 -0
- package/dist/providers/cloudflare/index.mjs +741 -0
- package/dist/providers/fireworks/index.d.mts +3 -3
- package/dist/providers/fireworks/index.d.ts +3 -3
- package/dist/providers/fireworks/index.js +181 -60
- package/dist/providers/fireworks/index.mjs +181 -60
- package/dist/providers/google/index.d.mts +3 -3
- package/dist/providers/google/index.d.ts +3 -3
- package/dist/providers/google/index.js +181 -60
- package/dist/providers/google/index.mjs +181 -60
- package/dist/providers/ollama/index.d.mts +4 -4
- package/dist/providers/ollama/index.d.ts +4 -4
- package/dist/providers/openai/index.d.mts +38 -6
- package/dist/providers/openai/index.d.ts +38 -6
- package/dist/providers/openai/index.js +1451 -85
- package/dist/providers/openai/index.mjs +1449 -86
- package/dist/providers/openrouter/index.d.mts +21 -4
- package/dist/providers/openrouter/index.d.ts +21 -4
- package/dist/providers/openrouter/index.js +1040 -131
- package/dist/providers/openrouter/index.mjs +1040 -132
- package/dist/providers/togetherai/index.d.mts +3 -3
- package/dist/providers/togetherai/index.d.ts +3 -3
- package/dist/providers/togetherai/index.js +181 -60
- package/dist/providers/togetherai/index.mjs +181 -60
- package/dist/providers/typesafe/index.d.mts +8 -0
- package/dist/providers/typesafe/index.d.ts +8 -0
- package/dist/providers/typesafe/index.js +735 -0
- package/dist/providers/typesafe/index.mjs +733 -0
- package/dist/providers/xai/index.d.mts +3 -3
- package/dist/providers/xai/index.d.ts +3 -3
- package/dist/providers/xai/index.js +181 -60
- package/dist/providers/xai/index.mjs +181 -60
- package/dist/{types-DCFqkJAy.d.ts → types-B0y6VZSQ.d.ts} +1 -1
- package/dist/{types-DHQEMFHA.d.mts → types-BXQMtaCX.d.ts} +17 -3
- package/dist/{types-C3RgRbEC.d.mts → types-BY6BVhFg.d.mts} +1 -1
- package/dist/{types-Dikj3En4.d.ts → types-C7_qVSSd.d.mts} +17 -3
- package/dist/{types-Dg1Bcg1k.d.mts → types-D7lzMMLu.d.mts} +13 -1
- package/dist/{types-Dg1Bcg1k.d.ts → types-D7lzMMLu.d.ts} +13 -1
- package/dist/yourgpt/index.d.mts +1 -1
- package/dist/yourgpt/index.d.ts +1 -1
- package/docs/decisions.md +174 -0
- package/docs/media.md +201 -0
- package/docs/multimodal-and-audio.md +9 -2
- package/package.json +34 -12
- package/dist/audio-BBSwnjo8.d.mts +0 -92
- 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:
|
|
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
|
-
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
495
|
-
|
|
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 (
|
|
500
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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 (
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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 =
|
|
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) {
|
|
@@ -688,6 +815,54 @@ function responseMetadata(response) {
|
|
|
688
815
|
}
|
|
689
816
|
};
|
|
690
817
|
}
|
|
818
|
+
function responseReplayMetadata(metadata) {
|
|
819
|
+
const response = metadata?.openai?.response;
|
|
820
|
+
if (!response) return metadata;
|
|
821
|
+
const output = response.output.map((item) => {
|
|
822
|
+
const fields = ["type", "id", "status"];
|
|
823
|
+
switch (item.type) {
|
|
824
|
+
case "reasoning":
|
|
825
|
+
fields.push("encrypted_content");
|
|
826
|
+
break;
|
|
827
|
+
case "message":
|
|
828
|
+
fields.push("role", "phase");
|
|
829
|
+
break;
|
|
830
|
+
case "function_call":
|
|
831
|
+
fields.push("call_id", "name", "namespace", "arguments");
|
|
832
|
+
break;
|
|
833
|
+
case "tool_search_call":
|
|
834
|
+
fields.push("call_id", "execution", "arguments");
|
|
835
|
+
break;
|
|
836
|
+
case "tool_search_output":
|
|
837
|
+
fields.push("call_id", "tools");
|
|
838
|
+
break;
|
|
839
|
+
case "web_search_call":
|
|
840
|
+
fields.push("action");
|
|
841
|
+
break;
|
|
842
|
+
default:
|
|
843
|
+
return stripItemDiagnostics(item);
|
|
844
|
+
}
|
|
845
|
+
const replay = Object.fromEntries(
|
|
846
|
+
fields.filter((key) => item[key] !== void 0).map((key) => [key, item[key]])
|
|
847
|
+
);
|
|
848
|
+
if (item.type === "reasoning") replay.summary = [];
|
|
849
|
+
if (item.type === "message")
|
|
850
|
+
replay.content = Array.isArray(item.content) ? item.content.flatMap(
|
|
851
|
+
(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 }] : []
|
|
852
|
+
) : [];
|
|
853
|
+
return stripItemDiagnostics(replay);
|
|
854
|
+
});
|
|
855
|
+
return { openai: { response: { output, refusals: [] } } };
|
|
856
|
+
}
|
|
857
|
+
function stripItemDiagnostics(item) {
|
|
858
|
+
const {
|
|
859
|
+
usage: _usage,
|
|
860
|
+
logprobs: _logprobs,
|
|
861
|
+
metadata: _metadata,
|
|
862
|
+
...state
|
|
863
|
+
} = item;
|
|
864
|
+
return state;
|
|
865
|
+
}
|
|
691
866
|
function getResponseReplay(metadata) {
|
|
692
867
|
const response = metadata?.openai?.response;
|
|
693
868
|
if (response === void 0) return void 0;
|
|
@@ -696,29 +871,7 @@ function getResponseReplay(metadata) {
|
|
|
696
871
|
"Invalid OpenAI Responses replay metadata: output must contain typed items"
|
|
697
872
|
);
|
|
698
873
|
}
|
|
699
|
-
return response
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// src/core/provider-error.ts
|
|
703
|
-
function providerErrorEvent(error, code) {
|
|
704
|
-
const details = error ?? {};
|
|
705
|
-
const event = {
|
|
706
|
-
type: "error",
|
|
707
|
-
message: error instanceof Error ? error.message : String(error),
|
|
708
|
-
code,
|
|
709
|
-
...typeof details.status === "number" ? { status: details.status } : {},
|
|
710
|
-
...typeof details.code === "string" ? { providerCode: details.code } : {},
|
|
711
|
-
...typeof details.param === "string" ? { param: details.param } : {},
|
|
712
|
-
...typeof details.request_id === "string" ? { requestId: details.request_id } : {}
|
|
713
|
-
};
|
|
714
|
-
Object.defineProperty(event, "cause", { value: error, enumerable: false });
|
|
715
|
-
return event;
|
|
716
|
-
}
|
|
717
|
-
function throwIfAborted(signal) {
|
|
718
|
-
if (!signal?.aborted) return;
|
|
719
|
-
const error = new Error("The operation was aborted");
|
|
720
|
-
error.name = "AbortError";
|
|
721
|
-
throw error;
|
|
874
|
+
return responseReplayMetadata(metadata)?.openai?.response?.output;
|
|
722
875
|
}
|
|
723
876
|
|
|
724
877
|
// src/core/utils.ts
|
|
@@ -728,9 +881,6 @@ function generateId(prefix = "id") {
|
|
|
728
881
|
function generateMessageId() {
|
|
729
882
|
return generateId("msg");
|
|
730
883
|
}
|
|
731
|
-
function generateToolCallId() {
|
|
732
|
-
return generateId("call");
|
|
733
|
-
}
|
|
734
884
|
|
|
735
885
|
// src/adapters/openai.ts
|
|
736
886
|
var OpenAIAdapter = class _OpenAIAdapter {
|
|
@@ -754,6 +904,8 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
754
904
|
apiKey: this.config.apiKey,
|
|
755
905
|
baseURL: this.config.baseUrl,
|
|
756
906
|
organization: this.config.organization,
|
|
907
|
+
project: this.config.project,
|
|
908
|
+
timeout: this.config.timeout,
|
|
757
909
|
defaultHeaders: this.config.headers
|
|
758
910
|
});
|
|
759
911
|
}
|
|
@@ -818,11 +970,11 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
818
970
|
}
|
|
819
971
|
buildResponsesInput(request) {
|
|
820
972
|
const sourceMessages = request.rawMessages && request.rawMessages.length > 0 ? this.formatRawMessages(request.rawMessages, true) : formatMessagesForOpenAI(request.messages, void 0, true);
|
|
821
|
-
const
|
|
973
|
+
const input2 = [];
|
|
822
974
|
for (const message of sourceMessages) {
|
|
823
975
|
if (message.role === "system") {
|
|
824
976
|
if (message.content && message.content !== request.systemPrompt) {
|
|
825
|
-
|
|
977
|
+
input2.push({
|
|
826
978
|
type: "message",
|
|
827
979
|
role: "developer",
|
|
828
980
|
content: this.toResponsesContent(message.content, "developer")
|
|
@@ -833,12 +985,12 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
833
985
|
if (message.role === "assistant") {
|
|
834
986
|
const replay = getResponseReplay(message.providerMetadata);
|
|
835
987
|
if (replay) {
|
|
836
|
-
|
|
988
|
+
input2.push(...replay);
|
|
837
989
|
continue;
|
|
838
990
|
}
|
|
839
991
|
const content = this.toResponsesContent(message.content, "assistant");
|
|
840
992
|
if (typeof content === "string" ? content : Array.isArray(content) && content.length > 0) {
|
|
841
|
-
|
|
993
|
+
input2.push({
|
|
842
994
|
type: "message",
|
|
843
995
|
role: "assistant",
|
|
844
996
|
content
|
|
@@ -846,7 +998,7 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
846
998
|
}
|
|
847
999
|
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
848
1000
|
for (const toolCall of toolCalls) {
|
|
849
|
-
|
|
1001
|
+
input2.push({
|
|
850
1002
|
type: "function_call",
|
|
851
1003
|
call_id: toolCall.id,
|
|
852
1004
|
name: toolCall.function?.name,
|
|
@@ -856,7 +1008,7 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
856
1008
|
continue;
|
|
857
1009
|
}
|
|
858
1010
|
if (message.role === "tool") {
|
|
859
|
-
|
|
1011
|
+
input2.push({
|
|
860
1012
|
type: "function_call_output",
|
|
861
1013
|
call_id: message.tool_call_id,
|
|
862
1014
|
output: message.modelOutput ? toolOutputForProvider(message.modelOutput, "responses") : typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? null)
|
|
@@ -864,19 +1016,19 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
864
1016
|
continue;
|
|
865
1017
|
}
|
|
866
1018
|
const role = message.role === "developer" ? "developer" : "user";
|
|
867
|
-
|
|
1019
|
+
input2.push({
|
|
868
1020
|
type: "message",
|
|
869
1021
|
role,
|
|
870
1022
|
content: this.toResponsesContent(message.content, role)
|
|
871
1023
|
});
|
|
872
1024
|
}
|
|
873
|
-
return
|
|
1025
|
+
return input2;
|
|
874
1026
|
}
|
|
875
1027
|
/** Preserve Copilot screenshot attachments on every OpenAI transport. */
|
|
876
1028
|
formatRawMessages(messages, preserveProviderMetadata = false) {
|
|
877
1029
|
return messages.map((message) => {
|
|
878
1030
|
const { attachments, providerMetadata, modelOutput, ...rest } = message;
|
|
879
|
-
let content = message.role === "tool" && !preserveProviderMetadata ? textToolOutput(modelOutput,
|
|
1031
|
+
let content = message.role === "tool" && !preserveProviderMetadata && modelOutput !== void 0 ? textToolOutput(modelOutput, "") : message.content;
|
|
880
1032
|
if (preserveProviderMetadata && modelOutput)
|
|
881
1033
|
rest.modelOutput = modelOutput;
|
|
882
1034
|
if (message.role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.length > 0 && content === "") {
|
|
@@ -979,17 +1131,32 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
979
1131
|
const seenCallIds = /* @__PURE__ */ new Set();
|
|
980
1132
|
const toolCalls = Array.isArray(response?.output) ? response.output.filter((item) => item?.type === "function_call").map((item) => {
|
|
981
1133
|
if (typeof item.call_id !== "string" || !item.call_id || typeof item.name !== "string" || !item.name || typeof item.arguments !== "string" || item.status && item.status !== "completed") {
|
|
982
|
-
throw
|
|
1134
|
+
throw invalidProviderResponse(
|
|
1135
|
+
"Responses contains an incomplete function call"
|
|
1136
|
+
);
|
|
983
1137
|
}
|
|
984
1138
|
if (seenCallIds.has(item.call_id))
|
|
985
|
-
throw
|
|
1139
|
+
throw invalidProviderResponse(
|
|
1140
|
+
"Responses contains duplicate function call IDs"
|
|
1141
|
+
);
|
|
986
1142
|
seenCallIds.add(item.call_id);
|
|
987
|
-
|
|
1143
|
+
let args;
|
|
1144
|
+
try {
|
|
1145
|
+
args = JSON.parse(item.arguments);
|
|
1146
|
+
} catch {
|
|
1147
|
+
throw invalidProviderResponse(
|
|
1148
|
+
"Responses function arguments must be valid JSON"
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
988
1151
|
if (!args || typeof args !== "object" || Array.isArray(args))
|
|
989
|
-
throw
|
|
1152
|
+
throw invalidProviderResponse(
|
|
990
1153
|
"Responses function arguments must be a JSON object"
|
|
991
1154
|
);
|
|
992
|
-
return {
|
|
1155
|
+
return {
|
|
1156
|
+
id: item.call_id,
|
|
1157
|
+
name: item.name,
|
|
1158
|
+
args
|
|
1159
|
+
};
|
|
993
1160
|
}) : [];
|
|
994
1161
|
return {
|
|
995
1162
|
content,
|
|
@@ -1020,6 +1187,16 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
1020
1187
|
request.config?.responseFormat
|
|
1021
1188
|
);
|
|
1022
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
|
+
);
|
|
1023
1200
|
const payload = {
|
|
1024
1201
|
model: modelIdForPayload,
|
|
1025
1202
|
instructions: request.systemPrompt,
|
|
@@ -1043,14 +1220,7 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
1043
1220
|
...options?.promptCacheKey !== void 0 ? { prompt_cache_key: options.promptCacheKey } : {},
|
|
1044
1221
|
...options?.promptCacheOptions !== void 0 ? { prompt_cache_options: options.promptCacheOptions } : {},
|
|
1045
1222
|
...options?.promptCacheRetention !== void 0 ? { prompt_cache_retention: options.promptCacheRetention } : {},
|
|
1046
|
-
...
|
|
1047
|
-
include: [
|
|
1048
|
-
.../* @__PURE__ */ new Set([
|
|
1049
|
-
...options?.include ?? [],
|
|
1050
|
-
...options?.store === false ? ["reasoning.encrypted_content"] : []
|
|
1051
|
-
])
|
|
1052
|
-
]
|
|
1053
|
-
} : {},
|
|
1223
|
+
...include ? { include } : {},
|
|
1054
1224
|
...responsesTextFormat ? { text: { format: responsesTextFormat } } : {},
|
|
1055
1225
|
stream
|
|
1056
1226
|
};
|
|
@@ -1232,7 +1402,9 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
1232
1402
|
const uniqueCalls = /* @__PURE__ */ new Map();
|
|
1233
1403
|
for (const call of toolCalls.values()) {
|
|
1234
1404
|
if (!call.id || !call.name)
|
|
1235
|
-
throw
|
|
1405
|
+
throw invalidProviderResponse(
|
|
1406
|
+
"Responses stream contains an incomplete tool call"
|
|
1407
|
+
);
|
|
1236
1408
|
uniqueCalls.set(call.id, call);
|
|
1237
1409
|
}
|
|
1238
1410
|
toolCalls.clear();
|
|
@@ -1419,15 +1591,20 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
1419
1591
|
}
|
|
1420
1592
|
throwIfAborted(request.signal);
|
|
1421
1593
|
if (!finishReason)
|
|
1422
|
-
throw
|
|
1594
|
+
throw invalidProviderResponse(
|
|
1595
|
+
"OpenAI stream ended without a finish reason"
|
|
1596
|
+
);
|
|
1423
1597
|
if (toolCalls.size && !["tool_calls", "function_call"].includes(finishReason)) {
|
|
1424
|
-
throw
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
if (!call.id || !call.name)
|
|
1428
|
-
throw new Error("OpenAI stream contains an incomplete tool call");
|
|
1429
|
-
JSON.parse(call.arguments || "{}");
|
|
1598
|
+
throw invalidProviderResponse(
|
|
1599
|
+
"OpenAI stream ended with incomplete tool calls"
|
|
1600
|
+
);
|
|
1430
1601
|
}
|
|
1602
|
+
validateToolCalls(
|
|
1603
|
+
[...toolCalls.values()].map((call) => ({
|
|
1604
|
+
...call,
|
|
1605
|
+
args: parseToolArguments(call.arguments)
|
|
1606
|
+
}))
|
|
1607
|
+
);
|
|
1431
1608
|
for (const call of toolCalls.values()) {
|
|
1432
1609
|
yield {
|
|
1433
1610
|
type: "action:start",
|
|
@@ -1511,20 +1688,20 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
1511
1688
|
logProviderPayload("openai", "response payload", response, request.debug);
|
|
1512
1689
|
const choice = response.choices?.[0];
|
|
1513
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
|
+
);
|
|
1514
1702
|
return {
|
|
1515
1703
|
content: message?.content ?? "",
|
|
1516
|
-
toolCalls
|
|
1517
|
-
id: toolCall.id ?? generateToolCallId(),
|
|
1518
|
-
name: toolCall.function?.name ?? "",
|
|
1519
|
-
args: (() => {
|
|
1520
|
-
try {
|
|
1521
|
-
return JSON.parse(toolCall.function?.arguments ?? "{}");
|
|
1522
|
-
} catch {
|
|
1523
|
-
return {};
|
|
1524
|
-
}
|
|
1525
|
-
})(),
|
|
1526
|
-
...toolCall.extra_content ? { extra_content: toolCall.extra_content } : {}
|
|
1527
|
-
})) ?? [],
|
|
1704
|
+
toolCalls,
|
|
1528
1705
|
usage: response.usage ? {
|
|
1529
1706
|
promptTokens: response.usage.prompt_tokens,
|
|
1530
1707
|
completionTokens: response.usage.completion_tokens,
|
|
@@ -1573,6 +1750,737 @@ function createCallableProvider(providerFn, properties) {
|
|
|
1573
1750
|
return providerFn;
|
|
1574
1751
|
}
|
|
1575
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
|
+
|
|
1576
2484
|
// src/providers/openrouter/index.ts
|
|
1577
2485
|
var DEFAULT_CAPABILITIES = {
|
|
1578
2486
|
vision: true,
|
|
@@ -1619,6 +2527,7 @@ exports.createOpenRouterModel = openrouter;
|
|
|
1619
2527
|
exports.createOpenRouterProvider = createOpenRouterProvider;
|
|
1620
2528
|
exports.fetchOpenRouterModels = fetchOpenRouterModels;
|
|
1621
2529
|
exports.openrouter = openrouter;
|
|
2530
|
+
exports.openrouterDecision = openrouterDecision;
|
|
1622
2531
|
exports.searchOpenRouterModels = searchOpenRouterModels;
|
|
1623
2532
|
//# sourceMappingURL=index.js.map
|
|
1624
2533
|
//# sourceMappingURL=index.js.map
|