@tyvm/knowhow 0.0.156 → 0.0.157
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/package.json +1 -1
- package/src/agents/base/base.ts +15 -5
- package/src/chat/modules/AgentModule.ts +3 -3
- package/src/chat/modules/RemoteSyncModule.ts +45 -17
- package/src/clients/anthropic.ts +156 -79
- package/src/clients/attribution.ts +53 -0
- package/src/clients/http.ts +211 -84
- package/src/clients/index.ts +21 -9
- package/src/clients/openai.ts +441 -220
- package/src/clients/openrouter.ts +10 -3
- package/src/clients/pricing/openai.ts +15 -1
- package/src/clients/pricing/types.ts +15 -0
- package/src/clients/responseMetadata.ts +221 -0
- package/src/clients/types.ts +49 -4
- package/tests/unit/clients/anthropic.responseMetadata.test.ts +81 -0
- package/tests/unit/clients/attribution.test.ts +185 -0
- package/tests/unit/clients/http.responseMetadata.test.ts +86 -0
- package/tests/unit/clients/openai.responseMetadata.test.ts +90 -0
- package/tests/unit/clients/responseMetadata.test.ts +123 -0
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/base/base.js +14 -4
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.js +3 -3
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/modules/RemoteSyncModule.d.ts +1 -0
- package/ts_build/src/chat/modules/RemoteSyncModule.js +19 -7
- package/ts_build/src/chat/modules/RemoteSyncModule.js.map +1 -1
- package/ts_build/src/clients/anthropic.d.ts +1 -0
- package/ts_build/src/clients/anthropic.js +83 -38
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/attribution.d.ts +2 -0
- package/ts_build/src/clients/attribution.js +32 -0
- package/ts_build/src/clients/attribution.js.map +1 -0
- package/ts_build/src/clients/http.js +100 -20
- package/ts_build/src/clients/http.js.map +1 -1
- package/ts_build/src/clients/index.d.ts +2 -1
- package/ts_build/src/clients/index.js +6 -2
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/clients/openai.d.ts +2 -1
- package/ts_build/src/clients/openai.js +225 -99
- package/ts_build/src/clients/openai.js.map +1 -1
- package/ts_build/src/clients/openrouter.d.ts +2 -2
- package/ts_build/src/clients/openrouter.js +6 -2
- package/ts_build/src/clients/openrouter.js.map +1 -1
- package/ts_build/src/clients/pricing/openai.js +15 -1
- package/ts_build/src/clients/pricing/openai.js.map +1 -1
- package/ts_build/src/clients/pricing/types.d.ts +6 -0
- package/ts_build/src/clients/pricing/types.js.map +1 -1
- package/ts_build/src/clients/responseMetadata.d.ts +15 -0
- package/ts_build/src/clients/responseMetadata.js +141 -0
- package/ts_build/src/clients/responseMetadata.js.map +1 -0
- package/ts_build/src/clients/types.d.ts +18 -1
- package/ts_build/src/clients/types.js +2 -1
- package/ts_build/src/clients/types.js.map +1 -1
- package/ts_build/tests/unit/clients/anthropic.responseMetadata.test.d.ts +1 -0
- package/ts_build/tests/unit/clients/anthropic.responseMetadata.test.js +77 -0
- package/ts_build/tests/unit/clients/anthropic.responseMetadata.test.js.map +1 -0
- package/ts_build/tests/unit/clients/attribution.test.d.ts +1 -0
- package/ts_build/tests/unit/clients/attribution.test.js +153 -0
- package/ts_build/tests/unit/clients/attribution.test.js.map +1 -0
- package/ts_build/tests/unit/clients/http.responseMetadata.test.d.ts +1 -0
- package/ts_build/tests/unit/clients/http.responseMetadata.test.js +77 -0
- package/ts_build/tests/unit/clients/http.responseMetadata.test.js.map +1 -0
- package/ts_build/tests/unit/clients/openai.responseMetadata.test.d.ts +1 -0
- package/ts_build/tests/unit/clients/openai.responseMetadata.test.js +86 -0
- package/ts_build/tests/unit/clients/openai.responseMetadata.test.js.map +1 -0
- package/ts_build/tests/unit/clients/responseMetadata.test.d.ts +1 -0
- package/ts_build/tests/unit/clients/responseMetadata.test.js +96 -0
- package/ts_build/tests/unit/clients/responseMetadata.test.js.map +1 -0
package/src/clients/openai.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
2
|
import { getConfigSync } from "../config";
|
|
3
|
+
import { emitResponseMetadata } from "./responseMetadata";
|
|
3
4
|
import { OpenAiTextPricing } from "./pricing";
|
|
4
5
|
import { ContextLimits } from "./contextLimits";
|
|
5
6
|
import {
|
|
@@ -59,10 +60,39 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
59
60
|
this.setKey(apiKey);
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
private emitErrorMetadata(
|
|
64
|
+
options:
|
|
65
|
+
| CompletionOptions
|
|
66
|
+
| EmbeddingOptions
|
|
67
|
+
| AudioTranscriptionOptions
|
|
68
|
+
| AudioGenerationOptions,
|
|
69
|
+
error: unknown
|
|
70
|
+
) {
|
|
71
|
+
if (!error || typeof error !== "object") return;
|
|
72
|
+
const apiError = error as {
|
|
73
|
+
status?: number;
|
|
74
|
+
headers?: Headers | Record<string, unknown>;
|
|
75
|
+
request_id?: string;
|
|
76
|
+
requestID?: string;
|
|
77
|
+
response?: Response;
|
|
78
|
+
};
|
|
79
|
+
const statusCode = apiError.status ?? apiError.response?.status;
|
|
80
|
+
const headers = apiError.headers ?? apiError.response?.headers;
|
|
81
|
+
if (typeof statusCode === "number" || headers) {
|
|
82
|
+
emitResponseMetadata(options, {
|
|
83
|
+
statusCode,
|
|
84
|
+
headers,
|
|
85
|
+
requestId: apiError.request_id ?? apiError.requestID,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
62
90
|
setKey(apiKey: string) {
|
|
63
91
|
this.apiKey = apiKey;
|
|
64
92
|
this.client = new OpenAI({
|
|
65
93
|
apiKey,
|
|
94
|
+
// SDK retries hide individual responses, which breaks response-scoped telemetry.
|
|
95
|
+
maxRetries: 0,
|
|
66
96
|
...(config?.openaiBaseUrl && { baseURL: config.openaiBaseUrl }),
|
|
67
97
|
});
|
|
68
98
|
}
|
|
@@ -77,7 +107,9 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
77
107
|
return this.detectReasoningEffort(messages);
|
|
78
108
|
}
|
|
79
109
|
|
|
80
|
-
detectReasoningEffort(
|
|
110
|
+
detectReasoningEffort(
|
|
111
|
+
messages: CompletionOptions["messages"]
|
|
112
|
+
): "low" | "medium" | "high" {
|
|
81
113
|
const effortMap: Record<string, "low" | "medium" | "high"> = {
|
|
82
114
|
ultrathink: "high",
|
|
83
115
|
"think hard": "high",
|
|
@@ -110,8 +142,11 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
110
142
|
return "medium"; // Default to medium if no specific effort is mentioned
|
|
111
143
|
}
|
|
112
144
|
|
|
113
|
-
resolveReasoningEffort(
|
|
114
|
-
|
|
145
|
+
resolveReasoningEffort(
|
|
146
|
+
options: CompletionOptions
|
|
147
|
+
): "low" | "medium" | "high" {
|
|
148
|
+
const effort =
|
|
149
|
+
options.reasoning_effort ?? this.detectReasoningEffort(options.messages);
|
|
115
150
|
// Chat Completions only accepts low/medium/high. Preserve newer values on
|
|
116
151
|
// the Responses API, but map them to the nearest legacy level here.
|
|
117
152
|
if (effort === "none" || effort === "minimal") return "low";
|
|
@@ -125,7 +160,8 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
125
160
|
* If the requested level is not supported, picks the lowest supported level.
|
|
126
161
|
*/
|
|
127
162
|
resolveReasoningEffortForModel(options: CompletionOptions): string {
|
|
128
|
-
const raw =
|
|
163
|
+
const raw =
|
|
164
|
+
options.reasoning_effort ?? this.detectReasoningEffort(options.messages);
|
|
129
165
|
// Anthropic calls its highest level `max`; OpenAI calls it `xhigh`.
|
|
130
166
|
// Accept both provider-neutral CLI spellings.
|
|
131
167
|
const requested = raw === "max" ? "xhigh" : raw;
|
|
@@ -175,10 +211,12 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
175
211
|
content: Array.isArray(msg.content)
|
|
176
212
|
? textParts.length
|
|
177
213
|
? textParts
|
|
178
|
-
: [
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
214
|
+
: [
|
|
215
|
+
{
|
|
216
|
+
type: "text",
|
|
217
|
+
text: "Image output attached in the following user message.",
|
|
218
|
+
},
|
|
219
|
+
]
|
|
182
220
|
: msg.content || "",
|
|
183
221
|
};
|
|
184
222
|
|
|
@@ -199,15 +237,13 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
199
237
|
for (const part of content) {
|
|
200
238
|
if (part.type === "text") {
|
|
201
239
|
result.push({ type: "input_text", text: part.text });
|
|
202
|
-
}
|
|
203
|
-
else if (part.type === "image_url") {
|
|
240
|
+
} else if (part.type === "image_url") {
|
|
204
241
|
result.push({
|
|
205
242
|
type: "input_image",
|
|
206
243
|
image_url: part.image_url.url,
|
|
207
244
|
detail: part.image_url.detail || "auto",
|
|
208
245
|
});
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
246
|
+
} else {
|
|
211
247
|
throw new Error(
|
|
212
248
|
`OpenAI Responses API conversion does not support ${part.type} content.`
|
|
213
249
|
);
|
|
@@ -226,20 +262,41 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
226
262
|
|
|
227
263
|
const openaiMessages = this.toChatCompletionMessages(options.messages);
|
|
228
264
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
265
|
+
let result;
|
|
266
|
+
try {
|
|
267
|
+
result = await this.client.chat.completions
|
|
268
|
+
.create(
|
|
269
|
+
{
|
|
270
|
+
model: options.model,
|
|
271
|
+
messages: openaiMessages,
|
|
272
|
+
max_tokens: options.max_tokens,
|
|
273
|
+
...(OpenAiReasoningModels.includes(options.model) &&
|
|
274
|
+
!this.isReasoningDisabled(options) && {
|
|
275
|
+
max_tokens: undefined,
|
|
276
|
+
max_completion_tokens: Math.max(
|
|
277
|
+
options.max_tokens ?? 0,
|
|
278
|
+
16_000
|
|
279
|
+
),
|
|
280
|
+
reasoning_effort: this.resolveReasoningEffort(options),
|
|
281
|
+
}),
|
|
282
|
+
...(options.tools?.length && {
|
|
283
|
+
tools: options.tools,
|
|
284
|
+
tool_choice: options.tool_choice ?? "auto",
|
|
285
|
+
}),
|
|
286
|
+
},
|
|
287
|
+
{ signal: options.signal }
|
|
288
|
+
)
|
|
289
|
+
.withResponse();
|
|
290
|
+
} catch (error) {
|
|
291
|
+
this.emitErrorMetadata(options, error);
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
const { data: response, response: nativeResponse, request_id } = result;
|
|
295
|
+
emitResponseMetadata(options, {
|
|
296
|
+
statusCode: nativeResponse.status,
|
|
297
|
+
headers: nativeResponse.headers,
|
|
298
|
+
requestId: request_id,
|
|
299
|
+
});
|
|
243
300
|
|
|
244
301
|
const usdCost = this.calculateCost(options.model, response.usage);
|
|
245
302
|
|
|
@@ -255,19 +312,24 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
255
312
|
})),
|
|
256
313
|
|
|
257
314
|
model: options.model,
|
|
258
|
-
usage: response.usage
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
315
|
+
usage: response.usage
|
|
316
|
+
? {
|
|
317
|
+
prompt_tokens: response.usage.prompt_tokens ?? 0,
|
|
318
|
+
completion_tokens: response.usage.completion_tokens ?? 0,
|
|
319
|
+
total_tokens: response.usage.total_tokens,
|
|
320
|
+
prompt_tokens_details: {
|
|
321
|
+
cached_tokens:
|
|
322
|
+
response.usage.prompt_tokens_details?.cached_tokens ?? 0,
|
|
323
|
+
},
|
|
324
|
+
...(response.usage.completion_tokens_details?.reasoning_tokens !=
|
|
325
|
+
null && {
|
|
326
|
+
output_tokens_details: {
|
|
327
|
+
reasoning_tokens:
|
|
328
|
+
response.usage.completion_tokens_details.reasoning_tokens,
|
|
329
|
+
},
|
|
330
|
+
}),
|
|
331
|
+
}
|
|
332
|
+
: undefined,
|
|
271
333
|
usd_cost: usdCost,
|
|
272
334
|
};
|
|
273
335
|
}
|
|
@@ -279,7 +341,12 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
279
341
|
*/
|
|
280
342
|
async *createChatCompletionStream(
|
|
281
343
|
options: CompletionOptions
|
|
282
|
-
): AsyncGenerator<{
|
|
344
|
+
): AsyncGenerator<{
|
|
345
|
+
delta?: string;
|
|
346
|
+
done: boolean;
|
|
347
|
+
usage?: CompletionResponse["usage"];
|
|
348
|
+
usd_cost?: number;
|
|
349
|
+
}> {
|
|
283
350
|
if (OpenAiResponsesOnlyModels.includes(options.model)) {
|
|
284
351
|
// Fallback: non-streaming for Responses-only models
|
|
285
352
|
const result = await this.createChatCompletion(options);
|
|
@@ -290,19 +357,36 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
290
357
|
|
|
291
358
|
const openaiMessages = this.toChatCompletionMessages(options.messages);
|
|
292
359
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
360
|
+
let result;
|
|
361
|
+
try {
|
|
362
|
+
result = await this.client.chat.completions
|
|
363
|
+
.create(
|
|
364
|
+
{
|
|
365
|
+
model: options.model,
|
|
366
|
+
messages: openaiMessages,
|
|
367
|
+
max_tokens: options.max_tokens,
|
|
368
|
+
stream: true,
|
|
369
|
+
stream_options: { include_usage: true },
|
|
370
|
+
...(options.tools?.length && {
|
|
371
|
+
tools: options.tools,
|
|
372
|
+
tool_choice: options.tool_choice ?? "auto",
|
|
373
|
+
}),
|
|
374
|
+
},
|
|
375
|
+
{ signal: options.signal }
|
|
376
|
+
)
|
|
377
|
+
.withResponse();
|
|
378
|
+
} catch (error) {
|
|
379
|
+
this.emitErrorMetadata(options, error);
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
const { data: stream, response: nativeResponse, request_id } = result;
|
|
383
|
+
emitResponseMetadata(options, {
|
|
384
|
+
statusCode: nativeResponse.status,
|
|
385
|
+
headers: nativeResponse.headers,
|
|
386
|
+
requestId: request_id,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
let usage: CompletionResponse["usage"] | undefined;
|
|
306
390
|
for await (const chunk of stream) {
|
|
307
391
|
const delta = chunk.choices[0]?.delta?.content;
|
|
308
392
|
if (delta) {
|
|
@@ -317,7 +401,10 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
317
401
|
}
|
|
318
402
|
}
|
|
319
403
|
const usdCost = usage
|
|
320
|
-
? this.calculateCost(
|
|
404
|
+
? this.calculateCost(
|
|
405
|
+
options.model,
|
|
406
|
+
usage as OpenAI.ChatCompletion["usage"]
|
|
407
|
+
)
|
|
321
408
|
: undefined;
|
|
322
409
|
yield { done: true, usage, usd_cost: usdCost };
|
|
323
410
|
}
|
|
@@ -345,18 +432,27 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
345
432
|
let inString = false;
|
|
346
433
|
let escaped = false;
|
|
347
434
|
for (const ch of repaired) {
|
|
348
|
-
if (escaped) {
|
|
349
|
-
|
|
350
|
-
|
|
435
|
+
if (escaped) {
|
|
436
|
+
escaped = false;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (ch === "\\" && inString) {
|
|
440
|
+
escaped = true;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (ch === '"') {
|
|
444
|
+
inString = !inString;
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
351
447
|
if (!inString) {
|
|
352
|
-
if (ch ===
|
|
353
|
-
else if (ch ===
|
|
448
|
+
if (ch === "{" || ch === "[") depth++;
|
|
449
|
+
else if (ch === "}" || ch === "]") depth--;
|
|
354
450
|
}
|
|
355
451
|
}
|
|
356
452
|
// If we're inside a string, close it
|
|
357
453
|
if (inString) repaired += '"';
|
|
358
454
|
// Close any open objects/arrays
|
|
359
|
-
for (let i = 0; i < depth; i++) repaired +=
|
|
455
|
+
for (let i = 0; i < depth; i++) repaired += "}";
|
|
360
456
|
try {
|
|
361
457
|
JSON.parse(repaired);
|
|
362
458
|
return repaired;
|
|
@@ -374,7 +470,11 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
374
470
|
*/
|
|
375
471
|
private getReasoningItems(msg: any): any[] {
|
|
376
472
|
const details = (msg as any)._reasoning_details;
|
|
377
|
-
if (
|
|
473
|
+
if (
|
|
474
|
+
details &&
|
|
475
|
+
details.provider === "openai" &&
|
|
476
|
+
Array.isArray(details.items)
|
|
477
|
+
) {
|
|
378
478
|
return details.items;
|
|
379
479
|
}
|
|
380
480
|
return (msg as any)._reasoning_items ?? [];
|
|
@@ -384,16 +484,15 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
384
484
|
options: CompletionOptions
|
|
385
485
|
): Promise<CompletionResponse> {
|
|
386
486
|
// Extract system message to use as instructions
|
|
387
|
-
const systemMessages = options.messages.filter(
|
|
388
|
-
(m) => m.role === "system"
|
|
389
|
-
);
|
|
487
|
+
const systemMessages = options.messages.filter((m) => m.role === "system");
|
|
390
488
|
const nonSystemMessages = options.messages.filter(
|
|
391
489
|
(m) => m.role !== "system"
|
|
392
490
|
);
|
|
393
|
-
const instructions =
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
491
|
+
const instructions =
|
|
492
|
+
systemMessages
|
|
493
|
+
.map((m) => (typeof m.content === "string" ? m.content : ""))
|
|
494
|
+
.join("\n")
|
|
495
|
+
.trim() || undefined;
|
|
397
496
|
|
|
398
497
|
// Convert chat messages to Responses API input items
|
|
399
498
|
// The Responses API accepts: user/assistant/system messages and function_call_output items
|
|
@@ -401,66 +500,71 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
401
500
|
const MAX_TOOL_OUTPUT = 10_485_000;
|
|
402
501
|
const truncateToolOutput = (s: string) =>
|
|
403
502
|
s.length > MAX_TOOL_OUTPUT
|
|
404
|
-
? s.slice(0, MAX_TOOL_OUTPUT) +
|
|
503
|
+
? s.slice(0, MAX_TOOL_OUTPUT) +
|
|
504
|
+
"\n\n[TRUNCATED: output exceeded 10MB API limit]"
|
|
405
505
|
: s;
|
|
406
|
-
const input: any[] = nonSystemMessages
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
506
|
+
const input: any[] = nonSystemMessages
|
|
507
|
+
.map((msg) => {
|
|
508
|
+
if (msg.role === "tool") {
|
|
509
|
+
// OpenAI function outputs cannot directly contain image content. Send
|
|
510
|
+
// the required function output, then attach images as a user message.
|
|
511
|
+
const parts = Array.isArray(msg.content) ? msg.content : [];
|
|
512
|
+
const imageParts = parts.filter((part) => part.type === "image_url");
|
|
513
|
+
const text = parts
|
|
514
|
+
.filter((part) => part.type === "text")
|
|
515
|
+
.map((part: any) => part.text)
|
|
516
|
+
.join("\n");
|
|
517
|
+
const toolOutput = {
|
|
518
|
+
type: "function_call_output",
|
|
519
|
+
call_id: msg.tool_call_id,
|
|
520
|
+
output: truncateToolOutput(
|
|
521
|
+
typeof msg.content === "string"
|
|
522
|
+
? msg.content
|
|
523
|
+
: text || "Image output attached in the following user message."
|
|
524
|
+
),
|
|
525
|
+
};
|
|
526
|
+
if (!imageParts.length) return toolOutput;
|
|
527
|
+
return [
|
|
528
|
+
toolOutput,
|
|
529
|
+
{ role: "user", content: this.toResponseContent(imageParts) },
|
|
530
|
+
];
|
|
531
|
+
}
|
|
532
|
+
if (msg.role === "assistant" && msg.tool_calls?.length) {
|
|
533
|
+
// assistant message with tool calls → function_call items.
|
|
534
|
+
// If the prior turn captured reasoning items (encrypted_content path),
|
|
535
|
+
// re-inject them *before* the function_call items so the model keeps its
|
|
536
|
+
// chain-of-thought instead of re-deriving it from scratch.
|
|
537
|
+
const reasoningItems = this.getReasoningItems(msg);
|
|
538
|
+
return [
|
|
539
|
+
...reasoningItems,
|
|
540
|
+
...msg.tool_calls.map((tc) => ({
|
|
541
|
+
type: "function_call",
|
|
542
|
+
// id must start with 'fc_'; call_id is the original call_ ID used for function_call_output matching
|
|
543
|
+
id: tc.id.startsWith("fc") ? tc.id : `fc_${tc.id}`,
|
|
544
|
+
call_id: tc.id,
|
|
545
|
+
name: tc.function.name,
|
|
546
|
+
arguments: tc.function.arguments,
|
|
547
|
+
})),
|
|
548
|
+
];
|
|
549
|
+
}
|
|
550
|
+
if (msg.role === "assistant" && this.getReasoningItems(msg).length) {
|
|
551
|
+
// assistant message (no tool calls) that carried reasoning items →
|
|
552
|
+
// re-inject reasoning items, then the visible message content.
|
|
553
|
+
return [
|
|
554
|
+
...this.getReasoningItems(msg),
|
|
555
|
+
{
|
|
556
|
+
role: msg.role,
|
|
557
|
+
content: this.toResponseContent(msg.content),
|
|
558
|
+
},
|
|
559
|
+
];
|
|
560
|
+
}
|
|
561
|
+
// Regular user/assistant message
|
|
562
|
+
return {
|
|
563
|
+
role: msg.role,
|
|
564
|
+
content: this.toResponseContent(msg.content),
|
|
422
565
|
};
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
toolOutput,
|
|
426
|
-
{ role: "user", content: this.toResponseContent(imageParts) },
|
|
427
|
-
];
|
|
428
|
-
}
|
|
429
|
-
if (msg.role === "assistant" && msg.tool_calls?.length) {
|
|
430
|
-
// assistant message with tool calls → function_call items.
|
|
431
|
-
// If the prior turn captured reasoning items (encrypted_content path),
|
|
432
|
-
// re-inject them *before* the function_call items so the model keeps its
|
|
433
|
-
// chain-of-thought instead of re-deriving it from scratch.
|
|
434
|
-
const reasoningItems = this.getReasoningItems(msg);
|
|
435
|
-
return [
|
|
436
|
-
...reasoningItems,
|
|
437
|
-
...msg.tool_calls.map((tc) => ({
|
|
438
|
-
type: "function_call",
|
|
439
|
-
// id must start with 'fc_'; call_id is the original call_ ID used for function_call_output matching
|
|
440
|
-
id: tc.id.startsWith("fc") ? tc.id : `fc_${tc.id}`,
|
|
441
|
-
call_id: tc.id,
|
|
442
|
-
name: tc.function.name,
|
|
443
|
-
arguments: tc.function.arguments,
|
|
444
|
-
})),
|
|
445
|
-
];
|
|
446
|
-
}
|
|
447
|
-
if (msg.role === "assistant" && this.getReasoningItems(msg).length) {
|
|
448
|
-
// assistant message (no tool calls) that carried reasoning items →
|
|
449
|
-
// re-inject reasoning items, then the visible message content.
|
|
450
|
-
return [
|
|
451
|
-
...this.getReasoningItems(msg),
|
|
452
|
-
{
|
|
453
|
-
role: msg.role,
|
|
454
|
-
content: this.toResponseContent(msg.content),
|
|
455
|
-
},
|
|
456
|
-
];
|
|
457
|
-
}
|
|
458
|
-
// Regular user/assistant message
|
|
459
|
-
return {
|
|
460
|
-
role: msg.role,
|
|
461
|
-
content: this.toResponseContent(msg.content),
|
|
462
|
-
};
|
|
463
|
-
}).flat();
|
|
566
|
+
})
|
|
567
|
+
.flat();
|
|
464
568
|
|
|
465
569
|
// Convert Chat Completions tool definitions to Responses API FunctionTool format
|
|
466
570
|
const tools = options.tools?.map((tool) => ({
|
|
@@ -481,38 +585,53 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
481
585
|
OpenAiReasoningModels.includes(options.model) &&
|
|
482
586
|
!this.isReasoningDisabled(options);
|
|
483
587
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
588
|
+
let result;
|
|
589
|
+
try {
|
|
590
|
+
result = await this.client.responses
|
|
591
|
+
.create({
|
|
592
|
+
model: options.model as any,
|
|
593
|
+
input,
|
|
594
|
+
...(instructions && { instructions }),
|
|
595
|
+
...(store && { store: true }),
|
|
596
|
+
...(!store && { store: false }),
|
|
597
|
+
...(options.previous_response_id && {
|
|
598
|
+
previous_response_id: options.previous_response_id,
|
|
599
|
+
}),
|
|
600
|
+
// When stateless, request encrypted reasoning so we can re-inject it next turn.
|
|
601
|
+
...(!store &&
|
|
602
|
+
wantsReasoning && {
|
|
603
|
+
include: ["reasoning.encrypted_content"],
|
|
604
|
+
}),
|
|
605
|
+
// Don't limit max_output_tokens for Responses API - codex truncates tool call arguments when limited
|
|
606
|
+
...(OpenAiReasoningModels.includes(options.model) && {
|
|
607
|
+
reasoning: {
|
|
608
|
+
...(this.isReasoningDisabled(options)
|
|
609
|
+
? { effort: "none" }
|
|
610
|
+
: {
|
|
611
|
+
effort: this.resolveReasoningEffortForModel(options),
|
|
612
|
+
...(options.reasoning_summary && { summary: "auto" }),
|
|
613
|
+
}),
|
|
614
|
+
},
|
|
615
|
+
}),
|
|
616
|
+
...(wantsReasoning && {
|
|
617
|
+
max_output_tokens: Math.max(options.max_tokens || 0, 16000),
|
|
618
|
+
}),
|
|
619
|
+
...(tools?.length && {
|
|
620
|
+
tools,
|
|
621
|
+
tool_choice: options.tool_choice ?? "auto",
|
|
622
|
+
}),
|
|
623
|
+
} as any)
|
|
624
|
+
.withResponse();
|
|
625
|
+
} catch (error) {
|
|
626
|
+
this.emitErrorMetadata(options, error);
|
|
627
|
+
throw error;
|
|
628
|
+
}
|
|
629
|
+
const { data: response, response: nativeResponse, request_id } = result;
|
|
630
|
+
emitResponseMetadata(options, {
|
|
631
|
+
statusCode: nativeResponse.status,
|
|
632
|
+
headers: nativeResponse.headers,
|
|
633
|
+
requestId: request_id,
|
|
634
|
+
});
|
|
516
635
|
|
|
517
636
|
// Map Responses API usage to Chat Completions usage format
|
|
518
637
|
const usage = response.usage
|
|
@@ -525,9 +644,11 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
525
644
|
cached_tokens:
|
|
526
645
|
response.usage.input_tokens_details?.cached_tokens ?? 0,
|
|
527
646
|
},
|
|
528
|
-
...((response.usage as any).output_tokens_details?.reasoning_tokens !=
|
|
647
|
+
...((response.usage as any).output_tokens_details?.reasoning_tokens !=
|
|
648
|
+
null && {
|
|
529
649
|
output_tokens_details: {
|
|
530
|
-
reasoning_tokens: (response.usage as any).output_tokens_details
|
|
650
|
+
reasoning_tokens: (response.usage as any).output_tokens_details
|
|
651
|
+
.reasoning_tokens,
|
|
531
652
|
},
|
|
532
653
|
}),
|
|
533
654
|
}
|
|
@@ -574,7 +695,9 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
574
695
|
try {
|
|
575
696
|
JSON.parse(repairedArgs);
|
|
576
697
|
} catch (e) {
|
|
577
|
-
console.warn(
|
|
698
|
+
console.warn(
|
|
699
|
+
`[Responses API] Invalid JSON arguments for ${fc.name} after repair: ${e.message}`
|
|
700
|
+
);
|
|
578
701
|
}
|
|
579
702
|
toolCalls.push({
|
|
580
703
|
// Store call_id so function_call_output.call_id matches it in subsequent turns
|
|
@@ -640,16 +763,25 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
640
763
|
};
|
|
641
764
|
}
|
|
642
765
|
|
|
643
|
-
|
|
644
766
|
pricesPerMillion() {
|
|
645
767
|
return OpenAiTextPricing;
|
|
646
768
|
}
|
|
647
769
|
|
|
648
|
-
getPricing(
|
|
770
|
+
getPricing(
|
|
771
|
+
model?: string
|
|
772
|
+
):
|
|
773
|
+
| import("./pricing/types").ModelPricing
|
|
774
|
+
| Record<string, import("./pricing/types").ModelPricing>
|
|
775
|
+
| undefined {
|
|
649
776
|
if (model !== undefined) {
|
|
650
|
-
return OpenAiTextPricing[model as keyof typeof OpenAiTextPricing] as
|
|
777
|
+
return OpenAiTextPricing[model as keyof typeof OpenAiTextPricing] as
|
|
778
|
+
| import("./pricing/types").ModelPricing
|
|
779
|
+
| undefined;
|
|
651
780
|
}
|
|
652
|
-
return OpenAiTextPricing as unknown as Record<
|
|
781
|
+
return OpenAiTextPricing as unknown as Record<
|
|
782
|
+
string,
|
|
783
|
+
import("./pricing/types").ModelPricing
|
|
784
|
+
>;
|
|
653
785
|
}
|
|
654
786
|
|
|
655
787
|
calculateCost(
|
|
@@ -668,10 +800,12 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
668
800
|
("prompt_tokens_details" in usage &&
|
|
669
801
|
usage.prompt_tokens_details?.cached_tokens) ||
|
|
670
802
|
0;
|
|
671
|
-
const cachedInputCost =
|
|
803
|
+
const cachedInputCost =
|
|
804
|
+
(cachedInputTokens * (pricing.cached_input ?? 0)) / 1e6;
|
|
672
805
|
|
|
673
806
|
const inputTokens = usage.prompt_tokens;
|
|
674
|
-
const inputCost =
|
|
807
|
+
const inputCost =
|
|
808
|
+
((inputTokens - cachedInputTokens) * (pricing.input ?? 0)) / 1e6;
|
|
675
809
|
|
|
676
810
|
const outputTokens =
|
|
677
811
|
("completion_tokens" in usage && usage?.completion_tokens) || 0;
|
|
@@ -684,7 +818,9 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
684
818
|
async getModels(modality?: ModelModality): Promise<{ id: string }[]> {
|
|
685
819
|
if (modality) {
|
|
686
820
|
const map: Partial<Record<ModelModality, string[]>> = {
|
|
687
|
-
completion: [
|
|
821
|
+
completion: [
|
|
822
|
+
...new Set([...OpenAiChatModels, ...OpenAiResponsesOnlyModels]),
|
|
823
|
+
],
|
|
688
824
|
embedding: OpenAiEmbeddingModelsList,
|
|
689
825
|
image: OpenAiImageModels,
|
|
690
826
|
audio: [...OpenAiTTSModels, ...OpenAiTranscriptionModels],
|
|
@@ -701,9 +837,27 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
701
837
|
}
|
|
702
838
|
|
|
703
839
|
async createEmbedding(options: EmbeddingOptions): Promise<EmbeddingResponse> {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
840
|
+
let result;
|
|
841
|
+
try {
|
|
842
|
+
result = await this.client.embeddings
|
|
843
|
+
.create({
|
|
844
|
+
input: options.input,
|
|
845
|
+
model: options.model,
|
|
846
|
+
})
|
|
847
|
+
.withResponse();
|
|
848
|
+
} catch (error) {
|
|
849
|
+
this.emitErrorMetadata(options, error);
|
|
850
|
+
throw error;
|
|
851
|
+
}
|
|
852
|
+
const {
|
|
853
|
+
data: openAiEmbedding,
|
|
854
|
+
response: nativeResponse,
|
|
855
|
+
request_id,
|
|
856
|
+
} = result;
|
|
857
|
+
emitResponseMetadata(options, {
|
|
858
|
+
statusCode: nativeResponse.status,
|
|
859
|
+
headers: nativeResponse.headers,
|
|
860
|
+
requestId: request_id,
|
|
707
861
|
});
|
|
708
862
|
|
|
709
863
|
return {
|
|
@@ -724,19 +878,39 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
724
878
|
file = await OpenAI.toFile(options.file, fileName);
|
|
725
879
|
}
|
|
726
880
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
881
|
+
let result;
|
|
882
|
+
try {
|
|
883
|
+
result = await this.client.audio.transcriptions
|
|
884
|
+
.create(
|
|
885
|
+
{
|
|
886
|
+
file,
|
|
887
|
+
model: options.model || "whisper-1",
|
|
888
|
+
language: options.language,
|
|
889
|
+
prompt: options.prompt,
|
|
890
|
+
response_format: options.response_format || "verbose_json",
|
|
891
|
+
temperature: options.temperature,
|
|
892
|
+
},
|
|
893
|
+
{ signal: options.signal }
|
|
894
|
+
)
|
|
895
|
+
.withResponse();
|
|
896
|
+
} catch (error) {
|
|
897
|
+
this.emitErrorMetadata(options, error);
|
|
898
|
+
throw error;
|
|
899
|
+
}
|
|
900
|
+
const { data: response, response: nativeResponse, request_id } = result;
|
|
901
|
+
emitResponseMetadata(options, {
|
|
902
|
+
statusCode: nativeResponse.status,
|
|
903
|
+
headers: nativeResponse.headers,
|
|
904
|
+
requestId: request_id,
|
|
905
|
+
});
|
|
735
906
|
|
|
736
907
|
// Calculate cost: $0.006 per minute for Whisper
|
|
737
|
-
const duration =
|
|
738
|
-
|
|
739
|
-
|
|
908
|
+
const duration =
|
|
909
|
+
typeof response === "object" &&
|
|
910
|
+
"duration" in response &&
|
|
911
|
+
typeof response.duration === "number"
|
|
912
|
+
? response.duration
|
|
913
|
+
: undefined;
|
|
740
914
|
const usdCost = duration ? (duration / 60) * 0.006 : undefined;
|
|
741
915
|
|
|
742
916
|
if (typeof response === "string") {
|
|
@@ -761,13 +935,30 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
761
935
|
async createAudioGeneration(
|
|
762
936
|
options: AudioGenerationOptions
|
|
763
937
|
): Promise<AudioGenerationResponse> {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
938
|
+
let result;
|
|
939
|
+
try {
|
|
940
|
+
result = await this.client.audio.speech
|
|
941
|
+
.create(
|
|
942
|
+
{
|
|
943
|
+
model: options.model,
|
|
944
|
+
input: options.input,
|
|
945
|
+
voice: options.voice as any,
|
|
946
|
+
response_format: options.response_format || "mp3",
|
|
947
|
+
speed: options.speed,
|
|
948
|
+
},
|
|
949
|
+
{ signal: options.signal }
|
|
950
|
+
)
|
|
951
|
+
.withResponse();
|
|
952
|
+
} catch (error) {
|
|
953
|
+
this.emitErrorMetadata(options, error);
|
|
954
|
+
throw error;
|
|
955
|
+
}
|
|
956
|
+
const { data: response, response: nativeResponse, request_id } = result;
|
|
957
|
+
emitResponseMetadata(options, {
|
|
958
|
+
statusCode: nativeResponse.status,
|
|
959
|
+
headers: nativeResponse.headers,
|
|
960
|
+
requestId: request_id,
|
|
961
|
+
});
|
|
771
962
|
|
|
772
963
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
773
964
|
|
|
@@ -787,16 +978,19 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
787
978
|
async createImageGeneration(
|
|
788
979
|
options: ImageGenerationOptions
|
|
789
980
|
): Promise<ImageGenerationResponse> {
|
|
790
|
-
const response = await this.client.images.generate(
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
981
|
+
const response = await this.client.images.generate(
|
|
982
|
+
{
|
|
983
|
+
model: options.model,
|
|
984
|
+
prompt: options.prompt,
|
|
985
|
+
n: options.n,
|
|
986
|
+
size: options.size,
|
|
987
|
+
quality: options.quality,
|
|
988
|
+
style: options.style,
|
|
989
|
+
response_format: options.response_format,
|
|
990
|
+
user: options.user,
|
|
991
|
+
},
|
|
992
|
+
{ signal: options.signal }
|
|
993
|
+
);
|
|
800
994
|
|
|
801
995
|
// Cost calculation varies by model and settings
|
|
802
996
|
// DALL-E 3: $0.040-$0.120 per image depending on quality/size
|
|
@@ -873,16 +1067,23 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
873
1067
|
};
|
|
874
1068
|
}
|
|
875
1069
|
|
|
876
|
-
async getVideoStatus(
|
|
1070
|
+
async getVideoStatus(
|
|
1071
|
+
options: VideoStatusOptions
|
|
1072
|
+
): Promise<VideoStatusResponse> {
|
|
877
1073
|
const apiKey = this.apiKey || process.env.OPENAI_KEY;
|
|
878
1074
|
if (!apiKey) throw new Error("OpenAI API key not set");
|
|
879
|
-
const response = await fetch(
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1075
|
+
const response = await fetch(
|
|
1076
|
+
`https://api.openai.com/v1/videos/${options.jobId}`,
|
|
1077
|
+
{
|
|
1078
|
+
method: "GET",
|
|
1079
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1080
|
+
}
|
|
1081
|
+
);
|
|
883
1082
|
if (!response.ok) {
|
|
884
1083
|
const errorText = await response.text();
|
|
885
|
-
throw new Error(
|
|
1084
|
+
throw new Error(
|
|
1085
|
+
`OpenAI getVideoStatus failed: ${response.status} ${errorText}`
|
|
1086
|
+
);
|
|
886
1087
|
}
|
|
887
1088
|
const data = await response.json();
|
|
888
1089
|
let status: VideoStatusResponse["status"] = "in_progress";
|
|
@@ -898,18 +1099,25 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
898
1099
|
};
|
|
899
1100
|
}
|
|
900
1101
|
|
|
901
|
-
async downloadVideo(
|
|
1102
|
+
async downloadVideo(
|
|
1103
|
+
options: FileDownloadOptions
|
|
1104
|
+
): Promise<FileDownloadResponse> {
|
|
902
1105
|
const apiKey = this.apiKey || process.env.OPENAI_KEY;
|
|
903
1106
|
if (!apiKey) throw new Error("OpenAI API key not set");
|
|
904
1107
|
const fileId = options.fileId;
|
|
905
1108
|
if (!fileId) throw new Error("downloadVideo requires fileId (the jobId)");
|
|
906
|
-
const response = await fetch(
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1109
|
+
const response = await fetch(
|
|
1110
|
+
`https://api.openai.com/v1/videos/${fileId}/content`,
|
|
1111
|
+
{
|
|
1112
|
+
method: "GET",
|
|
1113
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1114
|
+
}
|
|
1115
|
+
);
|
|
910
1116
|
if (!response.ok) {
|
|
911
1117
|
const errorText = await response.text();
|
|
912
|
-
throw new Error(
|
|
1118
|
+
throw new Error(
|
|
1119
|
+
`OpenAI downloadVideo failed: ${response.status} ${errorText}`
|
|
1120
|
+
);
|
|
913
1121
|
}
|
|
914
1122
|
const mimeType = response.headers.get("content-type") || "video/mp4";
|
|
915
1123
|
return { data: Buffer.from(await response.arrayBuffer()), mimeType };
|
|
@@ -920,7 +1128,9 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
920
1128
|
if (!apiKey) throw new Error("OpenAI API key not set");
|
|
921
1129
|
const formData = new FormData();
|
|
922
1130
|
formData.append("purpose", "assistants");
|
|
923
|
-
const blob = new Blob([new Uint8Array(options.data)], {
|
|
1131
|
+
const blob = new Blob([new Uint8Array(options.data)], {
|
|
1132
|
+
type: options.mimeType || "application/octet-stream",
|
|
1133
|
+
});
|
|
924
1134
|
formData.append("file", blob, options.fileName || "upload");
|
|
925
1135
|
const response = await fetch("https://api.openai.com/v1/files", {
|
|
926
1136
|
method: "POST",
|
|
@@ -929,31 +1139,42 @@ export class GenericOpenAiClient implements GenericClient {
|
|
|
929
1139
|
});
|
|
930
1140
|
if (!response.ok) {
|
|
931
1141
|
const errorText = await response.text();
|
|
932
|
-
throw new Error(
|
|
1142
|
+
throw new Error(
|
|
1143
|
+
`OpenAI uploadFile failed: ${response.status} ${errorText}`
|
|
1144
|
+
);
|
|
933
1145
|
}
|
|
934
1146
|
const data = await response.json();
|
|
935
1147
|
return { fileId: data.id, uri: data.uri };
|
|
936
1148
|
}
|
|
937
1149
|
|
|
938
|
-
async downloadFile(
|
|
1150
|
+
async downloadFile(
|
|
1151
|
+
options: FileDownloadOptions
|
|
1152
|
+
): Promise<FileDownloadResponse> {
|
|
939
1153
|
const apiKey = this.apiKey || process.env.OPENAI_KEY;
|
|
940
1154
|
if (!apiKey) throw new Error("OpenAI API key not set");
|
|
941
1155
|
const fileId = options.fileId;
|
|
942
1156
|
if (!fileId) throw new Error("downloadFile requires fileId");
|
|
943
|
-
const response = await fetch(
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1157
|
+
const response = await fetch(
|
|
1158
|
+
`https://api.openai.com/v1/files/${fileId}/content`,
|
|
1159
|
+
{
|
|
1160
|
+
method: "GET",
|
|
1161
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1162
|
+
}
|
|
1163
|
+
);
|
|
947
1164
|
if (!response.ok) {
|
|
948
1165
|
const errorText = await response.text();
|
|
949
|
-
throw new Error(
|
|
1166
|
+
throw new Error(
|
|
1167
|
+
`OpenAI downloadFile failed: ${response.status} ${errorText}`
|
|
1168
|
+
);
|
|
950
1169
|
}
|
|
951
1170
|
const mimeType = response.headers.get("content-type") || undefined;
|
|
952
1171
|
const data = Buffer.from(await response.arrayBuffer());
|
|
953
1172
|
return { data, mimeType };
|
|
954
1173
|
}
|
|
955
1174
|
|
|
956
|
-
getContextLimit(
|
|
1175
|
+
getContextLimit(
|
|
1176
|
+
model: string
|
|
1177
|
+
): { contextLimit: number; threshold: number } | undefined {
|
|
957
1178
|
const contextLimit = ContextLimits[model];
|
|
958
1179
|
if (contextLimit === undefined) return undefined;
|
|
959
1180
|
const pricing = OpenAiTextPricing[model];
|