@byfriends/kosong 0.1.0 → 0.2.1
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/dist/{anthropic-Dm_GqFgS.d.mts → anthropic-0CVG5rBE.d.mts} +1 -2
- package/dist/{provider-DiJKWMsQ.d.mts → errors-wlT14tC4.d.mts} +227 -2
- package/dist/{google-genai-hX0X6CF3.d.mts → google-genai-xKK8lI_R.d.mts} +1 -2
- package/dist/index.d.mts +66 -12
- package/dist/index.mjs +945 -21
- package/dist/{openai-common-08qin3UI.mjs → openai-common-Dl42y_vn.mjs} +27 -2
- package/dist/{openai-common-B6cK2ig3.d.mts → openai-common-DwkxUSyI.d.mts} +7 -3
- package/dist/{openai-responses-BxOwxtd3.d.mts → openai-responses-DZ9mQ5RA.d.mts} +2 -2
- package/dist/providers/anthropic.d.mts +1 -1
- package/dist/providers/anthropic.mjs +56 -68
- package/dist/providers/google-genai.d.mts +1 -1
- package/dist/providers/google-genai.mjs +1 -2
- package/dist/providers/openai-common.d.mts +1 -1
- package/dist/providers/openai-common.mjs +1 -1
- package/dist/providers/openai-responses.d.mts +1 -1
- package/dist/providers/openai-responses.mjs +21 -4
- package/dist/request-auth-BMXt8jRu.mjs +341 -0
- package/package.json +1 -11
- package/dist/capability-registry-CMBuEYcf.mjs +0 -161
- package/dist/chat-completions-stream-BuMu_xr9.mjs +0 -62
- package/dist/errors-DweKbIOf.d.mts +0 -42
- package/dist/openai-compat-CMrIk-ib.d.mts +0 -132
- package/dist/openai-compat-CWbwO4b7.mjs +0 -801
- package/dist/openai-legacy-B6CVfLlr.d.mts +0 -71
- package/dist/providers/openai-compat.d.mts +0 -2
- package/dist/providers/openai-compat.mjs +0 -2
- package/dist/providers/openai-legacy.d.mts +0 -2
- package/dist/providers/openai-legacy.mjs +0 -248
- package/dist/request-auth-DCWSyCKI.mjs +0 -63
package/dist/index.mjs
CHANGED
|
@@ -1,17 +1,913 @@
|
|
|
1
|
-
import { _ as mergeInPlace, d as createToolMessage, f as createUserMessage, g as isToolCallPart, h as isToolCall, m as isContentPart, p as extractText, u as createAssistantMessage } from "./openai-common-
|
|
1
|
+
import { _ as mergeInPlace, a as isFunctionToolCall, d as createToolMessage, f as createUserMessage, g as isToolCallPart, h as isToolCall, i as extractUsage, l as toolToOpenAI, m as isContentPart, n as convertOpenAIError, o as normalizeOpenAIFinishReason, p as extractText, r as convertToolMessageContent, s as reasoningEffortToThinkingEffort, t as convertContentPart, u as createAssistantMessage } from "./openai-common-Dl42y_vn.mjs";
|
|
2
2
|
import { a as APITimeoutError, i as APIStatusError, n as APIContextOverflowError, o as ChatProviderError, r as APIEmptyResponseError, t as APIConnectionError } from "./errors-WFxxzL1B.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { c as resolveCapabilityFromRegistry, d as isUnknownCapability, n as requireProviderApiKey, o as getOpenAILegacyModelCapability, r as resolveAuthBackedClient, t as mergeRequestHeaders, u as UNKNOWN_CAPABILITY } from "./request-auth-BMXt8jRu.mjs";
|
|
4
4
|
import { AnthropicChatProvider } from "./providers/anthropic.mjs";
|
|
5
5
|
import { GoogleGenAIChatProvider } from "./providers/google-genai.mjs";
|
|
6
|
-
import { t as OpenAICompatChatProvider } from "./openai-compat-CWbwO4b7.mjs";
|
|
7
|
-
import { OpenAILegacyChatProvider } from "./providers/openai-legacy.mjs";
|
|
8
6
|
import { OpenAIResponsesChatProvider } from "./providers/openai-responses.mjs";
|
|
7
|
+
import OpenAI from "openai";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
//#region src/providers/openai-compat-schema.ts
|
|
12
|
+
/**
|
|
13
|
+
* Dereference all `$ref` references in a JSON Schema by inlining definitions
|
|
14
|
+
* from local JSON pointers such as `$defs` and draft-7 `definitions`. Resolved
|
|
15
|
+
* top-level definition buckets are removed from the result.
|
|
16
|
+
*
|
|
17
|
+
* Circular references are detected and left as `$ref` to avoid infinite
|
|
18
|
+
* recursion; in that case the referenced definition bucket is preserved so the
|
|
19
|
+
* remaining local `$ref` pointers stay resolvable to a JSON Schema validator.
|
|
20
|
+
*/
|
|
21
|
+
function derefJsonSchema(schema) {
|
|
22
|
+
const result = resolveNode(schema, schema, /* @__PURE__ */ new Set());
|
|
23
|
+
if (!hasUnresolvedDefinitionRef(result, "$defs")) delete result["$defs"];
|
|
24
|
+
if (!hasUnresolvedDefinitionRef(result, "definitions")) delete result["definitions"];
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
const TYPE_COMPLETION_SKIP_KEYS = new Set([
|
|
28
|
+
"$ref",
|
|
29
|
+
"allOf",
|
|
30
|
+
"anyOf",
|
|
31
|
+
"else",
|
|
32
|
+
"if",
|
|
33
|
+
"not",
|
|
34
|
+
"oneOf",
|
|
35
|
+
"then"
|
|
36
|
+
]);
|
|
37
|
+
const CHILD_SCHEMA_SLOTS = [
|
|
38
|
+
{
|
|
39
|
+
key: "$defs",
|
|
40
|
+
kind: "map"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: "definitions",
|
|
44
|
+
kind: "map"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: "dependencies",
|
|
48
|
+
kind: "map",
|
|
49
|
+
parentType: "object"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: "dependentSchemas",
|
|
53
|
+
kind: "map",
|
|
54
|
+
parentType: "object"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
key: "patternProperties",
|
|
58
|
+
kind: "map",
|
|
59
|
+
parentType: "object"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: "properties",
|
|
63
|
+
kind: "map",
|
|
64
|
+
parentType: "object"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
key: "additionalItems",
|
|
68
|
+
kind: "single",
|
|
69
|
+
parentType: "array"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: "additionalProperties",
|
|
73
|
+
kind: "single",
|
|
74
|
+
parentType: "object"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: "contains",
|
|
78
|
+
kind: "single",
|
|
79
|
+
parentType: "array"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
key: "contentSchema",
|
|
83
|
+
kind: "single",
|
|
84
|
+
parentType: "string"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
key: "else",
|
|
88
|
+
kind: "single"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
key: "if",
|
|
92
|
+
kind: "single"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
key: "not",
|
|
96
|
+
kind: "single"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
key: "propertyNames",
|
|
100
|
+
kind: "single",
|
|
101
|
+
parentType: "object"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
key: "then",
|
|
105
|
+
kind: "single"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
key: "unevaluatedItems",
|
|
109
|
+
kind: "single",
|
|
110
|
+
parentType: "array"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
key: "unevaluatedProperties",
|
|
114
|
+
kind: "single",
|
|
115
|
+
parentType: "object"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
key: "allOf",
|
|
119
|
+
kind: "array"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
key: "anyOf",
|
|
123
|
+
kind: "array"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
key: "oneOf",
|
|
127
|
+
kind: "array"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
key: "prefixItems",
|
|
131
|
+
kind: "array",
|
|
132
|
+
parentType: "array"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
key: "items",
|
|
136
|
+
kind: "schema-or-array",
|
|
137
|
+
parentType: "array"
|
|
138
|
+
}
|
|
139
|
+
];
|
|
140
|
+
const OBJECT_STRUCTURE_KEYS = new Set([
|
|
141
|
+
...childSchemaKeysForParentType("object"),
|
|
142
|
+
"dependentRequired",
|
|
143
|
+
"maxProperties",
|
|
144
|
+
"minProperties",
|
|
145
|
+
"required"
|
|
146
|
+
]);
|
|
147
|
+
const ARRAY_STRUCTURE_KEYS = new Set([
|
|
148
|
+
...childSchemaKeysForParentType("array"),
|
|
149
|
+
"maxContains",
|
|
150
|
+
"maxItems",
|
|
151
|
+
"minContains",
|
|
152
|
+
"minItems",
|
|
153
|
+
"uniqueItems"
|
|
154
|
+
]);
|
|
155
|
+
const STRING_STRUCTURE_KEYS = new Set([
|
|
156
|
+
...childSchemaKeysForParentType("string"),
|
|
157
|
+
"contentEncoding",
|
|
158
|
+
"contentMediaType",
|
|
159
|
+
"format",
|
|
160
|
+
"maxLength",
|
|
161
|
+
"minLength",
|
|
162
|
+
"pattern"
|
|
163
|
+
]);
|
|
164
|
+
const NUMERIC_STRUCTURE_KEYS = new Set([
|
|
165
|
+
"exclusiveMaximum",
|
|
166
|
+
"exclusiveMinimum",
|
|
167
|
+
"maximum",
|
|
168
|
+
"minimum",
|
|
169
|
+
"multipleOf"
|
|
170
|
+
]);
|
|
171
|
+
/**
|
|
172
|
+
* Return a deep-cloned JSON Schema with missing `type` fields filled in for
|
|
173
|
+
* OpenAI-compatible tool compatibility.
|
|
174
|
+
*
|
|
175
|
+
* The tool validator rejects some valid JSON Schema shapes when nested
|
|
176
|
+
* property schemas omit `type` (for example enum-only MCP properties). This is
|
|
177
|
+
* a provider-compatibility normalizer, not a complete JSON Schema compiler:
|
|
178
|
+
* it resolves local refs, preserves combinator nodes, infers obvious
|
|
179
|
+
* scalar/object/array types, and falls back to `string` only for nested
|
|
180
|
+
* typeless property schemas. The root schema object is treated as a container
|
|
181
|
+
* and is not itself normalized.
|
|
182
|
+
*/
|
|
183
|
+
function normalizeOpenAICompatToolSchema(schema) {
|
|
184
|
+
return ensureOpenAICompatPropertyTypes(derefJsonSchema(schema));
|
|
185
|
+
}
|
|
186
|
+
function ensureOpenAICompatPropertyTypes(schema) {
|
|
187
|
+
const normalized = cloneJsonValue(schema);
|
|
188
|
+
if (!isRecord(normalized)) throw new Error("JSON Schema root must normalize to an object.");
|
|
189
|
+
recurseSchema(normalized);
|
|
190
|
+
return normalized;
|
|
191
|
+
}
|
|
192
|
+
function hasUnresolvedDefinitionRef(node, bucketKey) {
|
|
193
|
+
if (Array.isArray(node)) return node.some((child) => hasUnresolvedDefinitionRef(child, bucketKey));
|
|
194
|
+
if (typeof node === "object" && node !== null) {
|
|
195
|
+
const obj = node;
|
|
196
|
+
const ref = obj["$ref"];
|
|
197
|
+
if (typeof ref === "string" && ref.startsWith(`#/${bucketKey}/`)) return true;
|
|
198
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
199
|
+
if (key === bucketKey) continue;
|
|
200
|
+
if (hasUnresolvedDefinitionRef(value, bucketKey)) return true;
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
function resolveNode(node, root, visited) {
|
|
207
|
+
if (Array.isArray(node)) return node.map((item) => resolveNode(item, root, visited));
|
|
208
|
+
if (typeof node === "object" && node !== null) {
|
|
209
|
+
const obj = node;
|
|
210
|
+
if (typeof obj["$ref"] === "string") {
|
|
211
|
+
const ref = obj["$ref"];
|
|
212
|
+
if (isLocalJsonPointerRef(ref)) {
|
|
213
|
+
if (visited.has(ref)) return obj;
|
|
214
|
+
const resolvedRef = resolveLocalJsonPointer(root, ref);
|
|
215
|
+
if (resolvedRef.found) {
|
|
216
|
+
visited.add(ref);
|
|
217
|
+
const resolved = resolveNode(resolvedRef.value, root, visited);
|
|
218
|
+
visited.delete(ref);
|
|
219
|
+
if (typeof resolved === "object" && resolved !== null && !Array.isArray(resolved)) {
|
|
220
|
+
const merged = { ...resolved };
|
|
221
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
222
|
+
if (key === "$ref") continue;
|
|
223
|
+
merged[key] = resolveNode(value, root, visited);
|
|
224
|
+
}
|
|
225
|
+
return merged;
|
|
226
|
+
}
|
|
227
|
+
return resolved;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return obj;
|
|
231
|
+
}
|
|
232
|
+
const resolved = {};
|
|
233
|
+
for (const [key, value] of Object.entries(obj)) resolved[key] = resolveNode(value, root, visited);
|
|
234
|
+
return resolved;
|
|
235
|
+
}
|
|
236
|
+
return node;
|
|
237
|
+
}
|
|
238
|
+
function isLocalJsonPointerRef(ref) {
|
|
239
|
+
return ref === "#" || ref.startsWith("#/");
|
|
240
|
+
}
|
|
241
|
+
function resolveLocalJsonPointer(root, ref) {
|
|
242
|
+
if (ref === "#") return {
|
|
243
|
+
found: true,
|
|
244
|
+
value: root
|
|
245
|
+
};
|
|
246
|
+
let current = root;
|
|
247
|
+
for (const rawPart of ref.slice(2).split("/")) {
|
|
248
|
+
const part = unescapeJsonPointerPart(rawPart);
|
|
249
|
+
if (isRecord(current)) {
|
|
250
|
+
if (!hasOwn(current, part)) return { found: false };
|
|
251
|
+
current = current[part];
|
|
252
|
+
} else if (Array.isArray(current)) {
|
|
253
|
+
const index = parseJsonPointerArrayIndex(part);
|
|
254
|
+
if (index === null || index >= current.length) return { found: false };
|
|
255
|
+
current = current[index];
|
|
256
|
+
} else return { found: false };
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
found: true,
|
|
260
|
+
value: current
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function unescapeJsonPointerPart(part) {
|
|
264
|
+
return part.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
265
|
+
}
|
|
266
|
+
function parseJsonPointerArrayIndex(part) {
|
|
267
|
+
if (!/^(0|[1-9]\d*)$/.test(part)) return null;
|
|
268
|
+
return Number(part);
|
|
269
|
+
}
|
|
270
|
+
function recurseSchema(node) {
|
|
271
|
+
if (!isRecord(node)) return;
|
|
272
|
+
visitChildSchemas(node, normalizeProperty);
|
|
273
|
+
}
|
|
274
|
+
function visitChildSchemas(node, visit) {
|
|
275
|
+
for (const { key, kind } of CHILD_SCHEMA_SLOTS) {
|
|
276
|
+
const value = node[key];
|
|
277
|
+
if (kind === "single") {
|
|
278
|
+
if (isRecord(value)) visit(value);
|
|
279
|
+
} else if (kind === "array") {
|
|
280
|
+
if (Array.isArray(value)) for (const item of value) visit(item);
|
|
281
|
+
} else if (kind === "map") {
|
|
282
|
+
if (isRecord(value)) for (const item of Object.values(value)) visit(item);
|
|
283
|
+
} else if (kind === "schema-or-array") {
|
|
284
|
+
if (isRecord(value)) visit(value);
|
|
285
|
+
else if (Array.isArray(value)) for (const item of value) visit(item);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function childSchemaKeysForParentType(parentType) {
|
|
290
|
+
return CHILD_SCHEMA_SLOTS.flatMap((slot) => {
|
|
291
|
+
if (!("parentType" in slot) || slot.parentType !== parentType) return [];
|
|
292
|
+
return [slot.key];
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
function normalizeProperty(node) {
|
|
296
|
+
if (!isRecord(node)) return;
|
|
297
|
+
if (!hasOwn(node, "type") && !hasAnyKey(node, TYPE_COMPLETION_SKIP_KEYS)) {
|
|
298
|
+
const enumValues = node["enum"];
|
|
299
|
+
if (Array.isArray(enumValues) && enumValues.length > 0) node["type"] = inferTypeFromValues(enumValues);
|
|
300
|
+
else if (hasOwn(node, "const")) node["type"] = inferTypeFromValues([node["const"]]);
|
|
301
|
+
else node["type"] = inferTypeFromStructure(node);
|
|
302
|
+
}
|
|
303
|
+
recurseSchema(node);
|
|
304
|
+
}
|
|
305
|
+
function inferTypeFromStructure(schema) {
|
|
306
|
+
if (hasAnyKey(schema, OBJECT_STRUCTURE_KEYS)) return "object";
|
|
307
|
+
if (hasAnyKey(schema, ARRAY_STRUCTURE_KEYS)) return "array";
|
|
308
|
+
if (hasAnyKey(schema, STRING_STRUCTURE_KEYS)) return "string";
|
|
309
|
+
if (hasAnyKey(schema, NUMERIC_STRUCTURE_KEYS)) return "number";
|
|
310
|
+
return "string";
|
|
311
|
+
}
|
|
312
|
+
function inferTypeFromValues(values) {
|
|
313
|
+
const inferred = /* @__PURE__ */ new Set();
|
|
314
|
+
for (const value of values) {
|
|
315
|
+
const valueType = inferValueType(value);
|
|
316
|
+
if (valueType === void 0) throw new Error("Cannot infer JSON Schema type from non-JSON enum or const value.");
|
|
317
|
+
inferred.add(valueType);
|
|
318
|
+
}
|
|
319
|
+
const types = normalizeInferredTypes(inferred);
|
|
320
|
+
if (types.length === 1) {
|
|
321
|
+
const onlyType = types[0];
|
|
322
|
+
if (onlyType === void 0) throw new Error("Cannot infer JSON Schema type from an empty enum.");
|
|
323
|
+
return onlyType;
|
|
324
|
+
}
|
|
325
|
+
throw new Error("Mixed JSON Schema enum or const types are not supported by OpenAI-compatible tool schemas.");
|
|
326
|
+
}
|
|
327
|
+
function inferValueType(value) {
|
|
328
|
+
if (value === null) return "null";
|
|
329
|
+
if (Array.isArray(value)) return "array";
|
|
330
|
+
switch (typeof value) {
|
|
331
|
+
case "string": return "string";
|
|
332
|
+
case "number": return Number.isInteger(value) ? "integer" : "number";
|
|
333
|
+
case "boolean": return "boolean";
|
|
334
|
+
case "object": return "object";
|
|
335
|
+
case "bigint":
|
|
336
|
+
case "function":
|
|
337
|
+
case "symbol":
|
|
338
|
+
case "undefined": return;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function normalizeInferredTypes(types) {
|
|
342
|
+
const normalized = new Set(types);
|
|
343
|
+
if (normalized.has("number")) normalized.delete("integer");
|
|
344
|
+
return [
|
|
345
|
+
"string",
|
|
346
|
+
"number",
|
|
347
|
+
"integer",
|
|
348
|
+
"boolean",
|
|
349
|
+
"object",
|
|
350
|
+
"array",
|
|
351
|
+
"null"
|
|
352
|
+
].filter((type) => normalized.has(type));
|
|
353
|
+
}
|
|
354
|
+
function hasAnyKey(obj, keys) {
|
|
355
|
+
for (const key of keys) if (hasOwn(obj, key)) return true;
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
function cloneJsonValue(value) {
|
|
359
|
+
if (Array.isArray(value)) return value.map((item) => cloneJsonValue(item));
|
|
360
|
+
if (isRecord(value)) {
|
|
361
|
+
const cloned = {};
|
|
362
|
+
for (const [key, child] of Object.entries(value)) cloned[key] = cloneJsonValue(child);
|
|
363
|
+
return cloned;
|
|
364
|
+
}
|
|
365
|
+
return value;
|
|
366
|
+
}
|
|
367
|
+
function isRecord(value) {
|
|
368
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
369
|
+
}
|
|
370
|
+
function hasOwn(obj, key) {
|
|
371
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
372
|
+
}
|
|
373
|
+
//#endregion
|
|
374
|
+
//#region src/providers/openai-compat-files.ts
|
|
375
|
+
/**
|
|
376
|
+
* OpenAI-compatible file upload client.
|
|
377
|
+
*
|
|
378
|
+
* Wraps the underlying OpenAI-compatible `files.create` API to upload videos
|
|
379
|
+
* to the file service and return them as {@link VideoURLPart} values
|
|
380
|
+
* suitable for use in chat messages.
|
|
381
|
+
*
|
|
382
|
+
* An `OpenAICompatFiles` instance is typically obtained from
|
|
383
|
+
* {@link OpenAICompatChatProvider.files}.
|
|
384
|
+
*/
|
|
385
|
+
var OpenAICompatFiles = class {
|
|
386
|
+
_apiKey;
|
|
387
|
+
_baseUrl;
|
|
388
|
+
_defaultHeaders;
|
|
389
|
+
_client;
|
|
390
|
+
_clientFactory;
|
|
391
|
+
constructor(options) {
|
|
392
|
+
this._apiKey = options.apiKey;
|
|
393
|
+
this._baseUrl = options.baseUrl;
|
|
394
|
+
this._defaultHeaders = options.defaultHeaders;
|
|
395
|
+
this._clientFactory = options.clientFactory;
|
|
396
|
+
this._client = options.apiKey === void 0 || options.apiKey.length === 0 ? void 0 : new OpenAI({
|
|
397
|
+
apiKey: options.apiKey,
|
|
398
|
+
baseURL: options.baseUrl,
|
|
399
|
+
defaultHeaders: options.defaultHeaders
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Upload a video file for use in chat messages.
|
|
404
|
+
*
|
|
405
|
+
* Accepts either a local filesystem path or an in-memory
|
|
406
|
+
* {@link VideoUploadInput}. Returns a {@link VideoURLPart} referencing the
|
|
407
|
+
* uploaded file by its file id.
|
|
408
|
+
*
|
|
409
|
+
* @param input - Local path string or `{ data, mimeType }` object.
|
|
410
|
+
* @returns A `VideoURLPart` whose `url` references the uploaded file
|
|
411
|
+
* by its file id (e.g. `ms://<file-id>`).
|
|
412
|
+
* @throws {ChatProviderError} if the input is not a video or the upload
|
|
413
|
+
* fails.
|
|
414
|
+
*/
|
|
415
|
+
async uploadVideo(input, options) {
|
|
416
|
+
let file;
|
|
417
|
+
if (typeof input === "string") {
|
|
418
|
+
if (!fs.existsSync(input)) throw new ChatProviderError(`Video file not found: ${input}`);
|
|
419
|
+
const filename = path.basename(input);
|
|
420
|
+
const mimeType = guessMimeTypeFromExt(filename);
|
|
421
|
+
if (mimeType === void 0 || !mimeType.startsWith("video/")) throw new ChatProviderError(`OpenAICompatFiles.uploadVideo: file extension does not indicate a video type: ${filename}`);
|
|
422
|
+
const data = await fs.promises.readFile(input);
|
|
423
|
+
const blob = new Blob([new Uint8Array(data)], { type: mimeType });
|
|
424
|
+
file = new File([blob], filename, { type: mimeType });
|
|
425
|
+
} else {
|
|
426
|
+
if (!input.mimeType.startsWith("video/")) throw new ChatProviderError(`Expected a video mime type, got ${input.mimeType}`);
|
|
427
|
+
const filename = input.filename ?? guessFilename(input.mimeType);
|
|
428
|
+
const bytes = input.data instanceof Uint8Array ? input.data : new Uint8Array(input.data);
|
|
429
|
+
const blob = new Blob([bytes], { type: input.mimeType });
|
|
430
|
+
file = new File([blob], filename, { type: input.mimeType });
|
|
431
|
+
}
|
|
432
|
+
let uploaded;
|
|
433
|
+
try {
|
|
434
|
+
uploaded = await this._createClient(options?.auth).files.create({
|
|
435
|
+
file,
|
|
436
|
+
purpose: "video"
|
|
437
|
+
}, options?.signal ? { signal: options.signal } : void 0);
|
|
438
|
+
} catch (error) {
|
|
439
|
+
throw convertOpenAIError(error);
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
type: "video_url",
|
|
443
|
+
videoUrl: {
|
|
444
|
+
url: `ms://${uploaded.id}`,
|
|
445
|
+
id: uploaded.id
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
_createClient(auth) {
|
|
450
|
+
return resolveAuthBackedClient({
|
|
451
|
+
cachedClient: this._client,
|
|
452
|
+
clientFactory: this._clientFactory
|
|
453
|
+
}, auth, (a) => {
|
|
454
|
+
const defaultHeaders = mergeRequestHeaders(this._defaultHeaders, a?.headers);
|
|
455
|
+
return new OpenAI({
|
|
456
|
+
apiKey: requireProviderApiKey("OpenAICompatFiles.uploadVideo", a, this._apiKey),
|
|
457
|
+
baseURL: this._baseUrl,
|
|
458
|
+
defaultHeaders
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
/**
|
|
464
|
+
* Guess a filename for an upload from a video MIME type.
|
|
465
|
+
* Falls back to `upload.bin` for unknown types.
|
|
466
|
+
*/
|
|
467
|
+
function guessFilename(mimeType) {
|
|
468
|
+
return `upload.${MIME_TO_EXT[mimeType.toLowerCase()] ?? "bin"}`;
|
|
469
|
+
}
|
|
470
|
+
const MIME_TO_EXT = {
|
|
471
|
+
"video/mp4": "mp4",
|
|
472
|
+
"video/mpeg": "mpeg",
|
|
473
|
+
"video/quicktime": "mov",
|
|
474
|
+
"video/webm": "webm",
|
|
475
|
+
"video/x-matroska": "mkv",
|
|
476
|
+
"video/x-msvideo": "avi",
|
|
477
|
+
"video/x-flv": "flv",
|
|
478
|
+
"video/3gpp": "3gp"
|
|
479
|
+
};
|
|
480
|
+
const EXT_TO_MIME = Object.fromEntries(Object.entries(MIME_TO_EXT).map(([mime, ext]) => [ext, mime]));
|
|
481
|
+
/**
|
|
482
|
+
* Guess a MIME type from a filename extension. Only recognises the video
|
|
483
|
+
* types listed in {@link MIME_TO_EXT}; returns `undefined` otherwise.
|
|
484
|
+
*/
|
|
485
|
+
function guessMimeTypeFromExt(filename) {
|
|
486
|
+
const dot = filename.lastIndexOf(".");
|
|
487
|
+
if (dot < 0) return void 0;
|
|
488
|
+
return EXT_TO_MIME[filename.slice(dot + 1).toLowerCase()];
|
|
489
|
+
}
|
|
490
|
+
//#endregion
|
|
491
|
+
//#region src/providers/chat-completions-stream.ts
|
|
492
|
+
/**
|
|
493
|
+
* Convert an OpenAI Chat Completions-style streamed tool-call delta into the
|
|
494
|
+
* normalized kosong stream part protocol.
|
|
495
|
+
*
|
|
496
|
+
* OpenAI-compatible providers may emit argument chunks before the function name
|
|
497
|
+
* for a stream index. Buffer those early argument chunks until the first named
|
|
498
|
+
* header arrives, then emit subsequent chunks as indexed `tool_call_part`s so
|
|
499
|
+
* the shared generate loop can route interleaved parallel calls.
|
|
500
|
+
*/
|
|
501
|
+
function convertChatCompletionStreamToolCall(toolCall, bufferedByIndex) {
|
|
502
|
+
if (toolCall.function === void 0 || toolCall.function === null) return [];
|
|
503
|
+
const streamIndex = toolCall.index;
|
|
504
|
+
const functionName = toolCall.function.name;
|
|
505
|
+
const functionArguments = toolCall.function.arguments;
|
|
506
|
+
const hasConcreteName = typeof functionName === "string" && functionName.length > 0;
|
|
507
|
+
const hasArguments = typeof functionArguments === "string" && functionArguments.length > 0;
|
|
508
|
+
if (streamIndex === void 0) {
|
|
509
|
+
if (hasConcreteName) return [{
|
|
510
|
+
type: "function",
|
|
511
|
+
id: toolCall.id ?? crypto.randomUUID(),
|
|
512
|
+
name: functionName,
|
|
513
|
+
arguments: functionArguments ?? null
|
|
514
|
+
}];
|
|
515
|
+
if (hasArguments) return [{
|
|
516
|
+
type: "tool_call_part",
|
|
517
|
+
argumentsPart: functionArguments
|
|
518
|
+
}];
|
|
519
|
+
return [];
|
|
520
|
+
}
|
|
521
|
+
const buffered = bufferedByIndex.get(streamIndex) ?? {
|
|
522
|
+
arguments: "",
|
|
523
|
+
emitted: false
|
|
524
|
+
};
|
|
525
|
+
if (toolCall.id !== void 0) buffered.id = toolCall.id;
|
|
526
|
+
if (!buffered.emitted) {
|
|
527
|
+
if (!hasConcreteName) {
|
|
528
|
+
if (hasArguments) buffered.arguments += functionArguments;
|
|
529
|
+
bufferedByIndex.set(streamIndex, buffered);
|
|
530
|
+
return [];
|
|
531
|
+
}
|
|
532
|
+
buffered.emitted = true;
|
|
533
|
+
const initialArguments = buffered.arguments.length > 0 ? buffered.arguments + (functionArguments ?? "") : functionArguments ?? null;
|
|
534
|
+
buffered.arguments = "";
|
|
535
|
+
bufferedByIndex.set(streamIndex, buffered);
|
|
536
|
+
return [{
|
|
537
|
+
type: "function",
|
|
538
|
+
id: buffered.id ?? toolCall.id ?? crypto.randomUUID(),
|
|
539
|
+
name: functionName,
|
|
540
|
+
arguments: initialArguments,
|
|
541
|
+
_streamIndex: streamIndex
|
|
542
|
+
}];
|
|
543
|
+
}
|
|
544
|
+
if (!hasArguments) return [];
|
|
545
|
+
return [{
|
|
546
|
+
type: "tool_call_part",
|
|
547
|
+
argumentsPart: functionArguments,
|
|
548
|
+
index: streamIndex
|
|
549
|
+
}];
|
|
550
|
+
}
|
|
551
|
+
//#endregion
|
|
552
|
+
//#region src/providers/openai-completions.ts
|
|
553
|
+
const KNOWN_REASONING_KEYS = [
|
|
554
|
+
"reasoning_content",
|
|
555
|
+
"reasoning_details",
|
|
556
|
+
"reasoning"
|
|
557
|
+
];
|
|
558
|
+
const DEFAULT_OUTBOUND_REASONING_KEY = KNOWN_REASONING_KEYS[0];
|
|
559
|
+
function extractReasoningContent(source, explicitKey) {
|
|
560
|
+
if (typeof source !== "object" || source === null) return void 0;
|
|
561
|
+
const record = source;
|
|
562
|
+
const keys = explicitKey !== void 0 ? [explicitKey] : KNOWN_REASONING_KEYS;
|
|
563
|
+
for (const key of keys) {
|
|
564
|
+
const value = record[key];
|
|
565
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
function isEffectivelyEmptyContent(parts) {
|
|
569
|
+
for (const part of parts) {
|
|
570
|
+
if (part.type !== "text") return false;
|
|
571
|
+
if (part.text.trim() !== "") return false;
|
|
572
|
+
}
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Derive a stable SHA256 hash from cacheable blocks in a PromptPlan.
|
|
577
|
+
*
|
|
578
|
+
* Only blocks with cacheScope 'global' are included in the hash, as OpenAI
|
|
579
|
+
* only supports caching the prefix (global scope).
|
|
580
|
+
*
|
|
581
|
+
* @param promptPlan - The prompt plan containing cacheable blocks.
|
|
582
|
+
* @returns A hexadecimal SHA256 hash string.
|
|
583
|
+
*/
|
|
584
|
+
function deriveCacheKeyFromPromptPlan(promptPlan) {
|
|
585
|
+
if (!promptPlan || promptPlan.blocks.length === 0) return createHash("sha256").digest("hex");
|
|
586
|
+
const cacheableTexts = [];
|
|
587
|
+
for (const block of promptPlan.blocks) if (block.cacheScope === "global") cacheableTexts.push(block.text);
|
|
588
|
+
const concatenated = cacheableTexts.join("");
|
|
589
|
+
return createHash("sha256").update(concatenated).digest("hex");
|
|
590
|
+
}
|
|
591
|
+
function convertMessage(message, reasoningKey, toolMessageConversion) {
|
|
592
|
+
let reasoningContent = "";
|
|
593
|
+
const nonThinkParts = [];
|
|
594
|
+
for (const part of message.content) if (part.type === "think") reasoningContent += part.think;
|
|
595
|
+
else nonThinkParts.push(part);
|
|
596
|
+
const result = { role: message.role };
|
|
597
|
+
const hasToolCalls = message.toolCalls.length > 0;
|
|
598
|
+
const shouldOmitContent = message.role === "assistant" && hasToolCalls && isEffectivelyEmptyContent(nonThinkParts);
|
|
599
|
+
if (message.role === "tool") {
|
|
600
|
+
const effectiveConversion = message.content.some((p) => p.type !== "text" && p.type !== "think") ? "extract_text" : toolMessageConversion;
|
|
601
|
+
if (effectiveConversion !== null) result.content = convertToolMessageContent(message, effectiveConversion);
|
|
602
|
+
else if (!shouldOmitContent) {
|
|
603
|
+
const firstPart = nonThinkParts[0];
|
|
604
|
+
if (nonThinkParts.length === 1 && firstPart?.type === "text") result.content = firstPart.text;
|
|
605
|
+
else if (nonThinkParts.length > 0) result.content = nonThinkParts.map((p) => convertContentPart(p)).filter((p) => p !== null);
|
|
606
|
+
}
|
|
607
|
+
} else if (!shouldOmitContent) {
|
|
608
|
+
const firstPart = nonThinkParts[0];
|
|
609
|
+
if (nonThinkParts.length === 1 && firstPart?.type === "text") result.content = firstPart.text;
|
|
610
|
+
else if (nonThinkParts.length > 0) result.content = nonThinkParts.map((p) => convertContentPart(p)).filter((p) => p !== null);
|
|
611
|
+
}
|
|
612
|
+
if (message.name !== void 0) result.name = message.name;
|
|
613
|
+
if (hasToolCalls) result.tool_calls = message.toolCalls.map((tc) => {
|
|
614
|
+
const mapped = {
|
|
615
|
+
type: tc.type,
|
|
616
|
+
id: tc.id,
|
|
617
|
+
function: {
|
|
618
|
+
name: tc.name,
|
|
619
|
+
arguments: tc.arguments
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
if (tc.extras !== void 0) mapped.extras = tc.extras;
|
|
623
|
+
return mapped;
|
|
624
|
+
});
|
|
625
|
+
if (message.toolCallId !== void 0) result.tool_call_id = message.toolCallId;
|
|
626
|
+
if (reasoningContent) result[reasoningKey ?? DEFAULT_OUTBOUND_REASONING_KEY] = reasoningContent;
|
|
627
|
+
return result;
|
|
628
|
+
}
|
|
629
|
+
function convertTool(tool) {
|
|
630
|
+
if (tool.name.startsWith("$")) return {
|
|
631
|
+
type: "builtin_function",
|
|
632
|
+
function: { name: tool.name }
|
|
633
|
+
};
|
|
634
|
+
const converted = toolToOpenAI(tool);
|
|
635
|
+
return {
|
|
636
|
+
...converted,
|
|
637
|
+
function: {
|
|
638
|
+
...converted.function,
|
|
639
|
+
parameters: normalizeOpenAICompatToolSchema(tool.parameters)
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
function extractUsageFromChunk(chunk) {
|
|
644
|
+
if (chunk["usage"] !== null && chunk["usage"] !== void 0 && typeof chunk["usage"] === "object") return chunk["usage"];
|
|
645
|
+
const choices = chunk["choices"];
|
|
646
|
+
if (!Array.isArray(choices) || choices.length === 0) return null;
|
|
647
|
+
const firstChoice = choices[0];
|
|
648
|
+
if (firstChoice === void 0) return null;
|
|
649
|
+
const choiceUsage = firstChoice["usage"];
|
|
650
|
+
if (choiceUsage !== null && choiceUsage !== void 0 && typeof choiceUsage === "object") return choiceUsage;
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
var OpenAICompletionsStreamedMessage = class {
|
|
654
|
+
_id = null;
|
|
655
|
+
_usage = null;
|
|
656
|
+
_finishReason = null;
|
|
657
|
+
_rawFinishReason = null;
|
|
658
|
+
_iter;
|
|
659
|
+
constructor(response, isStream, reasoningKey) {
|
|
660
|
+
if (isStream) this._iter = this._convertStreamResponse(response, reasoningKey);
|
|
661
|
+
else this._iter = this._convertNonStreamResponse(response, reasoningKey);
|
|
662
|
+
}
|
|
663
|
+
get id() {
|
|
664
|
+
return this._id;
|
|
665
|
+
}
|
|
666
|
+
get usage() {
|
|
667
|
+
return this._usage;
|
|
668
|
+
}
|
|
669
|
+
get finishReason() {
|
|
670
|
+
return this._finishReason;
|
|
671
|
+
}
|
|
672
|
+
get rawFinishReason() {
|
|
673
|
+
return this._rawFinishReason;
|
|
674
|
+
}
|
|
675
|
+
async *[Symbol.asyncIterator]() {
|
|
676
|
+
yield* this._iter;
|
|
677
|
+
}
|
|
678
|
+
_captureFinishReason(raw) {
|
|
679
|
+
const normalized = normalizeOpenAIFinishReason(raw);
|
|
680
|
+
this._finishReason = normalized.finishReason;
|
|
681
|
+
this._rawFinishReason = normalized.rawFinishReason;
|
|
682
|
+
}
|
|
683
|
+
async *_convertNonStreamResponse(response, reasoningKey) {
|
|
684
|
+
this._id = response.id;
|
|
685
|
+
if (response.usage) this._usage = extractUsage(response.usage) ?? null;
|
|
686
|
+
this._captureFinishReason(response.choices[0]?.finish_reason ?? null);
|
|
687
|
+
const message = response.choices[0]?.message;
|
|
688
|
+
if (!message) return;
|
|
689
|
+
const reasoning = extractReasoningContent(message, reasoningKey);
|
|
690
|
+
if (reasoning) yield {
|
|
691
|
+
type: "think",
|
|
692
|
+
think: reasoning
|
|
693
|
+
};
|
|
694
|
+
if (message.content) yield {
|
|
695
|
+
type: "text",
|
|
696
|
+
text: message.content
|
|
697
|
+
};
|
|
698
|
+
if (message.tool_calls) for (const toolCall of message.tool_calls) {
|
|
699
|
+
if (!isFunctionToolCall(toolCall)) continue;
|
|
700
|
+
yield {
|
|
701
|
+
type: "function",
|
|
702
|
+
id: toolCall.id || crypto.randomUUID(),
|
|
703
|
+
name: toolCall.function.name,
|
|
704
|
+
arguments: toolCall.function.arguments
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
async *_convertStreamResponse(response, reasoningKey) {
|
|
709
|
+
const bufferedToolCalls = /* @__PURE__ */ new Map();
|
|
710
|
+
try {
|
|
711
|
+
for await (const chunk of response) {
|
|
712
|
+
if (chunk.id) this._id = chunk.id;
|
|
713
|
+
const rawUsage = extractUsageFromChunk(chunk);
|
|
714
|
+
if (rawUsage) this._usage = extractUsage(rawUsage) ?? null;
|
|
715
|
+
if (!chunk.choices || chunk.choices.length === 0) continue;
|
|
716
|
+
const choice = chunk.choices[0];
|
|
717
|
+
if (!choice) continue;
|
|
718
|
+
if (choice.finish_reason !== null && choice.finish_reason !== void 0) this._captureFinishReason(choice.finish_reason);
|
|
719
|
+
const delta = choice.delta;
|
|
720
|
+
const reasoning = extractReasoningContent(delta, reasoningKey);
|
|
721
|
+
if (reasoning) yield {
|
|
722
|
+
type: "think",
|
|
723
|
+
think: reasoning
|
|
724
|
+
};
|
|
725
|
+
if (delta.content) yield {
|
|
726
|
+
type: "text",
|
|
727
|
+
text: delta.content
|
|
728
|
+
};
|
|
729
|
+
for (const toolCall of delta.tool_calls ?? []) for (const part of convertChatCompletionStreamToolCall(toolCall, bufferedToolCalls)) yield part;
|
|
730
|
+
}
|
|
731
|
+
} catch (error) {
|
|
732
|
+
throw convertOpenAIError(error);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
var OpenAICompletionsChatProvider = class {
|
|
737
|
+
name = "openai-completions";
|
|
738
|
+
_model;
|
|
739
|
+
_stream;
|
|
740
|
+
_apiKey;
|
|
741
|
+
_baseUrl;
|
|
742
|
+
_defaultHeaders;
|
|
743
|
+
_generationKwargs;
|
|
744
|
+
_thinkingEffortKey;
|
|
745
|
+
_reasoningKey;
|
|
746
|
+
_toolMessageConversion;
|
|
747
|
+
_client;
|
|
748
|
+
_clientFactory;
|
|
749
|
+
_files;
|
|
750
|
+
constructor(options) {
|
|
751
|
+
const apiKey = options.apiKey;
|
|
752
|
+
this._apiKey = apiKey === void 0 || apiKey.length === 0 ? void 0 : apiKey;
|
|
753
|
+
this._baseUrl = options.baseUrl ?? "";
|
|
754
|
+
this._defaultHeaders = options.defaultHeaders;
|
|
755
|
+
this._clientFactory = options.clientFactory;
|
|
756
|
+
this._model = options.model;
|
|
757
|
+
this._stream = options.stream ?? true;
|
|
758
|
+
const normalizedThinkingEffortKey = options.thinkingEffortKey?.trim();
|
|
759
|
+
this._thinkingEffortKey = normalizedThinkingEffortKey !== void 0 && normalizedThinkingEffortKey.length > 0 ? normalizedThinkingEffortKey : "reasoning_effort";
|
|
760
|
+
const normalizedReasoningKey = options.reasoningKey?.trim();
|
|
761
|
+
this._reasoningKey = normalizedReasoningKey !== void 0 && normalizedReasoningKey.length > 0 ? normalizedReasoningKey : void 0;
|
|
762
|
+
this._generationKwargs = { ...options.generationKwargs };
|
|
763
|
+
this._toolMessageConversion = options.toolMessageConversion ?? null;
|
|
764
|
+
this._client = this._apiKey === void 0 ? void 0 : new OpenAI({
|
|
765
|
+
apiKey: this._apiKey,
|
|
766
|
+
baseURL: this._baseUrl,
|
|
767
|
+
defaultHeaders: this._defaultHeaders
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
get modelName() {
|
|
771
|
+
return this._model;
|
|
772
|
+
}
|
|
773
|
+
get thinkingEffort() {
|
|
774
|
+
const customValue = this._generationKwargs[this._thinkingEffortKey];
|
|
775
|
+
if (typeof customValue === "string") return reasoningEffortToThinkingEffort(customValue);
|
|
776
|
+
const defaultValue = this._generationKwargs.reasoning_effort;
|
|
777
|
+
return reasoningEffortToThinkingEffort(defaultValue);
|
|
778
|
+
}
|
|
779
|
+
get files() {
|
|
780
|
+
this._files ??= new OpenAICompatFiles({
|
|
781
|
+
apiKey: this._apiKey,
|
|
782
|
+
baseUrl: this._baseUrl,
|
|
783
|
+
defaultHeaders: this._defaultHeaders,
|
|
784
|
+
clientFactory: this._clientFactory
|
|
785
|
+
});
|
|
786
|
+
return this._files;
|
|
787
|
+
}
|
|
788
|
+
uploadVideo(input, options) {
|
|
789
|
+
return this.files.uploadVideo(input, options);
|
|
790
|
+
}
|
|
791
|
+
get modelParameters() {
|
|
792
|
+
return {
|
|
793
|
+
model: this._model,
|
|
794
|
+
baseUrl: this._baseUrl,
|
|
795
|
+
...this._generationKwargs
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
getCapability(model) {
|
|
799
|
+
return getOpenAILegacyModelCapability(model ?? this._model);
|
|
800
|
+
}
|
|
801
|
+
async generate(systemPrompt, tools, history, options) {
|
|
802
|
+
const messages = [];
|
|
803
|
+
if (systemPrompt) messages.push({
|
|
804
|
+
role: "system",
|
|
805
|
+
content: systemPrompt
|
|
806
|
+
});
|
|
807
|
+
for (const msg of history) messages.push(convertMessage(msg, this._reasoningKey, this._toolMessageConversion));
|
|
808
|
+
const kwargs = { ...this._generationKwargs };
|
|
809
|
+
if (kwargs[this._thinkingEffortKey] === void 0 && kwargs["reasoning_effort"] === void 0) {
|
|
810
|
+
if (history.some((message) => message.content.some((part) => part.type === "think"))) kwargs[this._thinkingEffortKey] = "high";
|
|
811
|
+
}
|
|
812
|
+
for (const key of Object.keys(kwargs)) if (kwargs[key] === void 0) delete kwargs[key];
|
|
813
|
+
if (kwargs["max_completion_tokens"] === void 0 && kwargs["max_tokens"] !== void 0) kwargs["max_completion_tokens"] = kwargs["max_tokens"];
|
|
814
|
+
delete kwargs["max_tokens"];
|
|
815
|
+
const { extra_body: extraBody, ...requestKwargs } = kwargs;
|
|
816
|
+
const createParams = {
|
|
817
|
+
model: this._model,
|
|
818
|
+
messages,
|
|
819
|
+
stream: this._stream,
|
|
820
|
+
...requestKwargs,
|
|
821
|
+
...extraBody
|
|
822
|
+
};
|
|
823
|
+
if (tools.length > 0) createParams["tools"] = tools.map((t) => convertTool(t));
|
|
824
|
+
if (this._stream) createParams["stream_options"] = { include_usage: true };
|
|
825
|
+
if (options?.promptPlan) {
|
|
826
|
+
const cacheKey = deriveCacheKeyFromPromptPlan(options.promptPlan);
|
|
827
|
+
if (cacheKey) createParams["prompt_cache_key"] = cacheKey;
|
|
828
|
+
}
|
|
829
|
+
try {
|
|
830
|
+
return new OpenAICompletionsStreamedMessage(await this._createClient(options?.auth).chat.completions.create(createParams, options?.signal ? { signal: options.signal } : void 0), this._stream, this._reasoningKey);
|
|
831
|
+
} catch (error) {
|
|
832
|
+
throw convertOpenAIError(error);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
withThinking(effort) {
|
|
836
|
+
const thinking = { type: effort === "off" ? "disabled" : "enabled" };
|
|
837
|
+
let reasoningEffort;
|
|
838
|
+
switch (effort) {
|
|
839
|
+
case "off":
|
|
840
|
+
reasoningEffort = void 0;
|
|
841
|
+
break;
|
|
842
|
+
case "low":
|
|
843
|
+
reasoningEffort = "low";
|
|
844
|
+
break;
|
|
845
|
+
case "medium":
|
|
846
|
+
reasoningEffort = "medium";
|
|
847
|
+
break;
|
|
848
|
+
case "high":
|
|
849
|
+
case "xhigh":
|
|
850
|
+
case "max":
|
|
851
|
+
reasoningEffort = "high";
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
const nextEffort = { [this._thinkingEffortKey]: reasoningEffort };
|
|
855
|
+
return this._withGenerationKwargs(nextEffort).withExtraBody({ thinking });
|
|
856
|
+
}
|
|
857
|
+
withGenerationKwargs(kwargs) {
|
|
858
|
+
return this._withGenerationKwargs(kwargs);
|
|
859
|
+
}
|
|
860
|
+
withMaxCompletionTokens(maxCompletionTokens) {
|
|
861
|
+
return this._withGenerationKwargs({ max_completion_tokens: maxCompletionTokens });
|
|
862
|
+
}
|
|
863
|
+
withExtraBody(extraBody) {
|
|
864
|
+
const oldExtra = this._generationKwargs.extra_body ?? {};
|
|
865
|
+
const merged = {
|
|
866
|
+
...oldExtra,
|
|
867
|
+
...extraBody
|
|
868
|
+
};
|
|
869
|
+
const oldThinking = oldExtra.thinking;
|
|
870
|
+
const newThinking = extraBody.thinking;
|
|
871
|
+
if (oldThinking !== void 0 && newThinking !== void 0) merged.thinking = {
|
|
872
|
+
...oldThinking,
|
|
873
|
+
...newThinking
|
|
874
|
+
};
|
|
875
|
+
return this._withGenerationKwargs({ extra_body: merged });
|
|
876
|
+
}
|
|
877
|
+
_createClient(auth) {
|
|
878
|
+
return resolveAuthBackedClient({
|
|
879
|
+
cachedClient: this._client,
|
|
880
|
+
clientFactory: this._clientFactory
|
|
881
|
+
}, auth, (a) => {
|
|
882
|
+
const defaultHeaders = mergeRequestHeaders(this._defaultHeaders, a?.headers);
|
|
883
|
+
return new OpenAI({
|
|
884
|
+
apiKey: requireProviderApiKey("OpenAICompletionsChatProvider", a, this._apiKey),
|
|
885
|
+
baseURL: this._baseUrl,
|
|
886
|
+
defaultHeaders
|
|
887
|
+
});
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
_withGenerationKwargs(kwargs) {
|
|
891
|
+
const clone = this._clone();
|
|
892
|
+
clone._generationKwargs = {
|
|
893
|
+
...clone._generationKwargs,
|
|
894
|
+
...kwargs
|
|
895
|
+
};
|
|
896
|
+
return clone;
|
|
897
|
+
}
|
|
898
|
+
_clone() {
|
|
899
|
+
const clone = Object.assign(Object.create(Object.getPrototypeOf(this)), this);
|
|
900
|
+
clone._generationKwargs = { ...this._generationKwargs };
|
|
901
|
+
clone._files = void 0;
|
|
902
|
+
return clone;
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
//#endregion
|
|
9
906
|
//#region src/providers/index.ts
|
|
10
907
|
function createProvider(config) {
|
|
11
908
|
switch (config.type) {
|
|
12
909
|
case "anthropic": return new AnthropicChatProvider(config);
|
|
13
|
-
case "openai": return new
|
|
14
|
-
case "openai-compat": return new OpenAICompatChatProvider(config);
|
|
910
|
+
case "openai-completions": return new OpenAICompletionsChatProvider(config);
|
|
15
911
|
case "google-genai": return new GoogleGenAIChatProvider(config);
|
|
16
912
|
case "openai_responses": return new OpenAIResponsesChatProvider(config);
|
|
17
913
|
case "vertexai": return new GoogleGenAIChatProvider(config);
|
|
@@ -22,8 +918,7 @@ function createProvider(config) {
|
|
|
22
918
|
//#region src/catalog.ts
|
|
23
919
|
const KNOWN_WIRE_TYPES = [
|
|
24
920
|
"anthropic",
|
|
25
|
-
"openai",
|
|
26
|
-
"openai-compat",
|
|
921
|
+
"openai-completions",
|
|
27
922
|
"google-genai",
|
|
28
923
|
"openai_responses",
|
|
29
924
|
"vertexai"
|
|
@@ -53,7 +948,7 @@ function inferWireType(entry) {
|
|
|
53
948
|
if (npm.includes("anthropic") || id.includes("anthropic") || id.includes("claude")) return "anthropic";
|
|
54
949
|
if (id.includes("vertex")) return "vertexai";
|
|
55
950
|
if (npm.includes("google") || id.includes("google") || id.includes("gemini")) return "google-genai";
|
|
56
|
-
if (npm.includes("openai") || id.includes("openai")) return "openai";
|
|
951
|
+
if (npm.includes("openai") || id.includes("openai")) return "openai-completions";
|
|
57
952
|
}
|
|
58
953
|
/**
|
|
59
954
|
* Resolves the base URL to store for a catalog provider, adapting the catalog's
|
|
@@ -80,19 +975,29 @@ function catalogModelToCapability(model) {
|
|
|
80
975
|
if (!isUsableChatModel(model)) return void 0;
|
|
81
976
|
const inputs = model.modalities?.input ?? [];
|
|
82
977
|
const output = model.limit?.output;
|
|
978
|
+
const base = {
|
|
979
|
+
image_in: inputs.includes("image"),
|
|
980
|
+
video_in: inputs.includes("video"),
|
|
981
|
+
audio_in: inputs.includes("audio"),
|
|
982
|
+
thinking: Boolean(model.reasoning),
|
|
983
|
+
tool_use: model.tool_call ?? true,
|
|
984
|
+
thinking_effort: false,
|
|
985
|
+
thinking_xhigh: false,
|
|
986
|
+
thinking_max: false,
|
|
987
|
+
max_context_tokens: context
|
|
988
|
+
};
|
|
989
|
+
const registry = resolveCapabilityFromRegistry(model.id);
|
|
990
|
+
const capability = registry !== void 0 ? {
|
|
991
|
+
...base,
|
|
992
|
+
...registry,
|
|
993
|
+
max_context_tokens: context
|
|
994
|
+
} : base;
|
|
83
995
|
return {
|
|
84
996
|
id: model.id,
|
|
85
997
|
name: typeof model.name === "string" && model.name.length > 0 ? model.name : void 0,
|
|
86
998
|
maxOutputSize: typeof output === "number" && output > 0 ? output : void 0,
|
|
87
999
|
reasoningKey: catalogReasoningKey(model.interleaved),
|
|
88
|
-
capability
|
|
89
|
-
image_in: inputs.includes("image"),
|
|
90
|
-
video_in: inputs.includes("video"),
|
|
91
|
-
audio_in: inputs.includes("audio"),
|
|
92
|
-
thinking: Boolean(model.reasoning),
|
|
93
|
-
tool_use: model.tool_call ?? true,
|
|
94
|
-
max_context_tokens: context
|
|
95
|
-
}
|
|
1000
|
+
capability
|
|
96
1001
|
};
|
|
97
1002
|
}
|
|
98
1003
|
function catalogReasoningKey(interleaved) {
|
|
@@ -141,9 +1046,12 @@ async function generate(provider, systemPrompt, tools, history, callbacks, optio
|
|
|
141
1046
|
let pendingPart = null;
|
|
142
1047
|
const toolCallIndexMap = /* @__PURE__ */ new Map();
|
|
143
1048
|
if (options?.signal?.aborted) throwAbortError();
|
|
1049
|
+
const generateStart = performance.now();
|
|
144
1050
|
const stream = await provider.generate(systemPrompt, tools, history, options);
|
|
145
1051
|
await throwIfAborted(options?.signal, stream);
|
|
1052
|
+
let firstChunkTime;
|
|
146
1053
|
for await (const part of stream) {
|
|
1054
|
+
firstChunkTime ??= performance.now();
|
|
147
1055
|
await throwIfAborted(options?.signal, stream);
|
|
148
1056
|
if (callbacks?.onMessagePart !== void 0) {
|
|
149
1057
|
await callbacks.onMessagePart(deepCopyPart(part));
|
|
@@ -174,12 +1082,17 @@ async function generate(provider, systemPrompt, tools, history, callbacks, optio
|
|
|
174
1082
|
await throwIfAborted(options?.signal, stream);
|
|
175
1083
|
await callbacks.onToolCall(toolCall);
|
|
176
1084
|
}
|
|
1085
|
+
const streamEnd = performance.now();
|
|
1086
|
+
const llmFirstTokenLatencyMs = firstChunkTime !== void 0 ? Math.round(firstChunkTime - generateStart) : void 0;
|
|
1087
|
+
const llmStreamDurationMs = firstChunkTime !== void 0 ? Math.round(streamEnd - generateStart) : void 0;
|
|
177
1088
|
return {
|
|
178
1089
|
id: stream.id,
|
|
179
1090
|
message,
|
|
180
1091
|
usage: stream.usage,
|
|
181
1092
|
finishReason: stream.finishReason,
|
|
182
|
-
rawFinishReason: stream.rawFinishReason
|
|
1093
|
+
rawFinishReason: stream.rawFinishReason,
|
|
1094
|
+
...llmFirstTokenLatencyMs !== void 0 ? { llmFirstTokenLatencyMs } : {},
|
|
1095
|
+
...llmStreamDurationMs !== void 0 ? { llmStreamDurationMs } : {}
|
|
183
1096
|
};
|
|
184
1097
|
}
|
|
185
1098
|
function throwAbortError() {
|
|
@@ -275,13 +1188,24 @@ function addUsage(a, b) {
|
|
|
275
1188
|
inputCacheCreation: a.inputCacheCreation + b.inputCacheCreation
|
|
276
1189
|
};
|
|
277
1190
|
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Compute the cache hit rate as a branded number between 0 and 1.
|
|
1193
|
+
*
|
|
1194
|
+
* Returns `undefined` when no input tokens were processed (inputTotal === 0),
|
|
1195
|
+
* so callers can distinguish "no data" from "zero hits".
|
|
1196
|
+
*/
|
|
1197
|
+
function cacheHitRate(usage) {
|
|
1198
|
+
const total = inputTotal(usage);
|
|
1199
|
+
if (total === 0) return void 0;
|
|
1200
|
+
return usage.inputCacheRead / total;
|
|
1201
|
+
}
|
|
278
1202
|
//#endregion
|
|
279
1203
|
//#region src/index.ts
|
|
280
1204
|
/**
|
|
281
1205
|
* Concrete provider adapters stay off the root barrel because their SDK type
|
|
282
1206
|
* graphs pollute downstream declaration bundles. Import them from subpaths:
|
|
283
|
-
* `@byfriends/kosong/providers/openai-
|
|
284
|
-
* `@byfriends/kosong/providers/
|
|
1207
|
+
* `@byfriends/kosong/providers/openai-responses`,
|
|
1208
|
+
* `@byfriends/kosong/providers/anthropic`, etc.
|
|
285
1209
|
*/
|
|
286
1210
|
//#endregion
|
|
287
|
-
export { APIConnectionError, APIContextOverflowError, APIEmptyResponseError, APIStatusError, APITimeoutError, ChatProviderError, UNKNOWN_CAPABILITY, addUsage, catalogBaseUrl, catalogModelToCapability, catalogProviderModels, createAssistantMessage, createProvider, createToolMessage, createUserMessage, emptyUsage, extractText, generate, grandTotal, inferWireType, inputTotal, isContentPart, isToolCall, isToolCallPart, isUnknownCapability, mergeInPlace };
|
|
1211
|
+
export { APIConnectionError, APIContextOverflowError, APIEmptyResponseError, APIStatusError, APITimeoutError, ChatProviderError, UNKNOWN_CAPABILITY, addUsage, cacheHitRate, catalogBaseUrl, catalogModelToCapability, catalogProviderModels, createAssistantMessage, createProvider, createToolMessage, createUserMessage, emptyUsage, extractText, generate, grandTotal, inferWireType, inputTotal, isContentPart, isToolCall, isToolCallPart, isUnknownCapability, mergeInPlace, resolveCapabilityFromRegistry };
|