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