@yourgpt/llm-sdk 2.6.0-beta.1 → 2.6.0-beta.3
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/README.md +146 -0
- package/dist/adapters/index.d.mts +5 -5
- package/dist/adapters/index.d.ts +5 -5
- package/dist/adapters/index.js +437 -39
- package/dist/adapters/index.mjs +437 -39
- package/dist/audio-BBSwnjo8.d.mts +92 -0
- package/dist/audio-BBSwnjo8.d.ts +92 -0
- package/dist/{base-CkeT7u-O.d.mts → base-BICaQaRS.d.mts} +7 -3
- package/dist/{base-BdTpUAWI.d.ts → base-CpRPB8u8.d.ts} +7 -3
- package/dist/fallback/index.d.mts +4 -4
- package/dist/fallback/index.d.ts +4 -4
- package/dist/index.d.mts +20 -36
- package/dist/index.d.ts +20 -36
- package/dist/index.js +327 -69
- package/dist/index.mjs +325 -70
- package/dist/providers/anthropic/index.d.mts +3 -3
- package/dist/providers/anthropic/index.d.ts +3 -3
- package/dist/providers/anthropic/index.js +116 -7
- package/dist/providers/anthropic/index.mjs +116 -7
- package/dist/providers/azure/index.d.mts +3 -3
- package/dist/providers/azure/index.d.ts +3 -3
- package/dist/providers/azure/index.js +101 -4
- package/dist/providers/azure/index.mjs +101 -4
- package/dist/providers/fireworks/index.d.mts +3 -3
- package/dist/providers/fireworks/index.d.ts +3 -3
- package/dist/providers/fireworks/index.js +393 -31
- package/dist/providers/fireworks/index.mjs +393 -31
- package/dist/providers/google/index.d.mts +3 -3
- package/dist/providers/google/index.d.ts +3 -3
- package/dist/providers/google/index.js +393 -31
- package/dist/providers/google/index.mjs +393 -31
- package/dist/providers/ollama/index.d.mts +4 -4
- package/dist/providers/ollama/index.d.ts +4 -4
- package/dist/providers/ollama/index.js +87 -2
- package/dist/providers/ollama/index.mjs +87 -2
- package/dist/providers/openai/index.d.mts +25 -6
- package/dist/providers/openai/index.d.ts +25 -6
- package/dist/providers/openai/index.js +822 -40
- package/dist/providers/openai/index.mjs +821 -41
- package/dist/providers/openrouter/index.d.mts +3 -3
- package/dist/providers/openrouter/index.d.ts +3 -3
- package/dist/providers/openrouter/index.js +393 -31
- package/dist/providers/openrouter/index.mjs +393 -31
- package/dist/providers/togetherai/index.d.mts +3 -3
- package/dist/providers/togetherai/index.d.ts +3 -3
- package/dist/providers/togetherai/index.js +393 -31
- package/dist/providers/togetherai/index.mjs +393 -31
- package/dist/providers/xai/index.d.mts +3 -3
- package/dist/providers/xai/index.d.ts +3 -3
- package/dist/providers/xai/index.js +393 -31
- package/dist/providers/xai/index.mjs +393 -31
- package/dist/{types-BWRYIvl_.d.mts → types-C3RgRbEC.d.mts} +1 -1
- package/dist/{types-CpXcT-tY.d.ts → types-DCFqkJAy.d.ts} +1 -1
- package/dist/{types-NOswlhm9.d.mts → types-DHQEMFHA.d.mts} +3 -3
- package/dist/{types-BW3csYo5.d.mts → types-Dg1Bcg1k.d.mts} +106 -3
- package/dist/{types-BW3csYo5.d.ts → types-Dg1Bcg1k.d.ts} +106 -3
- package/dist/{types-DA54bSmX.d.ts → types-Dikj3En4.d.ts} +3 -3
- package/dist/yourgpt/index.d.mts +1 -1
- package/dist/yourgpt/index.d.ts +1 -1
- package/docs/multimodal-and-audio.md +170 -0
- package/package.json +3 -2
|
@@ -1,5 +1,108 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// src/core/tool-output.ts
|
|
4
|
+
var imageTypes = /* @__PURE__ */ new Set([
|
|
5
|
+
"image/png",
|
|
6
|
+
"image/jpeg",
|
|
7
|
+
"image/webp",
|
|
8
|
+
"image/gif"
|
|
9
|
+
]);
|
|
10
|
+
function normalizeToolOutput(value) {
|
|
11
|
+
const output = value;
|
|
12
|
+
if (output?.type !== "content" || !Array.isArray(output.content) || !output.content.length) {
|
|
13
|
+
throw new Error("Tool model output must contain a nonempty content array");
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
type: "content",
|
|
17
|
+
content: output.content.map((part) => {
|
|
18
|
+
if (part?.type === "text" && typeof part.text === "string")
|
|
19
|
+
return { type: "text", text: part.text };
|
|
20
|
+
if (part?.type !== "image")
|
|
21
|
+
throw new Error("Unsupported tool output content type");
|
|
22
|
+
const sources = ["data", "url", "fileId"].filter((key) => key in part);
|
|
23
|
+
if (sources.length !== 1)
|
|
24
|
+
throw new Error(
|
|
25
|
+
"Tool image requires exactly one of data, url, or fileId"
|
|
26
|
+
);
|
|
27
|
+
if ("fileId" in part) {
|
|
28
|
+
if (part.provider !== "openai" || typeof part.fileId !== "string" || !part.fileId.trim())
|
|
29
|
+
throw new Error(
|
|
30
|
+
"Tool image file references must name the openai provider and fileId"
|
|
31
|
+
);
|
|
32
|
+
return { type: "image", fileId: part.fileId, provider: "openai" };
|
|
33
|
+
}
|
|
34
|
+
if ("url" in part) {
|
|
35
|
+
let url;
|
|
36
|
+
try {
|
|
37
|
+
url = new URL(part.url);
|
|
38
|
+
} catch {
|
|
39
|
+
throw new Error("Invalid tool image URL");
|
|
40
|
+
}
|
|
41
|
+
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password)
|
|
42
|
+
throw new Error(
|
|
43
|
+
"Tool image URL must use HTTP(S) without credentials"
|
|
44
|
+
);
|
|
45
|
+
return { type: "image", url: url.href };
|
|
46
|
+
}
|
|
47
|
+
if (!("data" in part) || !imageTypes.has(part.mediaType))
|
|
48
|
+
throw new Error("Unsupported tool image media type");
|
|
49
|
+
let data = part.data instanceof Uint8Array ? Buffer.from(part.data).toString("base64") : part.data;
|
|
50
|
+
if (typeof data !== "string")
|
|
51
|
+
throw new Error("Tool image data must be base64 or bytes");
|
|
52
|
+
if (data.startsWith("data:")) {
|
|
53
|
+
const match = /^data:([^;,]+);base64,(.+)$/.exec(data);
|
|
54
|
+
if (!match || match[1] !== part.mediaType)
|
|
55
|
+
throw new Error(
|
|
56
|
+
"Tool image data URL media type does not match mediaType"
|
|
57
|
+
);
|
|
58
|
+
data = match[2];
|
|
59
|
+
}
|
|
60
|
+
if (!data.length || data.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(data))
|
|
61
|
+
throw new Error("Invalid tool image base64 data");
|
|
62
|
+
if (Buffer.byteLength(data, "base64") > 20 * 1024 * 1024)
|
|
63
|
+
throw new Error("Tool image exceeds 20 MiB");
|
|
64
|
+
if (Buffer.from(data, "base64").toString("base64") !== data)
|
|
65
|
+
throw new Error("Invalid tool image base64 data");
|
|
66
|
+
return { type: "image", data, mediaType: part.mediaType };
|
|
67
|
+
})
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function toolOutputForProvider(value, provider) {
|
|
71
|
+
const output = normalizeToolOutput(value);
|
|
72
|
+
return output.content.map((part) => {
|
|
73
|
+
if (part.type === "text")
|
|
74
|
+
return {
|
|
75
|
+
type: provider === "responses" ? "input_text" : "text",
|
|
76
|
+
text: part.text
|
|
77
|
+
};
|
|
78
|
+
if (provider === "unsupported")
|
|
79
|
+
throw new Error(
|
|
80
|
+
"Image tool results require OpenAI Responses or Anthropic; this endpoint is not supported"
|
|
81
|
+
);
|
|
82
|
+
if ("fileId" in part) {
|
|
83
|
+
if (provider !== "responses")
|
|
84
|
+
throw new Error(
|
|
85
|
+
"OpenAI image file references cannot be sent to another provider"
|
|
86
|
+
);
|
|
87
|
+
return { type: "input_image", file_id: part.fileId, detail: "auto" };
|
|
88
|
+
}
|
|
89
|
+
if (provider === "responses")
|
|
90
|
+
return {
|
|
91
|
+
type: "input_image",
|
|
92
|
+
image_url: "url" in part ? part.url : `data:${part.mediaType};base64,${part.data}`,
|
|
93
|
+
detail: "auto"
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
type: "image",
|
|
97
|
+
source: "url" in part ? { type: "url", url: part.url } : { type: "base64", media_type: part.mediaType, data: part.data }
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function textToolOutput(value, fallback) {
|
|
102
|
+
if (value === void 0) return fallback;
|
|
103
|
+
return toolOutputForProvider(value, "unsupported").map((part) => part.text).join("\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
3
106
|
// src/core/openai-parameters.ts
|
|
4
107
|
function capabilities(model) {
|
|
5
108
|
const gpt = /^gpt-(\d+)(?:\.(\d+))?(?:-(.+))?$/i.exec(model);
|
|
@@ -51,7 +154,120 @@ function buildOpenAIParameters(model, maxTokens, temperature, options = {}, api
|
|
|
51
154
|
return result;
|
|
52
155
|
}
|
|
53
156
|
|
|
157
|
+
// src/core/openai-responses.ts
|
|
158
|
+
function resolveOpenAITransport(defaults, overrides) {
|
|
159
|
+
return {
|
|
160
|
+
api: overrides?.api ?? defaults.api ?? "auto",
|
|
161
|
+
responses: defaults.responses !== void 0 || overrides?.responses !== void 0 ? { ...defaults.responses, ...overrides?.responses } : void 0
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function useOpenAIResponses(model, baseURL, parameters, transport2, hasTools, hasReplay, nativeToolSearch = false) {
|
|
165
|
+
const api = transport2.api ?? "auto";
|
|
166
|
+
if (!["auto", "chat", "responses"].includes(api)) {
|
|
167
|
+
throw new Error(`Unknown OpenAI API selection: ${api}`);
|
|
168
|
+
}
|
|
169
|
+
if (api === "chat") {
|
|
170
|
+
if (transport2.responses !== void 0 || nativeToolSearch) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
"Responses options and native tool search require the Responses API"
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
let officialEndpoint = !baseURL;
|
|
178
|
+
try {
|
|
179
|
+
officialEndpoint = !baseURL || new URL(baseURL).hostname === "api.openai.com";
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
return api === "responses" || transport2.responses !== void 0 || hasReplay && officialEndpoint || nativeToolSearch || hasTools && requiresOpenAIResponses(model, baseURL, parameters);
|
|
183
|
+
}
|
|
184
|
+
function responseMetadata(response) {
|
|
185
|
+
const output = Array.isArray(response?.output) ? response.output : [];
|
|
186
|
+
return {
|
|
187
|
+
openai: {
|
|
188
|
+
response: {
|
|
189
|
+
id: response?.id,
|
|
190
|
+
model: response?.model,
|
|
191
|
+
status: response?.status,
|
|
192
|
+
output,
|
|
193
|
+
usage: response?.usage,
|
|
194
|
+
store: response?.store,
|
|
195
|
+
refusals: output.flatMap(
|
|
196
|
+
(item) => item?.type === "message" && Array.isArray(item.content) ? item.content.filter(
|
|
197
|
+
(part) => part?.type === "refusal" && typeof part.refusal === "string"
|
|
198
|
+
).map((part) => part.refusal) : []
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function responseReplayMetadata(metadata) {
|
|
205
|
+
const response = metadata?.openai?.response;
|
|
206
|
+
if (!response) return metadata;
|
|
207
|
+
const output = response.output.map((item) => {
|
|
208
|
+
const fields = ["type", "id", "status"];
|
|
209
|
+
switch (item.type) {
|
|
210
|
+
case "reasoning":
|
|
211
|
+
fields.push("encrypted_content");
|
|
212
|
+
break;
|
|
213
|
+
case "message":
|
|
214
|
+
fields.push("role", "phase");
|
|
215
|
+
break;
|
|
216
|
+
case "function_call":
|
|
217
|
+
fields.push("call_id", "name", "namespace", "arguments");
|
|
218
|
+
break;
|
|
219
|
+
case "tool_search_call":
|
|
220
|
+
fields.push("call_id", "execution", "arguments");
|
|
221
|
+
break;
|
|
222
|
+
case "tool_search_output":
|
|
223
|
+
fields.push("call_id", "tools");
|
|
224
|
+
break;
|
|
225
|
+
case "web_search_call":
|
|
226
|
+
fields.push("action");
|
|
227
|
+
break;
|
|
228
|
+
default:
|
|
229
|
+
return stripItemDiagnostics(item);
|
|
230
|
+
}
|
|
231
|
+
const replay = Object.fromEntries(
|
|
232
|
+
fields.filter((key) => item[key] !== void 0).map((key) => [key, item[key]])
|
|
233
|
+
);
|
|
234
|
+
if (item.type === "reasoning") replay.summary = [];
|
|
235
|
+
if (item.type === "message")
|
|
236
|
+
replay.content = Array.isArray(item.content) ? item.content.flatMap(
|
|
237
|
+
(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 }] : []
|
|
238
|
+
) : [];
|
|
239
|
+
return stripItemDiagnostics(replay);
|
|
240
|
+
});
|
|
241
|
+
return { openai: { response: { output, refusals: [] } } };
|
|
242
|
+
}
|
|
243
|
+
function stripItemDiagnostics(item) {
|
|
244
|
+
const {
|
|
245
|
+
usage: _usage,
|
|
246
|
+
logprobs: _logprobs,
|
|
247
|
+
metadata: _metadata,
|
|
248
|
+
...state
|
|
249
|
+
} = item;
|
|
250
|
+
return state;
|
|
251
|
+
}
|
|
252
|
+
function getResponseReplay(metadata) {
|
|
253
|
+
const response = metadata?.openai?.response;
|
|
254
|
+
if (response === void 0) return void 0;
|
|
255
|
+
if (!Array.isArray(response.output) || response.output.some((item) => !item || typeof item.type !== "string")) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
"Invalid OpenAI Responses replay metadata: output must contain typed items"
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
return responseReplayMetadata(metadata)?.openai?.response?.output;
|
|
261
|
+
}
|
|
262
|
+
|
|
54
263
|
// src/core/provider-error.ts
|
|
264
|
+
function invalidProviderResponse(message) {
|
|
265
|
+
return Object.assign(new Error(message), {
|
|
266
|
+
name: "InvalidProviderResponseError",
|
|
267
|
+
status: 502,
|
|
268
|
+
code: "INVALID_PROVIDER_RESPONSE"
|
|
269
|
+
});
|
|
270
|
+
}
|
|
55
271
|
function providerErrorEvent(error, code) {
|
|
56
272
|
const details = error ?? {};
|
|
57
273
|
const event = {
|
|
@@ -283,7 +499,7 @@ function messageToOpenAIContent(message) {
|
|
|
283
499
|
}
|
|
284
500
|
return blocks;
|
|
285
501
|
}
|
|
286
|
-
function formatMessagesForOpenAI(messages, systemPrompt) {
|
|
502
|
+
function formatMessagesForOpenAI(messages, systemPrompt, preserveProviderMetadata = false) {
|
|
287
503
|
const formatted = [];
|
|
288
504
|
if (systemPrompt) {
|
|
289
505
|
formatted.push({ role: "system", content: systemPrompt });
|
|
@@ -301,7 +517,8 @@ function formatMessagesForOpenAI(messages, systemPrompt) {
|
|
|
301
517
|
const assistantMsg = {
|
|
302
518
|
role: "assistant",
|
|
303
519
|
// Gemini/xAI (OpenAI-compatible) reject content: "" on assistant messages with tool_calls
|
|
304
|
-
content: hasToolCalls ? msg.content || null : msg.content
|
|
520
|
+
content: hasToolCalls ? msg.content || null : msg.content,
|
|
521
|
+
...preserveProviderMetadata && msg.providerMetadata ? { providerMetadata: msg.providerMetadata } : {}
|
|
305
522
|
};
|
|
306
523
|
if (hasToolCalls) {
|
|
307
524
|
assistantMsg.tool_calls = msg.tool_calls;
|
|
@@ -310,7 +527,8 @@ function formatMessagesForOpenAI(messages, systemPrompt) {
|
|
|
310
527
|
} else if (msg.role === "tool" && msg.tool_call_id) {
|
|
311
528
|
formatted.push({
|
|
312
529
|
role: "tool",
|
|
313
|
-
content: msg.content ?? "",
|
|
530
|
+
content: preserveProviderMetadata ? msg.content ?? "" : textToolOutput(msg.modelOutput, msg.content ?? ""),
|
|
531
|
+
...preserveProviderMetadata && msg.modelOutput ? { modelOutput: msg.modelOutput } : {},
|
|
314
532
|
tool_call_id: msg.tool_call_id
|
|
315
533
|
});
|
|
316
534
|
}
|
|
@@ -361,13 +579,16 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
361
579
|
}));
|
|
362
580
|
}
|
|
363
581
|
shouldUseResponsesApi(request) {
|
|
364
|
-
|
|
365
|
-
if (request.providerToolOptions?.openai?.nativeToolSearch?.enabled === true && request.providerToolOptions.openai.nativeToolSearch.useResponsesApi !== false)
|
|
366
|
-
return true;
|
|
367
|
-
return requiresOpenAIResponses(
|
|
582
|
+
return useOpenAIResponses(
|
|
368
583
|
request.config?.model || this.model,
|
|
369
584
|
this.config.baseUrl,
|
|
370
|
-
this.parameterOptions(request)
|
|
585
|
+
this.parameterOptions(request),
|
|
586
|
+
resolveOpenAITransport(this.config, request.config?.openai),
|
|
587
|
+
this.toolDefinitions(request).length > 0,
|
|
588
|
+
!!(request.rawMessages ?? request.messages)?.some(
|
|
589
|
+
(message) => message.providerMetadata?.openai?.response
|
|
590
|
+
),
|
|
591
|
+
request.providerToolOptions?.openai?.nativeToolSearch?.enabled === true && request.providerToolOptions.openai.nativeToolSearch.useResponsesApi !== false
|
|
371
592
|
);
|
|
372
593
|
}
|
|
373
594
|
/**
|
|
@@ -400,11 +621,11 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
400
621
|
});
|
|
401
622
|
}
|
|
402
623
|
buildResponsesInput(request) {
|
|
403
|
-
const sourceMessages = request.rawMessages && request.rawMessages.length > 0 ? this.formatRawMessages(request.rawMessages) : formatMessagesForOpenAI(request.messages, void 0);
|
|
624
|
+
const sourceMessages = request.rawMessages && request.rawMessages.length > 0 ? this.formatRawMessages(request.rawMessages, true) : formatMessagesForOpenAI(request.messages, void 0, true);
|
|
404
625
|
const input = [];
|
|
405
626
|
for (const message of sourceMessages) {
|
|
406
627
|
if (message.role === "system") {
|
|
407
|
-
if (
|
|
628
|
+
if (message.content && message.content !== request.systemPrompt) {
|
|
408
629
|
input.push({
|
|
409
630
|
type: "message",
|
|
410
631
|
role: "developer",
|
|
@@ -414,6 +635,11 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
414
635
|
continue;
|
|
415
636
|
}
|
|
416
637
|
if (message.role === "assistant") {
|
|
638
|
+
const replay = getResponseReplay(message.providerMetadata);
|
|
639
|
+
if (replay) {
|
|
640
|
+
input.push(...replay);
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
417
643
|
const content = this.toResponsesContent(message.content, "assistant");
|
|
418
644
|
if (typeof content === "string" ? content : Array.isArray(content) && content.length > 0) {
|
|
419
645
|
input.push({
|
|
@@ -437,7 +663,7 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
437
663
|
input.push({
|
|
438
664
|
type: "function_call_output",
|
|
439
665
|
call_id: message.tool_call_id,
|
|
440
|
-
output: typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? null)
|
|
666
|
+
output: message.modelOutput ? toolOutputForProvider(message.modelOutput, "responses") : typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? null)
|
|
441
667
|
});
|
|
442
668
|
continue;
|
|
443
669
|
}
|
|
@@ -451,10 +677,12 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
451
677
|
return input;
|
|
452
678
|
}
|
|
453
679
|
/** Preserve Copilot screenshot attachments on every OpenAI transport. */
|
|
454
|
-
formatRawMessages(messages) {
|
|
680
|
+
formatRawMessages(messages, preserveProviderMetadata = false) {
|
|
455
681
|
return messages.map((message) => {
|
|
456
|
-
const { attachments, ...rest } = message;
|
|
457
|
-
let content = message.content;
|
|
682
|
+
const { attachments, providerMetadata, modelOutput, ...rest } = message;
|
|
683
|
+
let content = message.role === "tool" && !preserveProviderMetadata && modelOutput !== void 0 ? textToolOutput(modelOutput, "") : message.content;
|
|
684
|
+
if (preserveProviderMetadata && modelOutput)
|
|
685
|
+
rest.modelOutput = modelOutput;
|
|
458
686
|
if (message.role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.length > 0 && content === "") {
|
|
459
687
|
content = null;
|
|
460
688
|
}
|
|
@@ -467,7 +695,11 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
467
695
|
content = [...parts, ...images];
|
|
468
696
|
}
|
|
469
697
|
}
|
|
470
|
-
return {
|
|
698
|
+
return {
|
|
699
|
+
...rest,
|
|
700
|
+
content,
|
|
701
|
+
...preserveProviderMetadata && providerMetadata ? { providerMetadata } : {}
|
|
702
|
+
};
|
|
471
703
|
});
|
|
472
704
|
}
|
|
473
705
|
/**
|
|
@@ -548,14 +780,40 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
548
780
|
).map((part) => part.text) : []
|
|
549
781
|
).join("") : "";
|
|
550
782
|
const content = textFromOutput || (typeof response?.output_text === "string" ? response.output_text : "");
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
name
|
|
554
|
-
|
|
555
|
-
|
|
783
|
+
const seenCallIds = /* @__PURE__ */ new Set();
|
|
784
|
+
const toolCalls = Array.isArray(response?.output) ? response.output.filter((item) => item?.type === "function_call").map((item) => {
|
|
785
|
+
if (typeof item.call_id !== "string" || !item.call_id || typeof item.name !== "string" || !item.name || typeof item.arguments !== "string" || item.status && item.status !== "completed") {
|
|
786
|
+
throw invalidProviderResponse(
|
|
787
|
+
"Responses contains an incomplete function call"
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
if (seenCallIds.has(item.call_id))
|
|
791
|
+
throw invalidProviderResponse(
|
|
792
|
+
"Responses contains duplicate function call IDs"
|
|
793
|
+
);
|
|
794
|
+
seenCallIds.add(item.call_id);
|
|
795
|
+
let args;
|
|
796
|
+
try {
|
|
797
|
+
args = JSON.parse(item.arguments);
|
|
798
|
+
} catch {
|
|
799
|
+
throw invalidProviderResponse(
|
|
800
|
+
"Responses function arguments must be valid JSON"
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
if (!args || typeof args !== "object" || Array.isArray(args))
|
|
804
|
+
throw invalidProviderResponse(
|
|
805
|
+
"Responses function arguments must be a JSON object"
|
|
806
|
+
);
|
|
807
|
+
return {
|
|
808
|
+
id: item.call_id,
|
|
809
|
+
name: item.name,
|
|
810
|
+
args
|
|
811
|
+
};
|
|
812
|
+
}) : [];
|
|
556
813
|
return {
|
|
557
814
|
content,
|
|
558
815
|
toolCalls,
|
|
816
|
+
providerMetadata: responseMetadata(response),
|
|
559
817
|
usage: response?.usage ? {
|
|
560
818
|
promptTokens: response.usage.input_tokens ?? 0,
|
|
561
819
|
completionTokens: response.usage.output_tokens ?? 0,
|
|
@@ -566,6 +824,17 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
566
824
|
}
|
|
567
825
|
buildResponsesPayload(request, stream) {
|
|
568
826
|
const openaiToolOptions = request.providerToolOptions?.openai;
|
|
827
|
+
const options = resolveOpenAITransport(
|
|
828
|
+
this.config,
|
|
829
|
+
request.config?.openai
|
|
830
|
+
).responses;
|
|
831
|
+
const parameters = buildOpenAIParameters(
|
|
832
|
+
request.config?.model || this.model,
|
|
833
|
+
request.config?.maxTokens ?? this.config.maxTokens,
|
|
834
|
+
request.config?.temperature ?? this.config.temperature,
|
|
835
|
+
this.parameterOptions(request),
|
|
836
|
+
"responses"
|
|
837
|
+
);
|
|
569
838
|
const responsesTextFormat = toOpenAIResponsesTextFormat(
|
|
570
839
|
request.config?.responseFormat
|
|
571
840
|
);
|
|
@@ -581,13 +850,26 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
581
850
|
),
|
|
582
851
|
tool_choice: openaiToolOptions?.toolChoice,
|
|
583
852
|
parallel_tool_calls: openaiToolOptions?.parallelToolCalls,
|
|
584
|
-
...
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
853
|
+
...parameters,
|
|
854
|
+
...options?.reasoningSummary ? {
|
|
855
|
+
reasoning: {
|
|
856
|
+
...parameters.reasoning,
|
|
857
|
+
summary: options.reasoningSummary
|
|
858
|
+
}
|
|
859
|
+
} : {},
|
|
860
|
+
...options?.store !== void 0 ? { store: options.store } : {},
|
|
861
|
+
...options?.previousResponseId !== void 0 ? { previous_response_id: options.previousResponseId } : {},
|
|
862
|
+
...options?.promptCacheKey !== void 0 ? { prompt_cache_key: options.promptCacheKey } : {},
|
|
863
|
+
...options?.promptCacheOptions !== void 0 ? { prompt_cache_options: options.promptCacheOptions } : {},
|
|
864
|
+
...options?.promptCacheRetention !== void 0 ? { prompt_cache_retention: options.promptCacheRetention } : {},
|
|
865
|
+
...options?.store === false || options?.include ? {
|
|
866
|
+
include: [
|
|
867
|
+
.../* @__PURE__ */ new Set([
|
|
868
|
+
...options?.include ?? [],
|
|
869
|
+
...options?.store === false ? ["reasoning.encrypted_content"] : []
|
|
870
|
+
])
|
|
871
|
+
]
|
|
872
|
+
} : {},
|
|
591
873
|
...responsesTextFormat ? { text: { format: responsesTextFormat } } : {},
|
|
592
874
|
stream
|
|
593
875
|
};
|
|
@@ -646,6 +928,10 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
646
928
|
let usage;
|
|
647
929
|
const citations = [];
|
|
648
930
|
let completed = false;
|
|
931
|
+
let terminalResponse;
|
|
932
|
+
let responseIdentity = {};
|
|
933
|
+
const outputItems = /* @__PURE__ */ new Map();
|
|
934
|
+
let streamedText = "";
|
|
649
935
|
function* flushToolCalls() {
|
|
650
936
|
for (const call of toolCalls.values()) {
|
|
651
937
|
if (!call.name)
|
|
@@ -662,17 +948,36 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
662
948
|
for await (const event of stream) {
|
|
663
949
|
throwIfAborted(request.signal);
|
|
664
950
|
logProviderPayload("openai", "stream chunk", event, request.debug);
|
|
951
|
+
if (completed)
|
|
952
|
+
throw new Error("Responses stream emitted an event after completion");
|
|
665
953
|
const index = event?.output_index ?? 0;
|
|
666
954
|
switch (event?.type) {
|
|
955
|
+
case "response.created":
|
|
956
|
+
responseIdentity = {
|
|
957
|
+
id: event.response?.id,
|
|
958
|
+
model: event.response?.model
|
|
959
|
+
};
|
|
960
|
+
break;
|
|
961
|
+
case "response.output_item.done":
|
|
962
|
+
if (event.item) outputItems.set(index, event.item);
|
|
963
|
+
if (event.item?.type === "function_call") {
|
|
964
|
+
toolCalls.set(index, {
|
|
965
|
+
id: event.item.call_id,
|
|
966
|
+
name: event.item.name,
|
|
967
|
+
arguments: event.item.arguments
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
break;
|
|
667
971
|
case "response.output_text.delta":
|
|
668
972
|
if (event.delta) {
|
|
973
|
+
streamedText += event.delta;
|
|
669
974
|
yield { type: "message:delta", content: event.delta };
|
|
670
975
|
}
|
|
671
976
|
break;
|
|
672
977
|
case "response.output_item.added":
|
|
673
978
|
if (event.item?.type === "function_call") {
|
|
674
979
|
toolCalls.set(index, {
|
|
675
|
-
id: event.item.call_id ??
|
|
980
|
+
id: event.item.call_id ?? "",
|
|
676
981
|
name: event.item.name ?? "",
|
|
677
982
|
arguments: event.item.arguments ?? ""
|
|
678
983
|
});
|
|
@@ -691,7 +996,10 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
691
996
|
break;
|
|
692
997
|
}
|
|
693
998
|
case "response.completed": {
|
|
999
|
+
if (event.response?.status && event.response.status !== "completed")
|
|
1000
|
+
throw new Error(`Responses stream ${event.response.status}`);
|
|
694
1001
|
completed = true;
|
|
1002
|
+
terminalResponse = { ...responseIdentity, ...event.response };
|
|
695
1003
|
const found = this.collectResponsesCitations(
|
|
696
1004
|
event.response?.output,
|
|
697
1005
|
citations.length
|
|
@@ -728,12 +1036,66 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
728
1036
|
throwIfAborted(request.signal);
|
|
729
1037
|
if (!completed)
|
|
730
1038
|
throw new Error("Responses stream ended without a completed response");
|
|
1039
|
+
const output = terminalResponse.output?.length ? terminalResponse.output : [...outputItems.entries()].sort(([a], [b]) => a - b).map(([, item]) => item);
|
|
1040
|
+
if (output.some((item) => item.type === "function_call"))
|
|
1041
|
+
toolCalls.clear();
|
|
1042
|
+
for (const [index, item] of output.entries()) {
|
|
1043
|
+
if (item.type === "function_call") {
|
|
1044
|
+
toolCalls.set(index, {
|
|
1045
|
+
id: item.call_id,
|
|
1046
|
+
name: item.name,
|
|
1047
|
+
arguments: item.arguments
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
const uniqueCalls = /* @__PURE__ */ new Map();
|
|
1052
|
+
for (const call of toolCalls.values()) {
|
|
1053
|
+
if (!call.id || !call.name)
|
|
1054
|
+
throw invalidProviderResponse(
|
|
1055
|
+
"Responses stream contains an incomplete tool call"
|
|
1056
|
+
);
|
|
1057
|
+
uniqueCalls.set(call.id, call);
|
|
1058
|
+
}
|
|
1059
|
+
toolCalls.clear();
|
|
1060
|
+
[...uniqueCalls.values()].forEach(
|
|
1061
|
+
(call, index) => toolCalls.set(index, call)
|
|
1062
|
+
);
|
|
1063
|
+
if (!output.length && toolCalls.size) {
|
|
1064
|
+
for (const call of toolCalls.values())
|
|
1065
|
+
output.push({
|
|
1066
|
+
type: "function_call",
|
|
1067
|
+
call_id: call.id,
|
|
1068
|
+
name: call.name,
|
|
1069
|
+
arguments: call.arguments
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
if (!output.length && streamedText)
|
|
1073
|
+
output.push({
|
|
1074
|
+
type: "message",
|
|
1075
|
+
role: "assistant",
|
|
1076
|
+
content: [{ type: "output_text", text: streamedText, annotations: [] }]
|
|
1077
|
+
});
|
|
1078
|
+
terminalResponse.output = output;
|
|
1079
|
+
const finalText = this.parseResponsesResult(terminalResponse).content;
|
|
1080
|
+
if (finalText.startsWith(streamedText) && finalText.length > streamedText.length) {
|
|
1081
|
+
yield {
|
|
1082
|
+
type: "message:delta",
|
|
1083
|
+
content: finalText.slice(streamedText.length)
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
731
1086
|
yield* flushToolCalls();
|
|
732
1087
|
if (citations.length > 0) {
|
|
733
1088
|
yield { type: "citation", citations: deduplicateCitations(citations) };
|
|
734
1089
|
}
|
|
735
|
-
yield {
|
|
736
|
-
|
|
1090
|
+
yield {
|
|
1091
|
+
type: "message:end",
|
|
1092
|
+
providerMetadata: responseMetadata(terminalResponse)
|
|
1093
|
+
};
|
|
1094
|
+
yield {
|
|
1095
|
+
type: "done",
|
|
1096
|
+
usage,
|
|
1097
|
+
providerMetadata: responseMetadata(terminalResponse)
|
|
1098
|
+
};
|
|
737
1099
|
}
|
|
738
1100
|
async *stream(request) {
|
|
739
1101
|
throwIfAborted(request.signal);
|
|
@@ -1101,7 +1463,16 @@ function openai(modelId, options = {}) {
|
|
|
1101
1463
|
};
|
|
1102
1464
|
}
|
|
1103
1465
|
function useResponses(params) {
|
|
1104
|
-
return
|
|
1466
|
+
return useOpenAIResponses(
|
|
1467
|
+
modelId,
|
|
1468
|
+
baseURL,
|
|
1469
|
+
parameterOptions(params),
|
|
1470
|
+
resolveOpenAITransport(options, params.openai),
|
|
1471
|
+
!!params.tools?.length,
|
|
1472
|
+
params.messages.some(
|
|
1473
|
+
(message) => message.role === "assistant" && !!message.providerMetadata?.openai?.response
|
|
1474
|
+
)
|
|
1475
|
+
);
|
|
1105
1476
|
}
|
|
1106
1477
|
const responsesAdapter = new OpenAIAdapter({
|
|
1107
1478
|
...options,
|
|
@@ -1112,7 +1483,7 @@ function openai(modelId, options = {}) {
|
|
|
1112
1483
|
function responsesRequest(params) {
|
|
1113
1484
|
return {
|
|
1114
1485
|
messages: [],
|
|
1115
|
-
rawMessages: formatMessagesForOpenAI2(params.messages),
|
|
1486
|
+
rawMessages: formatMessagesForOpenAI2(params.messages, true),
|
|
1116
1487
|
toolDefinitions: params.tools?.map((raw) => {
|
|
1117
1488
|
const tool = raw;
|
|
1118
1489
|
if (tool.type !== "function")
|
|
@@ -1127,6 +1498,10 @@ function openai(modelId, options = {}) {
|
|
|
1127
1498
|
};
|
|
1128
1499
|
}),
|
|
1129
1500
|
config: {
|
|
1501
|
+
openai: {
|
|
1502
|
+
...resolveOpenAITransport(options, params.openai),
|
|
1503
|
+
api: "responses"
|
|
1504
|
+
},
|
|
1130
1505
|
temperature: params.temperature,
|
|
1131
1506
|
maxTokens: params.maxTokens,
|
|
1132
1507
|
responseFormat: params.responseFormat,
|
|
@@ -1158,13 +1533,14 @@ function openai(modelId, options = {}) {
|
|
|
1158
1533
|
return {
|
|
1159
1534
|
text: result.content,
|
|
1160
1535
|
toolCalls: result.toolCalls,
|
|
1161
|
-
finishReason: result.toolCalls.length ? "tool-calls" : "stop",
|
|
1536
|
+
finishReason: result.toolCalls.length ? "tool-calls" : result.providerMetadata?.openai?.response?.refusals.length ? "content-filter" : "stop",
|
|
1162
1537
|
usage: result.usage ?? {
|
|
1163
1538
|
promptTokens: 0,
|
|
1164
1539
|
completionTokens: 0,
|
|
1165
1540
|
totalTokens: 0
|
|
1166
1541
|
},
|
|
1167
|
-
rawResponse: result.rawResponse
|
|
1542
|
+
rawResponse: result.rawResponse,
|
|
1543
|
+
providerMetadata: result.providerMetadata
|
|
1168
1544
|
};
|
|
1169
1545
|
}
|
|
1170
1546
|
const client2 = await getClient();
|
|
@@ -1246,7 +1622,8 @@ function openai(modelId, options = {}) {
|
|
|
1246
1622
|
case "done":
|
|
1247
1623
|
yield {
|
|
1248
1624
|
type: "finish",
|
|
1249
|
-
finishReason: hasTools ? "tool-calls" : "stop",
|
|
1625
|
+
finishReason: hasTools ? "tool-calls" : event.providerMetadata?.openai?.response?.refusals.length ? "content-filter" : "stop",
|
|
1626
|
+
providerMetadata: event.providerMetadata,
|
|
1250
1627
|
usage: {
|
|
1251
1628
|
promptTokens: event.usage?.prompt_tokens ?? 0,
|
|
1252
1629
|
completionTokens: event.usage?.completion_tokens ?? 0,
|
|
@@ -1342,7 +1719,7 @@ function mapFinishReason(reason) {
|
|
|
1342
1719
|
return "unknown";
|
|
1343
1720
|
}
|
|
1344
1721
|
}
|
|
1345
|
-
function formatMessagesForOpenAI2(messages) {
|
|
1722
|
+
function formatMessagesForOpenAI2(messages, responses = false) {
|
|
1346
1723
|
return messages.map((msg) => {
|
|
1347
1724
|
switch (msg.role) {
|
|
1348
1725
|
case "system":
|
|
@@ -1359,16 +1736,28 @@ function formatMessagesForOpenAI2(messages) {
|
|
|
1359
1736
|
}
|
|
1360
1737
|
if (part.type === "image") {
|
|
1361
1738
|
const imageData = typeof part.image === "string" ? part.image : Buffer.from(part.image).toString("base64");
|
|
1362
|
-
const url =
|
|
1739
|
+
const url = /^(data:|https?:\/\/)/.test(imageData) ? imageData : `data:${part.mimeType ?? "image/png"};base64,${imageData}`;
|
|
1363
1740
|
return { type: "image_url", image_url: { url, detail: "auto" } };
|
|
1364
1741
|
}
|
|
1365
|
-
|
|
1742
|
+
if (part.type === "file") {
|
|
1743
|
+
if (!responses)
|
|
1744
|
+
throw new Error(
|
|
1745
|
+
"File inputs require the Responses API on this OpenAI path"
|
|
1746
|
+
);
|
|
1747
|
+
return /^https?:\/\//.test(part.data) ? { type: "input_file", file_url: part.data } : {
|
|
1748
|
+
type: "input_file",
|
|
1749
|
+
filename: part.filename ?? "document.pdf",
|
|
1750
|
+
file_data: part.data.startsWith("data:") ? part.data : `data:${part.mimeType};base64,${part.data}`
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
throw new Error("Unsupported OpenAI message content");
|
|
1366
1754
|
})
|
|
1367
1755
|
};
|
|
1368
1756
|
case "assistant":
|
|
1369
1757
|
const assistantMsg = {
|
|
1370
1758
|
role: "assistant",
|
|
1371
|
-
content: msg.content
|
|
1759
|
+
content: msg.content,
|
|
1760
|
+
...responses && msg.providerMetadata ? { providerMetadata: msg.providerMetadata } : {}
|
|
1372
1761
|
};
|
|
1373
1762
|
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
1374
1763
|
assistantMsg.tool_calls = msg.toolCalls.map((tc) => ({
|
|
@@ -1385,7 +1774,8 @@ function formatMessagesForOpenAI2(messages) {
|
|
|
1385
1774
|
return {
|
|
1386
1775
|
role: "tool",
|
|
1387
1776
|
tool_call_id: msg.toolCallId,
|
|
1388
|
-
content: msg.content
|
|
1777
|
+
content: responses ? msg.content : textToolOutput(msg.modelOutput, msg.content),
|
|
1778
|
+
...responses && msg.modelOutput ? { modelOutput: msg.modelOutput } : {}
|
|
1389
1779
|
};
|
|
1390
1780
|
default:
|
|
1391
1781
|
return msg;
|
|
@@ -1409,6 +1799,394 @@ function createCallableProvider(providerFn, properties) {
|
|
|
1409
1799
|
return providerFn;
|
|
1410
1800
|
}
|
|
1411
1801
|
|
|
1802
|
+
// src/core/audio.ts
|
|
1803
|
+
var audioErrorMarker = /* @__PURE__ */ Symbol.for("@yourgpt/llm-sdk/AudioAPIError");
|
|
1804
|
+
var _a;
|
|
1805
|
+
var AudioAPIError = class extends Error {
|
|
1806
|
+
constructor(message, code, status, requestId) {
|
|
1807
|
+
super(message);
|
|
1808
|
+
this.code = code;
|
|
1809
|
+
this.status = status;
|
|
1810
|
+
this.requestId = requestId;
|
|
1811
|
+
this[_a] = true;
|
|
1812
|
+
this.name = "AudioAPIError";
|
|
1813
|
+
}
|
|
1814
|
+
static [(_a = audioErrorMarker, Symbol.hasInstance)](value) {
|
|
1815
|
+
return !!value && typeof value === "object" && value[audioErrorMarker] === true;
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
|
|
1819
|
+
// src/providers/openai/audio.ts
|
|
1820
|
+
var formats = {
|
|
1821
|
+
mp3: "audio/mpeg",
|
|
1822
|
+
opus: "audio/ogg",
|
|
1823
|
+
aac: "audio/aac",
|
|
1824
|
+
flac: "audio/flac",
|
|
1825
|
+
wav: "audio/wav",
|
|
1826
|
+
pcm: "audio/pcm"
|
|
1827
|
+
};
|
|
1828
|
+
var extensions = {
|
|
1829
|
+
"audio/mpeg": "mp3",
|
|
1830
|
+
"audio/mp3": "mp3",
|
|
1831
|
+
"audio/mp4": "m4a",
|
|
1832
|
+
"video/mp4": "mp4",
|
|
1833
|
+
"audio/wav": "wav",
|
|
1834
|
+
"audio/x-wav": "wav",
|
|
1835
|
+
"audio/webm": "webm",
|
|
1836
|
+
"video/webm": "webm",
|
|
1837
|
+
"audio/ogg": "ogg",
|
|
1838
|
+
"audio/flac": "flac",
|
|
1839
|
+
"audio/x-flac": "flac"
|
|
1840
|
+
};
|
|
1841
|
+
var MAX_AUDIO = 25e6;
|
|
1842
|
+
function hasAudioHeader(data, format) {
|
|
1843
|
+
const head = Buffer.from(data.subarray(0, 12)).toString("latin1");
|
|
1844
|
+
if (format === "wav")
|
|
1845
|
+
return head.startsWith("RIFF") && head.slice(8, 12) === "WAVE";
|
|
1846
|
+
if (format === "mp3" || format === "mpeg" || format === "mpga")
|
|
1847
|
+
return head.startsWith("ID3") || data[0] === 255 && (data[1] & 224) === 224;
|
|
1848
|
+
if (format === "opus" || format === "ogg") return head.startsWith("OggS");
|
|
1849
|
+
if (format === "flac") return head.startsWith("fLaC");
|
|
1850
|
+
if (format === "aac") return data[0] === 255 && (data[1] & 246) === 240;
|
|
1851
|
+
if (format === "webm")
|
|
1852
|
+
return data[0] === 26 && data[1] === 69 && data[2] === 223 && data[3] === 163;
|
|
1853
|
+
if (format === "m4a" || format === "mp4") return head.slice(4, 8) === "ftyp";
|
|
1854
|
+
return format === "pcm" && data.length > 0 && data.length % 2 === 0;
|
|
1855
|
+
}
|
|
1856
|
+
function invalid(message) {
|
|
1857
|
+
throw new AudioAPIError(message, "invalid_input");
|
|
1858
|
+
}
|
|
1859
|
+
function unsupported(message) {
|
|
1860
|
+
throw new AudioAPIError(message, "unsupported_option");
|
|
1861
|
+
}
|
|
1862
|
+
function providerOptions(value, allowed) {
|
|
1863
|
+
for (const [provider, options] of Object.entries(value ?? {})) {
|
|
1864
|
+
if (provider !== "openai")
|
|
1865
|
+
unsupported(
|
|
1866
|
+
`Provider options for ${provider} cannot be used with OpenAI audio`
|
|
1867
|
+
);
|
|
1868
|
+
for (const key of Object.keys(options))
|
|
1869
|
+
if (!allowed.includes(key))
|
|
1870
|
+
unsupported(`Unsupported OpenAI audio option: ${key}`);
|
|
1871
|
+
}
|
|
1872
|
+
return value?.openai ?? {};
|
|
1873
|
+
}
|
|
1874
|
+
async function readBody(response, limit) {
|
|
1875
|
+
if (Number(response.headers.get("content-length")) > limit) {
|
|
1876
|
+
await response.body?.cancel();
|
|
1877
|
+
throw new AudioAPIError(
|
|
1878
|
+
"Audio API response exceeds the size limit",
|
|
1879
|
+
"invalid_response"
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
const reader = response.body?.getReader();
|
|
1883
|
+
if (!reader) return new Uint8Array();
|
|
1884
|
+
const chunks = [];
|
|
1885
|
+
let length = 0;
|
|
1886
|
+
try {
|
|
1887
|
+
while (true) {
|
|
1888
|
+
const { done, value } = await reader.read();
|
|
1889
|
+
if (done) break;
|
|
1890
|
+
length += value.length;
|
|
1891
|
+
if (length > limit) {
|
|
1892
|
+
await reader.cancel();
|
|
1893
|
+
throw new AudioAPIError(
|
|
1894
|
+
"Audio API response exceeds the size limit",
|
|
1895
|
+
"invalid_response"
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
chunks.push(value);
|
|
1899
|
+
}
|
|
1900
|
+
} finally {
|
|
1901
|
+
reader.releaseLock();
|
|
1902
|
+
}
|
|
1903
|
+
const data = new Uint8Array(length);
|
|
1904
|
+
let offset = 0;
|
|
1905
|
+
for (const chunk of chunks) {
|
|
1906
|
+
data.set(chunk, offset);
|
|
1907
|
+
offset += chunk.length;
|
|
1908
|
+
}
|
|
1909
|
+
return data;
|
|
1910
|
+
}
|
|
1911
|
+
function transport(config) {
|
|
1912
|
+
return async (path, body, signal) => {
|
|
1913
|
+
signal?.throwIfAborted();
|
|
1914
|
+
const apiKey = config.apiKey ?? process.env.OPENAI_API_KEY;
|
|
1915
|
+
if (!apiKey) invalid("OpenAI audio requires an API key on the server");
|
|
1916
|
+
const headers = {
|
|
1917
|
+
Authorization: `Bearer ${apiKey}`
|
|
1918
|
+
};
|
|
1919
|
+
if (typeof body === "string") headers["Content-Type"] = "application/json";
|
|
1920
|
+
if (config.organization)
|
|
1921
|
+
headers["OpenAI-Organization"] = config.organization;
|
|
1922
|
+
if (config.project) headers["OpenAI-Project"] = config.project;
|
|
1923
|
+
let response;
|
|
1924
|
+
try {
|
|
1925
|
+
response = await (config.fetch ?? globalThis.fetch)(
|
|
1926
|
+
`${(config.baseUrl ?? "https://api.openai.com/v1").replace(/\/$/, "")}${path}`,
|
|
1927
|
+
{ method: "POST", headers, body, signal, redirect: "error" }
|
|
1928
|
+
);
|
|
1929
|
+
} catch {
|
|
1930
|
+
signal?.throwIfAborted();
|
|
1931
|
+
throw new AudioAPIError("OpenAI audio request failed", "network_error");
|
|
1932
|
+
}
|
|
1933
|
+
if (!response.ok) {
|
|
1934
|
+
await response.body?.cancel();
|
|
1935
|
+
throw new AudioAPIError(
|
|
1936
|
+
`OpenAI audio request failed (HTTP ${response.status})`,
|
|
1937
|
+
"provider_error",
|
|
1938
|
+
response.status,
|
|
1939
|
+
response.headers.get("x-request-id") ?? void 0
|
|
1940
|
+
);
|
|
1941
|
+
}
|
|
1942
|
+
return response;
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
function openaiSpeech(modelId = "gpt-4o-mini-tts", config = {}) {
|
|
1946
|
+
const request = transport(config);
|
|
1947
|
+
return {
|
|
1948
|
+
provider: "openai",
|
|
1949
|
+
modelId,
|
|
1950
|
+
async doGenerate(options) {
|
|
1951
|
+
const {
|
|
1952
|
+
text,
|
|
1953
|
+
voice = "alloy",
|
|
1954
|
+
outputFormat = "mp3",
|
|
1955
|
+
speed,
|
|
1956
|
+
instructions,
|
|
1957
|
+
signal
|
|
1958
|
+
} = options;
|
|
1959
|
+
signal?.throwIfAborted();
|
|
1960
|
+
providerOptions(options.providerOptions, []);
|
|
1961
|
+
if (typeof text !== "string" || !text.trim() || text.length > 4096)
|
|
1962
|
+
invalid("Speech text must contain 1\u20134096 characters");
|
|
1963
|
+
if (!Object.prototype.hasOwnProperty.call(formats, outputFormat))
|
|
1964
|
+
unsupported("Unsupported OpenAI speech output format");
|
|
1965
|
+
if (typeof voice !== "string" || !voice.trim())
|
|
1966
|
+
invalid("Speech voice must not be empty");
|
|
1967
|
+
if (speed !== void 0 && (!Number.isFinite(speed) || speed < 0.25 || speed > 4))
|
|
1968
|
+
invalid("Speech speed must be between 0.25 and 4");
|
|
1969
|
+
if (instructions !== void 0) {
|
|
1970
|
+
if (typeof instructions !== "string" || instructions.length > 4096)
|
|
1971
|
+
invalid(
|
|
1972
|
+
"Speech instructions must be a string of at most 4096 characters"
|
|
1973
|
+
);
|
|
1974
|
+
if (modelId === "tts-1" || modelId === "tts-1-hd")
|
|
1975
|
+
unsupported("Instructions are not supported by tts-1 or tts-1-hd");
|
|
1976
|
+
}
|
|
1977
|
+
const response = await request(
|
|
1978
|
+
"/audio/speech",
|
|
1979
|
+
JSON.stringify({
|
|
1980
|
+
model: modelId,
|
|
1981
|
+
input: text,
|
|
1982
|
+
voice,
|
|
1983
|
+
response_format: outputFormat,
|
|
1984
|
+
speed,
|
|
1985
|
+
instructions
|
|
1986
|
+
}),
|
|
1987
|
+
signal
|
|
1988
|
+
);
|
|
1989
|
+
const mediaType = response.headers.get("content-type")?.split(";")[0].trim();
|
|
1990
|
+
if (mediaType && !mediaType.startsWith("audio/") && mediaType !== "application/octet-stream") {
|
|
1991
|
+
await response.body?.cancel();
|
|
1992
|
+
throw new AudioAPIError(
|
|
1993
|
+
"Speech endpoint returned a non-audio response",
|
|
1994
|
+
"invalid_response"
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
const data = await readBody(response, MAX_AUDIO);
|
|
1998
|
+
signal?.throwIfAborted();
|
|
1999
|
+
if (!data.length || !hasAudioHeader(data, outputFormat))
|
|
2000
|
+
throw new AudioAPIError(
|
|
2001
|
+
"Speech endpoint returned empty or invalid audio",
|
|
2002
|
+
"invalid_response"
|
|
2003
|
+
);
|
|
2004
|
+
return {
|
|
2005
|
+
audio: {
|
|
2006
|
+
uint8Array: data,
|
|
2007
|
+
get base64() {
|
|
2008
|
+
return Buffer.from(data).toString("base64");
|
|
2009
|
+
},
|
|
2010
|
+
mediaType: formats[outputFormat],
|
|
2011
|
+
format: outputFormat
|
|
2012
|
+
},
|
|
2013
|
+
warnings: [],
|
|
2014
|
+
response: {
|
|
2015
|
+
provider: "openai",
|
|
2016
|
+
modelId,
|
|
2017
|
+
requestId: response.headers.get("x-request-id") ?? void 0,
|
|
2018
|
+
status: response.status
|
|
2019
|
+
}
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
async function audioBytes(options) {
|
|
2025
|
+
const blob = typeof Blob !== "undefined" && options.audio instanceof Blob ? options.audio : void 0;
|
|
2026
|
+
if (blob && blob.size > MAX_AUDIO)
|
|
2027
|
+
invalid("Transcription file exceeds 25 MB");
|
|
2028
|
+
const declared = options.mediaType?.split(";")[0].trim();
|
|
2029
|
+
const blobType = blob?.type.split(";")[0].trim();
|
|
2030
|
+
if (declared && blobType && declared !== blobType)
|
|
2031
|
+
invalid("Transcription mediaType does not match the Blob type");
|
|
2032
|
+
const mediaType = declared || blobType || "";
|
|
2033
|
+
const extension = extensions[mediaType];
|
|
2034
|
+
if (!extension)
|
|
2035
|
+
unsupported(
|
|
2036
|
+
"Unsupported transcription media type; provide an encoded audio file"
|
|
2037
|
+
);
|
|
2038
|
+
let bytes;
|
|
2039
|
+
if (blob) bytes = new Uint8Array(await blob.arrayBuffer());
|
|
2040
|
+
else if (options.audio instanceof Uint8Array) bytes = options.audio;
|
|
2041
|
+
else if (options.audio instanceof ArrayBuffer)
|
|
2042
|
+
bytes = new Uint8Array(options.audio);
|
|
2043
|
+
else if (typeof options.audio === "string") {
|
|
2044
|
+
const base64 = options.audio;
|
|
2045
|
+
if (base64.length > MAX_AUDIO * 4 / 3 + 4)
|
|
2046
|
+
invalid("Transcription file exceeds 25 MB");
|
|
2047
|
+
if (!base64 || base64.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(base64))
|
|
2048
|
+
invalid(
|
|
2049
|
+
"Audio must be bytes, a Blob, or valid base64; URLs are not fetched"
|
|
2050
|
+
);
|
|
2051
|
+
bytes = Buffer.from(base64, "base64");
|
|
2052
|
+
if (Buffer.from(bytes).toString("base64") !== base64)
|
|
2053
|
+
invalid("Invalid audio base64");
|
|
2054
|
+
} else invalid("Audio must be bytes, a Blob, or base64");
|
|
2055
|
+
if (!bytes.length || bytes.length > MAX_AUDIO)
|
|
2056
|
+
invalid("Transcription file must contain 1 byte to 25 MB");
|
|
2057
|
+
if (!hasAudioHeader(bytes, extension))
|
|
2058
|
+
invalid("Audio header does not match the declared media type");
|
|
2059
|
+
return { bytes, mediaType, extension };
|
|
2060
|
+
}
|
|
2061
|
+
function openaiTranscription(modelId = "gpt-4o-mini-transcribe", config = {}) {
|
|
2062
|
+
const request = transport(config);
|
|
2063
|
+
return {
|
|
2064
|
+
provider: "openai",
|
|
2065
|
+
modelId,
|
|
2066
|
+
async doTranscribe(options) {
|
|
2067
|
+
options.signal?.throwIfAborted();
|
|
2068
|
+
if (modelId.includes("realtime"))
|
|
2069
|
+
unsupported(
|
|
2070
|
+
"Realtime models require a realtime session; use a file transcription model"
|
|
2071
|
+
);
|
|
2072
|
+
const opts = providerOptions(options.providerOptions, [
|
|
2073
|
+
"timestampGranularities",
|
|
2074
|
+
"includeLogprobs",
|
|
2075
|
+
"chunkingStrategy"
|
|
2076
|
+
]);
|
|
2077
|
+
const timestamps = opts.timestampGranularities;
|
|
2078
|
+
if (timestamps !== void 0 && (!Array.isArray(timestamps) || !timestamps.length || timestamps.some((x) => x !== "word" && x !== "segment")))
|
|
2079
|
+
invalid("timestampGranularities must contain word or segment");
|
|
2080
|
+
if (timestamps !== void 0 && modelId !== "whisper-1")
|
|
2081
|
+
unsupported("Timestamp granularities require whisper-1");
|
|
2082
|
+
if (opts.includeLogprobs !== void 0 && typeof opts.includeLogprobs !== "boolean")
|
|
2083
|
+
invalid("includeLogprobs must be boolean");
|
|
2084
|
+
if (opts.includeLogprobs && !/^gpt-4o-(mini-)?transcribe(?:-\d{4}-\d{2}-\d{2})?$/.test(modelId))
|
|
2085
|
+
unsupported(
|
|
2086
|
+
"Log probabilities require a supported gpt-4o transcription model"
|
|
2087
|
+
);
|
|
2088
|
+
const diarize = modelId === "gpt-4o-transcribe-diarize";
|
|
2089
|
+
if (opts.chunkingStrategy !== void 0 && opts.chunkingStrategy !== "auto")
|
|
2090
|
+
unsupported("Only automatic audio chunking is supported");
|
|
2091
|
+
if (diarize && options.prompt !== void 0)
|
|
2092
|
+
unsupported("Prompts are not supported by the diarization model");
|
|
2093
|
+
if (options.language !== void 0 && !/^[a-z]{2}$/.test(options.language))
|
|
2094
|
+
invalid("language must be a two-letter ISO-639-1 code");
|
|
2095
|
+
if (options.prompt !== void 0 && typeof options.prompt !== "string")
|
|
2096
|
+
invalid("Transcription prompt must be a string");
|
|
2097
|
+
const { bytes, mediaType, extension } = await audioBytes(options);
|
|
2098
|
+
options.signal?.throwIfAborted();
|
|
2099
|
+
const fileName = options.filename ?? `audio.${extension}`;
|
|
2100
|
+
if (!/^[^/\\\r\n\x00]+\.[a-z0-9]+$/i.test(fileName))
|
|
2101
|
+
invalid("Transcription filename must be a filename with an extension");
|
|
2102
|
+
const allowedExt = mediaType === "audio/mpeg" ? ["mp3", "mpeg", "mpga"] : mediaType === "audio/mp4" ? ["m4a", "mp4"] : [extension];
|
|
2103
|
+
if (!allowedExt.includes(fileName.split(".").at(-1).toLowerCase()))
|
|
2104
|
+
invalid("Transcription filename extension does not match mediaType");
|
|
2105
|
+
const form = new FormData();
|
|
2106
|
+
form.append(
|
|
2107
|
+
"file",
|
|
2108
|
+
new Blob([new Uint8Array(bytes)], { type: mediaType }),
|
|
2109
|
+
fileName
|
|
2110
|
+
);
|
|
2111
|
+
form.append("model", modelId);
|
|
2112
|
+
form.append(
|
|
2113
|
+
"response_format",
|
|
2114
|
+
diarize ? "diarized_json" : timestamps ? "verbose_json" : "json"
|
|
2115
|
+
);
|
|
2116
|
+
if (options.language) form.append("language", options.language);
|
|
2117
|
+
if (options.prompt) form.append("prompt", options.prompt);
|
|
2118
|
+
if (timestamps)
|
|
2119
|
+
for (const value2 of timestamps)
|
|
2120
|
+
form.append("timestamp_granularities[]", value2);
|
|
2121
|
+
if (opts.includeLogprobs) form.append("include[]", "logprobs");
|
|
2122
|
+
if (diarize || opts.chunkingStrategy)
|
|
2123
|
+
form.append("chunking_strategy", "auto");
|
|
2124
|
+
const response = await request(
|
|
2125
|
+
"/audio/transcriptions",
|
|
2126
|
+
form,
|
|
2127
|
+
options.signal
|
|
2128
|
+
);
|
|
2129
|
+
let value;
|
|
2130
|
+
try {
|
|
2131
|
+
value = JSON.parse(
|
|
2132
|
+
Buffer.from(await readBody(response, 5 * 1024 * 1024)).toString(
|
|
2133
|
+
"utf8"
|
|
2134
|
+
)
|
|
2135
|
+
);
|
|
2136
|
+
} catch (error) {
|
|
2137
|
+
options.signal?.throwIfAborted();
|
|
2138
|
+
if (error instanceof AudioAPIError) throw error;
|
|
2139
|
+
throw new AudioAPIError(
|
|
2140
|
+
"Transcription endpoint returned invalid JSON",
|
|
2141
|
+
"invalid_response"
|
|
2142
|
+
);
|
|
2143
|
+
}
|
|
2144
|
+
options.signal?.throwIfAborted();
|
|
2145
|
+
if (!value || typeof value.text !== "string")
|
|
2146
|
+
throw new AudioAPIError(
|
|
2147
|
+
"Transcription response is missing text",
|
|
2148
|
+
"invalid_response"
|
|
2149
|
+
);
|
|
2150
|
+
function timed(items, word = false) {
|
|
2151
|
+
if (items === void 0) return void 0;
|
|
2152
|
+
if (!Array.isArray(items) || items.some(
|
|
2153
|
+
(x) => !x || typeof x[word ? "word" : "text"] !== "string" || !Number.isFinite(x.start) || !Number.isFinite(x.end) || x.start < 0 || x.end < x.start
|
|
2154
|
+
))
|
|
2155
|
+
throw new AudioAPIError(
|
|
2156
|
+
"Invalid transcription timestamps",
|
|
2157
|
+
"invalid_response"
|
|
2158
|
+
);
|
|
2159
|
+
return items.map(
|
|
2160
|
+
(x) => word ? { word: x.word, start: x.start, end: x.end } : {
|
|
2161
|
+
text: x.text,
|
|
2162
|
+
start: x.start,
|
|
2163
|
+
end: x.end,
|
|
2164
|
+
...typeof x.speaker === "string" ? { speaker: x.speaker } : {}
|
|
2165
|
+
}
|
|
2166
|
+
);
|
|
2167
|
+
}
|
|
2168
|
+
return {
|
|
2169
|
+
text: value.text,
|
|
2170
|
+
language: typeof value.language === "string" ? value.language : void 0,
|
|
2171
|
+
durationSeconds: typeof value.duration === "number" ? value.duration : void 0,
|
|
2172
|
+
segments: timed(value.segments),
|
|
2173
|
+
words: timed(value.words, true),
|
|
2174
|
+
usage: value.usage,
|
|
2175
|
+
providerMetadata: {
|
|
2176
|
+
openai: { ...value.logprobs ? { logprobs: value.logprobs } : {} }
|
|
2177
|
+
},
|
|
2178
|
+
warnings: [],
|
|
2179
|
+
response: {
|
|
2180
|
+
provider: "openai",
|
|
2181
|
+
modelId,
|
|
2182
|
+
status: response.status,
|
|
2183
|
+
requestId: response.headers.get("x-request-id") ?? void 0
|
|
2184
|
+
}
|
|
2185
|
+
};
|
|
2186
|
+
}
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
|
|
1412
2190
|
// src/providers/openai/index.ts
|
|
1413
2191
|
var OPENAI_MODELS2 = {
|
|
1414
2192
|
// GPT-4o series
|
|
@@ -1525,6 +2303,8 @@ function createOpenAI(config = {}) {
|
|
|
1525
2303
|
model: modelId,
|
|
1526
2304
|
baseUrl: config.baseUrl,
|
|
1527
2305
|
reasoningEffort: config.reasoningEffort,
|
|
2306
|
+
api: config.api,
|
|
2307
|
+
responses: config.responses,
|
|
1528
2308
|
parameterPolicy: config.parameterPolicy,
|
|
1529
2309
|
onWarning: config.onWarning
|
|
1530
2310
|
});
|
|
@@ -1558,5 +2338,7 @@ exports.createOpenAI = createOpenAI;
|
|
|
1558
2338
|
exports.createOpenAIModel = openai;
|
|
1559
2339
|
exports.createOpenAIProvider = createOpenAIProvider;
|
|
1560
2340
|
exports.openai = openai;
|
|
2341
|
+
exports.openaiSpeech = openaiSpeech;
|
|
2342
|
+
exports.openaiTranscription = openaiTranscription;
|
|
1561
2343
|
//# sourceMappingURL=index.js.map
|
|
1562
2344
|
//# sourceMappingURL=index.js.map
|