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