@veryfront/ext-llm-openai 0.1.1186 → 0.1.1189
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 +49 -2
- package/esm/_dnt.polyfills.d.ts +12 -0
- package/esm/_dnt.polyfills.d.ts.map +1 -0
- package/esm/_dnt.polyfills.js +15 -0
- package/esm/index.d.ts +1 -0
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +7 -1
- package/esm/openai-chat-request-builder.d.ts +5 -43
- package/esm/openai-chat-request-builder.d.ts.map +1 -1
- package/esm/openai-chat-request-builder.js +18 -4
- package/esm/openai-chat-stream.d.ts +8 -1
- package/esm/openai-chat-stream.d.ts.map +1 -1
- package/esm/openai-chat-stream.js +286 -104
- package/esm/openai-provider-options.d.ts +6 -0
- package/esm/openai-provider-options.d.ts.map +1 -0
- package/esm/openai-provider-options.js +14 -0
- package/esm/openai-provider.d.ts +6 -5
- package/esm/openai-provider.d.ts.map +1 -1
- package/esm/openai-provider.js +579 -129
- package/esm/openai-reasoning-models.d.ts +3 -6
- package/esm/openai-reasoning-models.d.ts.map +1 -1
- package/esm/openai-responses-request-builder.d.ts.map +1 -1
- package/esm/openai-responses-request-builder.js +240 -19
- package/esm/openai-responses-stream.d.ts +12 -1
- package/esm/openai-responses-stream.d.ts.map +1 -1
- package/esm/openai-responses-stream.js +1053 -113
- package/esm/openai-sse-buffer.d.ts +8 -0
- package/esm/openai-sse-buffer.d.ts.map +1 -0
- package/esm/openai-sse-buffer.js +47 -0
- package/esm/openai-stream-metadata.d.ts +6 -0
- package/esm/openai-stream-metadata.d.ts.map +1 -0
- package/esm/openai-stream-metadata.js +10 -0
- package/esm/openai-tool-input.d.ts +10 -0
- package/esm/openai-tool-input.d.ts.map +1 -0
- package/esm/openai-tool-input.js +30 -0
- package/esm/openai-web-search.d.ts +35 -0
- package/esm/openai-web-search.d.ts.map +1 -0
- package/esm/openai-web-search.js +369 -0
- package/package.json +3 -3
package/esm/openai-provider.js
CHANGED
|
@@ -9,12 +9,16 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { buildProviderError, createOpenAIRequestInit, createWarningCollector, getOpenAIChatCompletionsUrl, getOpenAIEmbeddingUrl, getOpenAIResponsesUrl, isNumberArray, mergeUsage, parseRetryAfterMs, ProviderError, ProviderOverloadedError, ProviderQuotaError, ProviderRateLimitError, ProviderRequestError, readGatewayBillingMode, readRecord, requestJson, requestStream, stringifyJsonValue, TOOL_INPUT_PENDING_THRESHOLD_MS, } from "veryfront/provider/shared";
|
|
11
11
|
import { buildOpenAIChatRequest, } from "./openai-chat-request-builder.js";
|
|
12
|
-
import { streamOpenAICompatibleParts } from "./openai-chat-stream.js";
|
|
12
|
+
import { MAX_OPENAI_STREAM_TOOL_CALLS, streamOpenAICompatibleParts } from "./openai-chat-stream.js";
|
|
13
13
|
import { buildOpenAIResponsesRequest } from "./openai-responses-request-builder.js";
|
|
14
14
|
import { isOpenAIReasoningModel } from "./openai-reasoning-models.js";
|
|
15
|
+
import { isBoundedOpenAIStreamString, MAX_OPENAI_STREAM_IDENTIFIER_BYTES, MAX_OPENAI_STREAM_ITEM_TYPE_BYTES, MAX_OPENAI_STREAM_TOOL_NAME_BYTES, } from "./openai-stream-metadata.js";
|
|
15
16
|
import { extractOpenAIResponsesUsage, normalizeOpenAIResponsesFinishReason, streamOpenAIResponsesParts, } from "./openai-responses-stream.js";
|
|
17
|
+
import { isJsonObjectText, MAX_OPENAI_STREAM_TOOL_ARGUMENT_BYTES } from "./openai-tool-input.js";
|
|
18
|
+
import { createOpenAIRawResponseMetadata, MAX_OPENAI_RAW_RESPONSE_METADATA_BYTES, MAX_OPENAI_RAW_RESPONSE_OUTPUT_ITEMS, normalizeOpenAIWebSearchCall, readOpenAIRawResponseOutputItems, resolveOpenAIWebSearchDescriptor, validateOpenAIUrlCitation, } from "./openai-web-search.js";
|
|
16
19
|
// Re-export error classes so extension tests can import from this module.
|
|
17
20
|
export { buildProviderError, isNumberArray, mergeUsage, parseRetryAfterMs, ProviderError, ProviderOverloadedError, ProviderQuotaError, ProviderRateLimitError, ProviderRequestError, TOOL_INPUT_PENDING_THRESHOLD_MS, };
|
|
21
|
+
const OPENAI_RESPONSE_TEXT_ENCODER = new TextEncoder();
|
|
18
22
|
function readNonEmptyString(value) {
|
|
19
23
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
20
24
|
}
|
|
@@ -27,31 +31,92 @@ function getRuntimeOpenAIProviderName(config) {
|
|
|
27
31
|
function getLLMOpenAIProviderName(config) {
|
|
28
32
|
return readNonEmptyString(config.providerName) ?? "openai";
|
|
29
33
|
}
|
|
34
|
+
function getOpenAICompatibleProviderKind(config) {
|
|
35
|
+
const providerName = getRuntimeOpenAIProviderName(config).trim().toLowerCase();
|
|
36
|
+
return providerName === "mistral" || providerName === "moonshotai" ? providerName : "openai";
|
|
37
|
+
}
|
|
38
|
+
function invalidOpenAIResponse(context, issue) {
|
|
39
|
+
return new ProviderRequestError({
|
|
40
|
+
provider: context.providerKind,
|
|
41
|
+
status: 200,
|
|
42
|
+
message: `${context.providerLabel} request failed: invalid successful response (${issue})`,
|
|
43
|
+
retryable: false,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function sanitizeRuntimeUsage(usage) {
|
|
47
|
+
const normalized = mergeUsage(undefined, usage);
|
|
48
|
+
return normalized && Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
49
|
+
}
|
|
50
|
+
function readUsageTokenCount(value) {
|
|
51
|
+
return typeof value === "number" &&
|
|
52
|
+
Number.isFinite(value) &&
|
|
53
|
+
Number.isSafeInteger(value) &&
|
|
54
|
+
value >= 0
|
|
55
|
+
? value
|
|
56
|
+
: undefined;
|
|
57
|
+
}
|
|
30
58
|
// ---------------------------------------------------------------------------
|
|
31
59
|
// Embedding helpers
|
|
32
60
|
// ---------------------------------------------------------------------------
|
|
33
|
-
function extractOpenAIEmbeddings(payload) {
|
|
61
|
+
function extractOpenAIEmbeddings(payload, expectedCount, context) {
|
|
34
62
|
const record = readRecord(payload);
|
|
35
63
|
const data = record?.data;
|
|
36
64
|
if (!Array.isArray(data)) {
|
|
37
|
-
throw
|
|
65
|
+
throw invalidOpenAIResponse(context, "embedding data array missing");
|
|
66
|
+
}
|
|
67
|
+
if (data.length !== expectedCount) {
|
|
68
|
+
throw invalidOpenAIResponse(context, `expected ${expectedCount} embedding vectors but received ${data.length}`);
|
|
38
69
|
}
|
|
39
|
-
const
|
|
70
|
+
const positionalEmbeddings = [];
|
|
71
|
+
const indexedEmbeddings = Array(expectedCount);
|
|
72
|
+
let indexMode;
|
|
73
|
+
let dimensions;
|
|
40
74
|
for (const item of data) {
|
|
41
75
|
const itemRecord = readRecord(item);
|
|
42
76
|
const embedding = itemRecord?.embedding;
|
|
43
|
-
if (!isNumberArray(embedding)) {
|
|
44
|
-
throw
|
|
77
|
+
if (!isNumberArray(embedding) || embedding.length === 0) {
|
|
78
|
+
throw invalidOpenAIResponse(context, "embedding vector missing or invalid");
|
|
79
|
+
}
|
|
80
|
+
dimensions ??= embedding.length;
|
|
81
|
+
if (embedding.length !== dimensions) {
|
|
82
|
+
throw invalidOpenAIResponse(context, "embedding vectors had inconsistent dimensions");
|
|
83
|
+
}
|
|
84
|
+
const rawIndex = itemRecord?.index;
|
|
85
|
+
const itemIndexMode = rawIndex === undefined ? "positional" : "indexed";
|
|
86
|
+
if (indexMode !== undefined && indexMode !== itemIndexMode) {
|
|
87
|
+
throw invalidOpenAIResponse(context, "embedding indices were inconsistently provided");
|
|
88
|
+
}
|
|
89
|
+
indexMode = itemIndexMode;
|
|
90
|
+
if (itemIndexMode === "positional") {
|
|
91
|
+
positionalEmbeddings.push(embedding);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (typeof rawIndex !== "number" ||
|
|
95
|
+
!Number.isSafeInteger(rawIndex) ||
|
|
96
|
+
rawIndex < 0 ||
|
|
97
|
+
rawIndex >= expectedCount) {
|
|
98
|
+
throw invalidOpenAIResponse(context, "embedding index was invalid");
|
|
45
99
|
}
|
|
46
|
-
|
|
100
|
+
if (indexedEmbeddings[rawIndex] !== undefined) {
|
|
101
|
+
throw invalidOpenAIResponse(context, "embedding indices contained a duplicate");
|
|
102
|
+
}
|
|
103
|
+
indexedEmbeddings[rawIndex] = embedding;
|
|
104
|
+
}
|
|
105
|
+
if (indexMode !== "indexed") {
|
|
106
|
+
return positionalEmbeddings;
|
|
47
107
|
}
|
|
48
|
-
return
|
|
108
|
+
return indexedEmbeddings.map((embedding) => {
|
|
109
|
+
if (embedding === undefined) {
|
|
110
|
+
throw invalidOpenAIResponse(context, "embedding indices were incomplete");
|
|
111
|
+
}
|
|
112
|
+
return embedding;
|
|
113
|
+
});
|
|
49
114
|
}
|
|
50
115
|
function extractOpenAIUsageTokens(payload) {
|
|
51
116
|
const record = readRecord(payload);
|
|
52
117
|
const usage = readRecord(record?.usage);
|
|
53
118
|
const totalTokens = usage?.total_tokens;
|
|
54
|
-
return
|
|
119
|
+
return readUsageTokenCount(totalTokens);
|
|
55
120
|
}
|
|
56
121
|
// ---------------------------------------------------------------------------
|
|
57
122
|
// Chat helpers
|
|
@@ -131,38 +196,94 @@ function extractOpenAIUsage(payload) {
|
|
|
131
196
|
: {}),
|
|
132
197
|
};
|
|
133
198
|
}
|
|
134
|
-
function extractOpenAIContentText(content) {
|
|
199
|
+
function extractOpenAIContentText(content, context) {
|
|
135
200
|
if (typeof content === "string") {
|
|
201
|
+
if (OPENAI_RESPONSE_TEXT_ENCODER.encode(content).byteLength >
|
|
202
|
+
MAX_OPENAI_RAW_RESPONSE_METADATA_BYTES) {
|
|
203
|
+
throw invalidOpenAIResponse(context, `message content exceeded ${MAX_OPENAI_RAW_RESPONSE_METADATA_BYTES} UTF-8 bytes`);
|
|
204
|
+
}
|
|
136
205
|
return content;
|
|
137
206
|
}
|
|
138
207
|
if (!Array.isArray(content)) {
|
|
139
208
|
return "";
|
|
140
209
|
}
|
|
141
|
-
|
|
210
|
+
if (content.length > MAX_OPENAI_RAW_RESPONSE_OUTPUT_ITEMS) {
|
|
211
|
+
throw invalidOpenAIResponse(context, `message content exceeded ${MAX_OPENAI_RAW_RESPONSE_OUTPUT_ITEMS} parts`);
|
|
212
|
+
}
|
|
213
|
+
const textParts = [];
|
|
214
|
+
let retainedBytes = 0;
|
|
142
215
|
for (const part of content) {
|
|
143
216
|
const record = readRecord(part);
|
|
217
|
+
if (!record) {
|
|
218
|
+
throw invalidOpenAIResponse(context, "message content part was not an object");
|
|
219
|
+
}
|
|
144
220
|
const type = record?.type;
|
|
145
|
-
|
|
146
|
-
|
|
221
|
+
let value;
|
|
222
|
+
if (type === "text") {
|
|
223
|
+
if (typeof record.text !== "string") {
|
|
224
|
+
throw invalidOpenAIResponse(context, "text content part was malformed");
|
|
225
|
+
}
|
|
226
|
+
value = record.text;
|
|
227
|
+
}
|
|
228
|
+
else if (type === "refusal") {
|
|
229
|
+
if (typeof record.refusal !== "string") {
|
|
230
|
+
throw invalidOpenAIResponse(context, "refusal content part was malformed");
|
|
231
|
+
}
|
|
232
|
+
value = record.refusal;
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
throw invalidOpenAIResponse(context, "message content part type was unsupported");
|
|
147
236
|
}
|
|
237
|
+
const valueBytes = OPENAI_RESPONSE_TEXT_ENCODER.encode(value).byteLength;
|
|
238
|
+
if (valueBytes >
|
|
239
|
+
MAX_OPENAI_RAW_RESPONSE_METADATA_BYTES - retainedBytes) {
|
|
240
|
+
throw invalidOpenAIResponse(context, `message content exceeded ${MAX_OPENAI_RAW_RESPONSE_METADATA_BYTES} UTF-8 bytes`);
|
|
241
|
+
}
|
|
242
|
+
retainedBytes += valueBytes;
|
|
243
|
+
textParts.push(value);
|
|
148
244
|
}
|
|
149
|
-
return
|
|
245
|
+
return textParts.join("");
|
|
150
246
|
}
|
|
151
|
-
function extractOpenAIToolCalls(message) {
|
|
247
|
+
function extractOpenAIToolCalls(message, context) {
|
|
152
248
|
const toolCalls = message.tool_calls;
|
|
153
|
-
if (
|
|
249
|
+
if (toolCalls === undefined || toolCalls === null) {
|
|
154
250
|
return [];
|
|
155
251
|
}
|
|
252
|
+
if (!Array.isArray(toolCalls)) {
|
|
253
|
+
throw invalidOpenAIResponse(context, "message tool_calls was not an array");
|
|
254
|
+
}
|
|
255
|
+
if (toolCalls.length > MAX_OPENAI_STREAM_TOOL_CALLS) {
|
|
256
|
+
throw invalidOpenAIResponse(context, `message exceeded ${MAX_OPENAI_STREAM_TOOL_CALLS} tool calls`);
|
|
257
|
+
}
|
|
156
258
|
const normalized = [];
|
|
259
|
+
const seenToolCallIds = new Set();
|
|
260
|
+
let retainedArgumentBytes = 0;
|
|
157
261
|
for (const entry of toolCalls) {
|
|
158
262
|
const record = readRecord(entry);
|
|
159
|
-
const id =
|
|
263
|
+
const id = isBoundedOpenAIStreamString(record?.id, MAX_OPENAI_STREAM_IDENTIFIER_BYTES)
|
|
264
|
+
? record.id
|
|
265
|
+
: undefined;
|
|
160
266
|
const fn = readRecord(record?.function);
|
|
161
|
-
const name =
|
|
267
|
+
const name = isBoundedOpenAIStreamString(fn?.name, MAX_OPENAI_STREAM_TOOL_NAME_BYTES)
|
|
268
|
+
? fn.name
|
|
269
|
+
: undefined;
|
|
162
270
|
const argumentsText = typeof fn?.arguments === "string" ? fn.arguments : undefined;
|
|
163
|
-
if (!id || !name || argumentsText === undefined) {
|
|
164
|
-
|
|
271
|
+
if (record?.type !== "function" || !id || !name || argumentsText === undefined) {
|
|
272
|
+
throw invalidOpenAIResponse(context, "message contained a malformed tool call");
|
|
273
|
+
}
|
|
274
|
+
if (seenToolCallIds.has(id)) {
|
|
275
|
+
throw invalidOpenAIResponse(context, "message contained duplicate tool call ids");
|
|
276
|
+
}
|
|
277
|
+
const argumentBytes = OPENAI_RESPONSE_TEXT_ENCODER.encode(argumentsText).byteLength;
|
|
278
|
+
if (argumentBytes >
|
|
279
|
+
MAX_OPENAI_STREAM_TOOL_ARGUMENT_BYTES - retainedArgumentBytes) {
|
|
280
|
+
throw invalidOpenAIResponse(context, `message tool call arguments exceeded ${MAX_OPENAI_STREAM_TOOL_ARGUMENT_BYTES} UTF-8 bytes`);
|
|
165
281
|
}
|
|
282
|
+
if (!isJsonObjectText(argumentsText)) {
|
|
283
|
+
throw invalidOpenAIResponse(context, "message tool call arguments were not valid JSON object text");
|
|
284
|
+
}
|
|
285
|
+
retainedArgumentBytes += argumentBytes;
|
|
286
|
+
seenToolCallIds.add(id);
|
|
166
287
|
normalized.push({
|
|
167
288
|
toolCallId: id,
|
|
168
289
|
toolName: name,
|
|
@@ -171,99 +292,386 @@ function extractOpenAIToolCalls(message) {
|
|
|
171
292
|
}
|
|
172
293
|
return normalized;
|
|
173
294
|
}
|
|
174
|
-
function extractFirstChoice(payload) {
|
|
295
|
+
function extractFirstChoice(payload, context) {
|
|
175
296
|
const record = readRecord(payload);
|
|
176
297
|
const choices = record?.choices;
|
|
177
298
|
if (!Array.isArray(choices) || choices.length === 0) {
|
|
178
|
-
|
|
299
|
+
throw invalidOpenAIResponse(context, "choices array missing or empty");
|
|
179
300
|
}
|
|
180
301
|
const first = readRecord(choices[0]);
|
|
181
302
|
if (!first) {
|
|
182
|
-
|
|
303
|
+
throw invalidOpenAIResponse(context, "first choice was not an object");
|
|
183
304
|
}
|
|
184
305
|
return first;
|
|
185
306
|
}
|
|
186
|
-
function buildOpenAIGenerateResult(payload) {
|
|
187
|
-
const choice = extractFirstChoice(payload);
|
|
188
|
-
const message = readRecord(choice
|
|
189
|
-
|
|
190
|
-
|
|
307
|
+
function buildOpenAIGenerateResult(payload, context) {
|
|
308
|
+
const choice = extractFirstChoice(payload, context);
|
|
309
|
+
const message = readRecord(choice.message);
|
|
310
|
+
if (!message) {
|
|
311
|
+
throw invalidOpenAIResponse(context, "choice message missing");
|
|
312
|
+
}
|
|
313
|
+
if (message.role !== "assistant") {
|
|
314
|
+
throw invalidOpenAIResponse(context, "choice message role was not assistant");
|
|
315
|
+
}
|
|
316
|
+
if (message.content !== undefined &&
|
|
317
|
+
message.content !== null &&
|
|
318
|
+
typeof message.content !== "string" &&
|
|
319
|
+
!Array.isArray(message.content)) {
|
|
320
|
+
throw invalidOpenAIResponse(context, "message content had an invalid type");
|
|
321
|
+
}
|
|
322
|
+
if (message.refusal !== undefined &&
|
|
323
|
+
message.refusal !== null &&
|
|
324
|
+
typeof message.refusal !== "string") {
|
|
325
|
+
throw invalidOpenAIResponse(context, "message refusal had an invalid type");
|
|
326
|
+
}
|
|
327
|
+
if (typeof choice.finish_reason !== "string" || choice.finish_reason.length === 0) {
|
|
328
|
+
throw invalidOpenAIResponse(context, "choice finish reason missing");
|
|
329
|
+
}
|
|
330
|
+
const regularText = extractOpenAIContentText(message.content, context);
|
|
331
|
+
const refusalText = typeof message.refusal === "string" ? message.refusal : "";
|
|
332
|
+
const regularTextBytes = OPENAI_RESPONSE_TEXT_ENCODER.encode(regularText).byteLength;
|
|
333
|
+
const refusalTextBytes = OPENAI_RESPONSE_TEXT_ENCODER.encode(refusalText).byteLength;
|
|
334
|
+
if (refusalTextBytes >
|
|
335
|
+
MAX_OPENAI_RAW_RESPONSE_METADATA_BYTES - regularTextBytes) {
|
|
336
|
+
throw invalidOpenAIResponse(context, `message content exceeded ${MAX_OPENAI_RAW_RESPONSE_METADATA_BYTES} UTF-8 bytes`);
|
|
337
|
+
}
|
|
338
|
+
const text = regularText + refusalText;
|
|
339
|
+
const toolCalls = extractOpenAIToolCalls(message, context);
|
|
340
|
+
if ((choice.finish_reason === "tool_calls") !== (toolCalls.length > 0)) {
|
|
341
|
+
throw invalidOpenAIResponse(context, "choice finish reason and tool calls were inconsistent");
|
|
342
|
+
}
|
|
343
|
+
const content = [
|
|
344
|
+
...(text.length > 0 ? [{ type: "text", text }] : []),
|
|
345
|
+
...toolCalls.map((toolCall) => ({
|
|
346
|
+
type: "tool-call",
|
|
347
|
+
toolCallId: toolCall.toolCallId,
|
|
348
|
+
toolName: toolCall.toolName,
|
|
349
|
+
input: toolCall.input,
|
|
350
|
+
})),
|
|
351
|
+
];
|
|
191
352
|
return {
|
|
192
|
-
content
|
|
193
|
-
...(text.length > 0 ? [{ type: "text", text }] : []),
|
|
194
|
-
...toolCalls.map((toolCall) => ({
|
|
195
|
-
type: "tool-call",
|
|
196
|
-
toolCallId: toolCall.toolCallId,
|
|
197
|
-
toolName: toolCall.toolName,
|
|
198
|
-
input: toolCall.input,
|
|
199
|
-
})),
|
|
200
|
-
],
|
|
353
|
+
content,
|
|
201
354
|
finishReason: normalizeOpenAIFinishReason(choice?.finish_reason),
|
|
202
|
-
usage: extractOpenAIUsage(payload),
|
|
355
|
+
usage: sanitizeRuntimeUsage(extractOpenAIUsage(payload)),
|
|
203
356
|
};
|
|
204
357
|
}
|
|
205
|
-
function
|
|
358
|
+
function assertTerminalOpenAIResponsesOutputItem(item, context) {
|
|
359
|
+
const validStatus = item.status === undefined ||
|
|
360
|
+
item.status === "completed" ||
|
|
361
|
+
item.status === "incomplete" ||
|
|
362
|
+
(item.type === "web_search_call" && item.status === "failed");
|
|
363
|
+
if (!validStatus) {
|
|
364
|
+
throw invalidOpenAIResponse(context, "output item status was unsupported or nonterminal");
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function createValidatedOpenAIRawResponseMetadata(outputItems, context) {
|
|
368
|
+
const metadata = createOpenAIRawResponseMetadata(outputItems);
|
|
369
|
+
try {
|
|
370
|
+
readOpenAIRawResponseOutputItems(metadata);
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
throw invalidOpenAIResponse(context, "raw response output items were unsafe to replay");
|
|
374
|
+
}
|
|
375
|
+
return metadata;
|
|
376
|
+
}
|
|
377
|
+
function buildOpenAIResponsesGenerateResult(payload, context, webSearchToolName) {
|
|
206
378
|
const record = readRecord(payload);
|
|
207
|
-
|
|
379
|
+
if (!record) {
|
|
380
|
+
throw invalidOpenAIResponse(context, "response body was not an object");
|
|
381
|
+
}
|
|
382
|
+
if (typeof record.status !== "string" || record.status.length === 0) {
|
|
383
|
+
throw invalidOpenAIResponse(context, "response status missing");
|
|
384
|
+
}
|
|
385
|
+
if (record.status !== "completed" &&
|
|
386
|
+
record.status !== "incomplete" &&
|
|
387
|
+
record.status !== "failed") {
|
|
388
|
+
throw invalidOpenAIResponse(context, "response status was unsupported or nonterminal");
|
|
389
|
+
}
|
|
390
|
+
if (!Array.isArray(record.output)) {
|
|
391
|
+
throw invalidOpenAIResponse(context, "output array missing");
|
|
392
|
+
}
|
|
393
|
+
const output = record.output;
|
|
394
|
+
if (output.length > MAX_OPENAI_RAW_RESPONSE_OUTPUT_ITEMS) {
|
|
395
|
+
throw invalidOpenAIResponse(context, `output exceeded ${MAX_OPENAI_RAW_RESPONSE_OUTPUT_ITEMS} items`);
|
|
396
|
+
}
|
|
208
397
|
const content = [];
|
|
398
|
+
const rawOutputItems = [];
|
|
399
|
+
const seenOutputItemIds = new Set();
|
|
400
|
+
const seenToolCallIds = new Set();
|
|
401
|
+
let normalizedContentBytes = 0;
|
|
402
|
+
function reserveNormalizedContent(value, issue) {
|
|
403
|
+
const valueBytes = OPENAI_RESPONSE_TEXT_ENCODER.encode(value).byteLength;
|
|
404
|
+
if (valueBytes > MAX_OPENAI_RAW_RESPONSE_METADATA_BYTES -
|
|
405
|
+
normalizedContentBytes) {
|
|
406
|
+
throw invalidOpenAIResponse(context, `${issue} exceeded ${MAX_OPENAI_RAW_RESPONSE_METADATA_BYTES} UTF-8 bytes`);
|
|
407
|
+
}
|
|
408
|
+
normalizedContentBytes += valueBytes;
|
|
409
|
+
}
|
|
209
410
|
for (const item of output) {
|
|
210
411
|
const itemRecord = readRecord(item);
|
|
211
|
-
|
|
212
|
-
|
|
412
|
+
if (!itemRecord) {
|
|
413
|
+
throw invalidOpenAIResponse(context, "output item was not an object");
|
|
414
|
+
}
|
|
415
|
+
rawOutputItems.push(itemRecord);
|
|
416
|
+
const itemType = isBoundedOpenAIStreamString(itemRecord.type, MAX_OPENAI_STREAM_ITEM_TYPE_BYTES)
|
|
417
|
+
? itemRecord.type
|
|
418
|
+
: undefined;
|
|
419
|
+
if (!itemType) {
|
|
420
|
+
throw invalidOpenAIResponse(context, "output item type missing");
|
|
421
|
+
}
|
|
422
|
+
assertTerminalOpenAIResponsesOutputItem(itemRecord, context);
|
|
423
|
+
if (!isBoundedOpenAIStreamString(itemRecord.id, MAX_OPENAI_STREAM_IDENTIFIER_BYTES)) {
|
|
424
|
+
throw invalidOpenAIResponse(context, "output item id was malformed");
|
|
425
|
+
}
|
|
426
|
+
if (seenOutputItemIds.has(itemRecord.id)) {
|
|
427
|
+
throw invalidOpenAIResponse(context, "response contained duplicate output item ids");
|
|
428
|
+
}
|
|
429
|
+
seenOutputItemIds.add(itemRecord.id);
|
|
430
|
+
if (itemType === "message") {
|
|
431
|
+
if (itemRecord.role !== "assistant") {
|
|
432
|
+
throw invalidOpenAIResponse(context, "message output role was not assistant");
|
|
433
|
+
}
|
|
434
|
+
if (!Array.isArray(itemRecord.content)) {
|
|
435
|
+
throw invalidOpenAIResponse(context, "message output content was not an array");
|
|
436
|
+
}
|
|
437
|
+
if (itemRecord.content.length > MAX_OPENAI_RAW_RESPONSE_OUTPUT_ITEMS) {
|
|
438
|
+
throw invalidOpenAIResponse(context, "message output contained too many content parts");
|
|
439
|
+
}
|
|
213
440
|
// A message item bundles one or more output_text parts.
|
|
214
|
-
|
|
441
|
+
const textParts = [];
|
|
215
442
|
for (const part of itemRecord.content) {
|
|
216
443
|
const p = readRecord(part);
|
|
217
|
-
if (
|
|
218
|
-
|
|
444
|
+
if (!p) {
|
|
445
|
+
throw invalidOpenAIResponse(context, "message content part was not an object");
|
|
446
|
+
}
|
|
447
|
+
if (p.type === "output_text") {
|
|
448
|
+
if (typeof p.text !== "string") {
|
|
449
|
+
throw invalidOpenAIResponse(context, "output-text content part was malformed");
|
|
450
|
+
}
|
|
451
|
+
if (p.annotations !== undefined) {
|
|
452
|
+
if (!Array.isArray(p.annotations) ||
|
|
453
|
+
p.annotations.length > MAX_OPENAI_RAW_RESPONSE_OUTPUT_ITEMS) {
|
|
454
|
+
throw invalidOpenAIResponse(context, "output-text annotations were malformed");
|
|
455
|
+
}
|
|
456
|
+
for (const annotation of p.annotations) {
|
|
457
|
+
const citation = validateOpenAIUrlCitation(annotation, (issue) => invalidOpenAIResponse(context, issue));
|
|
458
|
+
if (citation.end_index > p.text.length) {
|
|
459
|
+
throw invalidOpenAIResponse(context, "URL citation annotation range exceeded output text");
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
reserveNormalizedContent(p.text, "normalized response content");
|
|
464
|
+
textParts.push(p.text);
|
|
465
|
+
}
|
|
466
|
+
else if (p.type === "refusal") {
|
|
467
|
+
if (typeof p.refusal !== "string" ||
|
|
468
|
+
p.annotations !== undefined) {
|
|
469
|
+
throw invalidOpenAIResponse(context, "refusal content part was malformed");
|
|
470
|
+
}
|
|
471
|
+
reserveNormalizedContent(p.refusal, "normalized response content");
|
|
472
|
+
textParts.push(p.refusal);
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
throw invalidOpenAIResponse(context, "message content part type was unsupported");
|
|
219
476
|
}
|
|
220
477
|
}
|
|
478
|
+
const text = textParts.join("");
|
|
221
479
|
if (text.length > 0) {
|
|
222
480
|
content.push({ type: "text", text });
|
|
223
481
|
}
|
|
224
482
|
continue;
|
|
225
483
|
}
|
|
226
484
|
if (itemType === "function_call") {
|
|
485
|
+
const toolCallId = isBoundedOpenAIStreamString(itemRecord.call_id, MAX_OPENAI_STREAM_IDENTIFIER_BYTES)
|
|
486
|
+
? itemRecord.call_id
|
|
487
|
+
: undefined;
|
|
488
|
+
const toolName = isBoundedOpenAIStreamString(itemRecord.name, MAX_OPENAI_STREAM_TOOL_NAME_BYTES)
|
|
489
|
+
? itemRecord.name
|
|
490
|
+
: undefined;
|
|
491
|
+
if (!toolCallId ||
|
|
492
|
+
!toolName ||
|
|
493
|
+
!isBoundedOpenAIStreamString(itemRecord.arguments, MAX_OPENAI_STREAM_TOOL_ARGUMENT_BYTES)) {
|
|
494
|
+
throw invalidOpenAIResponse(context, "function call output item was malformed");
|
|
495
|
+
}
|
|
496
|
+
if (seenToolCallIds.has(toolCallId)) {
|
|
497
|
+
throw invalidOpenAIResponse(context, "response contained duplicate function call ids");
|
|
498
|
+
}
|
|
499
|
+
if (!isJsonObjectText(itemRecord.arguments)) {
|
|
500
|
+
throw invalidOpenAIResponse(context, "function call arguments were not valid JSON object text");
|
|
501
|
+
}
|
|
502
|
+
reserveNormalizedContent(itemRecord.arguments, "normalized response content");
|
|
227
503
|
content.push({
|
|
228
504
|
type: "tool-call",
|
|
229
|
-
toolCallId
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
toolName: typeof itemRecord?.name === "string" ? itemRecord.name : "",
|
|
233
|
-
input: typeof itemRecord?.arguments === "string"
|
|
234
|
-
? itemRecord.arguments
|
|
235
|
-
: stringifyJsonValue(itemRecord?.arguments ?? {}),
|
|
505
|
+
toolCallId,
|
|
506
|
+
toolName,
|
|
507
|
+
input: itemRecord.arguments,
|
|
236
508
|
});
|
|
509
|
+
seenToolCallIds.add(toolCallId);
|
|
237
510
|
continue;
|
|
238
511
|
}
|
|
239
|
-
if (itemType === "
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
for (const s of summary) {
|
|
243
|
-
const sr = readRecord(s);
|
|
244
|
-
if (typeof sr?.text === "string" && sr.text.length > 0) {
|
|
245
|
-
summaries.push({
|
|
246
|
-
...(typeof sr?.id === "string" ? { id: sr.id } : {}),
|
|
247
|
-
text: sr.text,
|
|
248
|
-
});
|
|
249
|
-
}
|
|
512
|
+
if (itemType === "web_search_call") {
|
|
513
|
+
if (!webSearchToolName) {
|
|
514
|
+
throw invalidOpenAIResponse(context, "provider emitted web search without a configured web-search tool");
|
|
250
515
|
}
|
|
516
|
+
const webSearch = normalizeOpenAIWebSearchCall(itemRecord, (issue) => invalidOpenAIResponse(context, issue));
|
|
517
|
+
if (seenToolCallIds.has(webSearch.id)) {
|
|
518
|
+
throw invalidOpenAIResponse(context, "response contained duplicate tool call ids");
|
|
519
|
+
}
|
|
520
|
+
reserveNormalizedContent(webSearch.input, "normalized response content");
|
|
521
|
+
reserveNormalizedContent(stringifyJsonValue(webSearch.result), "normalized response content");
|
|
251
522
|
content.push({
|
|
252
|
-
type: "
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
523
|
+
type: "tool-call",
|
|
524
|
+
toolCallId: webSearch.id,
|
|
525
|
+
toolName: webSearchToolName,
|
|
526
|
+
input: webSearch.input,
|
|
527
|
+
providerExecuted: true,
|
|
257
528
|
});
|
|
529
|
+
content.push({
|
|
530
|
+
type: "tool-result",
|
|
531
|
+
toolCallId: webSearch.id,
|
|
532
|
+
toolName: webSearchToolName,
|
|
533
|
+
result: webSearch.result,
|
|
534
|
+
...(webSearch.isError ? { isError: true } : {}),
|
|
535
|
+
providerExecuted: true,
|
|
536
|
+
});
|
|
537
|
+
seenToolCallIds.add(webSearch.id);
|
|
258
538
|
continue;
|
|
259
539
|
}
|
|
540
|
+
if (itemType === "reasoning") {
|
|
541
|
+
if ((itemRecord.summary !== undefined && !Array.isArray(itemRecord.summary)) ||
|
|
542
|
+
(itemRecord.content !== undefined && !Array.isArray(itemRecord.content))) {
|
|
543
|
+
throw invalidOpenAIResponse(context, "reasoning summary or content was not an array");
|
|
544
|
+
}
|
|
545
|
+
const summary = Array.isArray(itemRecord.summary) ? itemRecord.summary : [];
|
|
546
|
+
const reasoningContent = Array.isArray(itemRecord.content) ? itemRecord.content : [];
|
|
547
|
+
if (summary.length > MAX_OPENAI_RAW_RESPONSE_OUTPUT_ITEMS ||
|
|
548
|
+
reasoningContent.length > MAX_OPENAI_RAW_RESPONSE_OUTPUT_ITEMS) {
|
|
549
|
+
throw invalidOpenAIResponse(context, "reasoning contained too many parts");
|
|
550
|
+
}
|
|
551
|
+
const reasoningText = [];
|
|
552
|
+
for (const [parts, expectedType] of [
|
|
553
|
+
[summary, "summary_text"],
|
|
554
|
+
[reasoningContent, "reasoning_text"],
|
|
555
|
+
]) {
|
|
556
|
+
for (const rawPart of parts) {
|
|
557
|
+
const part = readRecord(rawPart);
|
|
558
|
+
if (!part ||
|
|
559
|
+
part.type !== expectedType ||
|
|
560
|
+
typeof part.text !== "string" ||
|
|
561
|
+
(part.id !== undefined &&
|
|
562
|
+
!isBoundedOpenAIStreamString(part.id, MAX_OPENAI_STREAM_IDENTIFIER_BYTES))) {
|
|
563
|
+
throw invalidOpenAIResponse(context, expectedType === "summary_text"
|
|
564
|
+
? "reasoning summary item was malformed"
|
|
565
|
+
: "reasoning content item was malformed");
|
|
566
|
+
}
|
|
567
|
+
if (part.text.length > 0) {
|
|
568
|
+
reserveNormalizedContent(part.text, "normalized response content");
|
|
569
|
+
reasoningText.push(part.text);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (itemRecord.encrypted_content !== undefined &&
|
|
574
|
+
(typeof itemRecord.encrypted_content !== "string" ||
|
|
575
|
+
OPENAI_RESPONSE_TEXT_ENCODER.encode(itemRecord.encrypted_content).byteLength >
|
|
576
|
+
MAX_OPENAI_RAW_RESPONSE_METADATA_BYTES)) {
|
|
577
|
+
throw invalidOpenAIResponse(context, "reasoning encrypted content was malformed");
|
|
578
|
+
}
|
|
579
|
+
const signature = typeof itemRecord.encrypted_content === "string"
|
|
580
|
+
? itemRecord.encrypted_content
|
|
581
|
+
: undefined;
|
|
582
|
+
if (signature !== undefined) {
|
|
583
|
+
reserveNormalizedContent(signature, "normalized response content");
|
|
584
|
+
}
|
|
585
|
+
if (reasoningText.length > 0 || signature !== undefined) {
|
|
586
|
+
content.push({
|
|
587
|
+
type: "reasoning",
|
|
588
|
+
...(reasoningText.length > 0 ? { text: reasoningText.join("") } : {}),
|
|
589
|
+
...(signature !== undefined ? { signature } : {}),
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
throw invalidOpenAIResponse(context, "output item type was unsupported");
|
|
260
595
|
}
|
|
261
596
|
return {
|
|
262
597
|
content,
|
|
263
598
|
finishReason: normalizeOpenAIResponsesFinishReason(record?.status),
|
|
264
|
-
usage: extractOpenAIResponsesUsage(payload),
|
|
599
|
+
usage: sanitizeRuntimeUsage(extractOpenAIResponsesUsage(payload)),
|
|
600
|
+
...(rawOutputItems.length > 0
|
|
601
|
+
? {
|
|
602
|
+
providerMetadata: createValidatedOpenAIRawResponseMetadata(rawOutputItems, context),
|
|
603
|
+
}
|
|
604
|
+
: {}),
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
function createOpenAIProviderAbortScope(callerSignal) {
|
|
608
|
+
const controller = new AbortController();
|
|
609
|
+
const abortFromCaller = () => controller.abort(callerSignal?.reason);
|
|
610
|
+
if (callerSignal?.aborted) {
|
|
611
|
+
abortFromCaller();
|
|
612
|
+
return { controller, dispose() { } };
|
|
613
|
+
}
|
|
614
|
+
callerSignal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
615
|
+
return {
|
|
616
|
+
controller,
|
|
617
|
+
dispose() {
|
|
618
|
+
callerSignal?.removeEventListener("abort", abortFromCaller);
|
|
619
|
+
},
|
|
265
620
|
};
|
|
266
621
|
}
|
|
622
|
+
function createCancelableOpenAIProviderStream(iterable, providerAbortController, disposeAbortScope) {
|
|
623
|
+
const iterator = iterable[Symbol.asyncIterator]();
|
|
624
|
+
let consumerCanceled = false;
|
|
625
|
+
let disposed = false;
|
|
626
|
+
const dispose = () => {
|
|
627
|
+
if (disposed)
|
|
628
|
+
return;
|
|
629
|
+
disposed = true;
|
|
630
|
+
disposeAbortScope();
|
|
631
|
+
};
|
|
632
|
+
return new ReadableStream({
|
|
633
|
+
async pull(controller) {
|
|
634
|
+
try {
|
|
635
|
+
const next = await iterator.next();
|
|
636
|
+
if (next.done) {
|
|
637
|
+
dispose();
|
|
638
|
+
controller.close();
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
controller.enqueue(next.value);
|
|
642
|
+
}
|
|
643
|
+
catch (error) {
|
|
644
|
+
if (!providerAbortController.signal.aborted) {
|
|
645
|
+
providerAbortController.abort(error);
|
|
646
|
+
}
|
|
647
|
+
dispose();
|
|
648
|
+
if (!consumerCanceled) {
|
|
649
|
+
controller.error(error);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
async cancel(reason) {
|
|
654
|
+
consumerCanceled = true;
|
|
655
|
+
if (!providerAbortController.signal.aborted) {
|
|
656
|
+
providerAbortController.abort(reason);
|
|
657
|
+
}
|
|
658
|
+
try {
|
|
659
|
+
await iterator.return?.();
|
|
660
|
+
}
|
|
661
|
+
catch (error) {
|
|
662
|
+
if (!providerAbortController.signal.aborted) {
|
|
663
|
+
throw error;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
finally {
|
|
667
|
+
dispose();
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
// Avoid speculative reads so cancellation reaches the provider body as
|
|
672
|
+
// soon as the consumer stops after its most recent part.
|
|
673
|
+
{ highWaterMark: 0 });
|
|
674
|
+
}
|
|
267
675
|
// ---------------------------------------------------------------------------
|
|
268
676
|
// Public factory functions
|
|
269
677
|
// ---------------------------------------------------------------------------
|
|
@@ -271,13 +679,14 @@ export function createOpenAIModelRuntime(config, modelId) {
|
|
|
271
679
|
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
272
680
|
const providerLabel = getOpenAIProviderLabel(config);
|
|
273
681
|
const providerName = getRuntimeOpenAIProviderName(config);
|
|
682
|
+
const providerKind = getOpenAICompatibleProviderKind(config);
|
|
683
|
+
const responseContext = { providerKind, providerLabel };
|
|
274
684
|
return {
|
|
275
685
|
provider: providerLabel,
|
|
276
686
|
modelId,
|
|
277
687
|
specificationVersion: "v3",
|
|
278
688
|
supportedUrls: {},
|
|
279
|
-
doGenerate(
|
|
280
|
-
const options = optionsForRuntime;
|
|
689
|
+
doGenerate(options) {
|
|
281
690
|
const url = getOpenAIChatCompletionsUrl(config.baseURL);
|
|
282
691
|
const warnings = createWarningCollector();
|
|
283
692
|
const body = buildOpenAIChatRequest(modelId, providerName, options, false, warnings);
|
|
@@ -285,7 +694,7 @@ export function createOpenAIModelRuntime(config, modelId) {
|
|
|
285
694
|
url,
|
|
286
695
|
fetchImpl,
|
|
287
696
|
providerLabel,
|
|
288
|
-
providerKind
|
|
697
|
+
providerKind,
|
|
289
698
|
init: createOpenAIRequestInit({
|
|
290
699
|
apiKey: config.apiKey,
|
|
291
700
|
extraHeaders: options.headers,
|
|
@@ -295,34 +704,39 @@ export function createOpenAIModelRuntime(config, modelId) {
|
|
|
295
704
|
}).then((payload) => {
|
|
296
705
|
const drained = warnings.drain();
|
|
297
706
|
return {
|
|
298
|
-
...buildOpenAIGenerateResult(payload),
|
|
707
|
+
...buildOpenAIGenerateResult(payload, responseContext),
|
|
299
708
|
...(drained.length > 0 ? { warnings: drained } : {}),
|
|
300
709
|
};
|
|
301
710
|
});
|
|
302
711
|
},
|
|
303
|
-
doStream(
|
|
304
|
-
const options = optionsForRuntime;
|
|
712
|
+
async doStream(options) {
|
|
305
713
|
const url = getOpenAIChatCompletionsUrl(config.baseURL);
|
|
306
714
|
const warnings = createWarningCollector();
|
|
307
715
|
const body = buildOpenAIChatRequest(modelId, providerName, options, true, warnings);
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
716
|
+
const providerAbortScope = createOpenAIProviderAbortScope(options.abortSignal);
|
|
717
|
+
try {
|
|
718
|
+
const responseStream = await requestStream({
|
|
719
|
+
url,
|
|
720
|
+
fetchImpl,
|
|
721
|
+
providerLabel,
|
|
722
|
+
providerKind,
|
|
723
|
+
init: createOpenAIRequestInit({
|
|
724
|
+
apiKey: config.apiKey,
|
|
725
|
+
extraHeaders: options.headers,
|
|
726
|
+
body: JSON.stringify(body),
|
|
727
|
+
signal: providerAbortScope.controller.signal,
|
|
728
|
+
}),
|
|
729
|
+
});
|
|
320
730
|
const drained = warnings.drain();
|
|
321
731
|
return {
|
|
322
|
-
stream:
|
|
732
|
+
stream: createCancelableOpenAIProviderStream(streamOpenAICompatibleParts(responseStream, responseContext), providerAbortScope.controller, providerAbortScope.dispose),
|
|
323
733
|
...(drained.length > 0 ? { warnings: drained } : {}),
|
|
324
734
|
};
|
|
325
|
-
}
|
|
735
|
+
}
|
|
736
|
+
catch (error) {
|
|
737
|
+
providerAbortScope.dispose();
|
|
738
|
+
throw error;
|
|
739
|
+
}
|
|
326
740
|
},
|
|
327
741
|
};
|
|
328
742
|
}
|
|
@@ -330,21 +744,23 @@ export function createOpenAIResponsesRuntime(config, modelId) {
|
|
|
330
744
|
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
331
745
|
const providerLabel = getOpenAIProviderLabel(config);
|
|
332
746
|
const providerName = getRuntimeOpenAIProviderName(config);
|
|
747
|
+
const providerKind = getOpenAICompatibleProviderKind(config);
|
|
748
|
+
const responseContext = { providerKind, providerLabel };
|
|
333
749
|
return {
|
|
334
750
|
provider: providerLabel,
|
|
335
751
|
modelId,
|
|
336
752
|
specificationVersion: "v3",
|
|
337
753
|
supportedUrls: {},
|
|
338
|
-
doGenerate(
|
|
339
|
-
const options = optionsForRuntime;
|
|
754
|
+
doGenerate(options) {
|
|
340
755
|
const url = getOpenAIResponsesUrl(config.baseURL);
|
|
341
756
|
const warnings = createWarningCollector();
|
|
342
757
|
const body = buildOpenAIResponsesRequest(modelId, providerName, options, false, warnings);
|
|
758
|
+
const webSearchToolName = resolveOpenAIWebSearchDescriptor(options.tools)?.name;
|
|
343
759
|
return requestJson({
|
|
344
760
|
url,
|
|
345
761
|
fetchImpl,
|
|
346
762
|
providerLabel,
|
|
347
|
-
providerKind
|
|
763
|
+
providerKind,
|
|
348
764
|
init: createOpenAIRequestInit({
|
|
349
765
|
apiKey: config.apiKey,
|
|
350
766
|
extraHeaders: options.headers,
|
|
@@ -354,40 +770,76 @@ export function createOpenAIResponsesRuntime(config, modelId) {
|
|
|
354
770
|
}).then((payload) => {
|
|
355
771
|
const drained = warnings.drain();
|
|
356
772
|
return {
|
|
357
|
-
...buildOpenAIResponsesGenerateResult(payload),
|
|
773
|
+
...buildOpenAIResponsesGenerateResult(payload, responseContext, webSearchToolName),
|
|
358
774
|
...(drained.length > 0 ? { warnings: drained } : {}),
|
|
359
775
|
};
|
|
360
776
|
});
|
|
361
777
|
},
|
|
362
|
-
doStream(
|
|
363
|
-
const options = optionsForRuntime;
|
|
778
|
+
async doStream(options) {
|
|
364
779
|
const url = getOpenAIResponsesUrl(config.baseURL);
|
|
365
780
|
const warnings = createWarningCollector();
|
|
366
781
|
const body = buildOpenAIResponsesRequest(modelId, providerName, options, true, warnings);
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
782
|
+
const webSearchToolName = resolveOpenAIWebSearchDescriptor(options.tools)?.name;
|
|
783
|
+
const providerAbortScope = createOpenAIProviderAbortScope(options.abortSignal);
|
|
784
|
+
try {
|
|
785
|
+
const responseStream = await requestStream({
|
|
786
|
+
url,
|
|
787
|
+
fetchImpl,
|
|
788
|
+
providerLabel,
|
|
789
|
+
providerKind,
|
|
790
|
+
init: createOpenAIRequestInit({
|
|
791
|
+
apiKey: config.apiKey,
|
|
792
|
+
extraHeaders: options.headers,
|
|
793
|
+
body: JSON.stringify(body),
|
|
794
|
+
signal: providerAbortScope.controller.signal,
|
|
795
|
+
}),
|
|
796
|
+
});
|
|
379
797
|
const drained = warnings.drain();
|
|
380
798
|
return {
|
|
381
|
-
stream:
|
|
799
|
+
stream: createCancelableOpenAIProviderStream(streamOpenAIResponsesParts(responseStream, {
|
|
800
|
+
...responseContext,
|
|
801
|
+
webSearchToolName,
|
|
802
|
+
preserveRawOutputItems: true,
|
|
803
|
+
}), providerAbortScope.controller, providerAbortScope.dispose),
|
|
382
804
|
...(drained.length > 0 ? { warnings: drained } : {}),
|
|
383
805
|
};
|
|
384
|
-
}
|
|
806
|
+
}
|
|
807
|
+
catch (error) {
|
|
808
|
+
providerAbortScope.dispose();
|
|
809
|
+
throw error;
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
function requestUsesOpenAIHostedTool(optionsForRuntime) {
|
|
815
|
+
const options = readRecord(optionsForRuntime);
|
|
816
|
+
if (!options || options.tools === undefined)
|
|
817
|
+
return false;
|
|
818
|
+
if (!Array.isArray(options.tools)) {
|
|
819
|
+
throw new TypeError("OpenAI runtime tools must be an array");
|
|
820
|
+
}
|
|
821
|
+
return resolveOpenAIWebSearchDescriptor(options.tools) !== undefined;
|
|
822
|
+
}
|
|
823
|
+
function createOpenAIAdaptiveModelRuntime(chatRuntime, responsesRuntime) {
|
|
824
|
+
return {
|
|
825
|
+
...chatRuntime,
|
|
826
|
+
doGenerate(optionsForRuntime) {
|
|
827
|
+
return requestUsesOpenAIHostedTool(optionsForRuntime)
|
|
828
|
+
? responsesRuntime.doGenerate(optionsForRuntime)
|
|
829
|
+
: chatRuntime.doGenerate(optionsForRuntime);
|
|
830
|
+
},
|
|
831
|
+
doStream(optionsForRuntime) {
|
|
832
|
+
return requestUsesOpenAIHostedTool(optionsForRuntime)
|
|
833
|
+
? responsesRuntime.doStream(optionsForRuntime)
|
|
834
|
+
: chatRuntime.doStream(optionsForRuntime);
|
|
385
835
|
},
|
|
386
836
|
};
|
|
387
837
|
}
|
|
388
838
|
export function createOpenAIEmbeddingRuntime(config, modelId) {
|
|
389
839
|
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
390
840
|
const providerLabel = getOpenAIProviderLabel(config);
|
|
841
|
+
const providerKind = getOpenAICompatibleProviderKind(config);
|
|
842
|
+
const responseContext = { providerKind, providerLabel };
|
|
391
843
|
return {
|
|
392
844
|
provider: providerLabel,
|
|
393
845
|
modelId,
|
|
@@ -405,7 +857,7 @@ export function createOpenAIEmbeddingRuntime(config, modelId) {
|
|
|
405
857
|
url,
|
|
406
858
|
fetchImpl,
|
|
407
859
|
providerLabel,
|
|
408
|
-
providerKind
|
|
860
|
+
providerKind,
|
|
409
861
|
init: createOpenAIRequestInit({
|
|
410
862
|
apiKey: config.apiKey,
|
|
411
863
|
body: JSON.stringify({
|
|
@@ -414,14 +866,15 @@ export function createOpenAIEmbeddingRuntime(config, modelId) {
|
|
|
414
866
|
}),
|
|
415
867
|
signal: abortSignal,
|
|
416
868
|
}),
|
|
417
|
-
}).then((payload) =>
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
869
|
+
}).then((payload) => {
|
|
870
|
+
const tokens = extractOpenAIUsageTokens(payload);
|
|
871
|
+
return {
|
|
872
|
+
embeddings: extractOpenAIEmbeddings(payload, values.length, responseContext),
|
|
873
|
+
...(tokens !== undefined ? { usage: { tokens } } : {}),
|
|
874
|
+
rawResponse: payload,
|
|
875
|
+
warnings: [],
|
|
876
|
+
};
|
|
877
|
+
});
|
|
425
878
|
},
|
|
426
879
|
};
|
|
427
880
|
}
|
|
@@ -430,28 +883,25 @@ export class OpenAIProvider {
|
|
|
430
883
|
createModel(modelId, config) {
|
|
431
884
|
const providerLabel = getOpenAIProviderLabel(config);
|
|
432
885
|
const providerName = getLLMOpenAIProviderName(config);
|
|
433
|
-
|
|
434
|
-
return createOpenAIResponsesRuntime({
|
|
435
|
-
apiKey: config.credential,
|
|
436
|
-
baseURL: config.baseURL,
|
|
437
|
-
name: providerLabel,
|
|
438
|
-
providerName,
|
|
439
|
-
fetch: config.fetch,
|
|
440
|
-
}, modelId);
|
|
441
|
-
}
|
|
442
|
-
return createOpenAIModelRuntime({
|
|
886
|
+
const runtimeConfig = {
|
|
443
887
|
apiKey: config.credential,
|
|
444
888
|
baseURL: config.baseURL,
|
|
445
889
|
name: providerLabel,
|
|
446
890
|
providerName,
|
|
447
891
|
fetch: config.fetch,
|
|
448
|
-
}
|
|
892
|
+
};
|
|
893
|
+
const responsesRuntime = createOpenAIResponsesRuntime(runtimeConfig, modelId);
|
|
894
|
+
if (isOpenAIReasoningModel(modelId, providerName)) {
|
|
895
|
+
return responsesRuntime;
|
|
896
|
+
}
|
|
897
|
+
return createOpenAIAdaptiveModelRuntime(createOpenAIModelRuntime(runtimeConfig, modelId), responsesRuntime);
|
|
449
898
|
}
|
|
450
899
|
createEmbedding(modelId, config) {
|
|
451
900
|
return createOpenAIEmbeddingRuntime({
|
|
452
901
|
apiKey: config.credential,
|
|
453
902
|
baseURL: config.baseURL,
|
|
454
903
|
name: getOpenAIProviderLabel(config),
|
|
904
|
+
providerName: getLLMOpenAIProviderName(config),
|
|
455
905
|
fetch: config.fetch,
|
|
456
906
|
}, modelId);
|
|
457
907
|
}
|