@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.
Files changed (29) hide show
  1. package/dist/{anthropic-Dm_GqFgS.d.mts → anthropic-0CVG5rBE.d.mts} +1 -2
  2. package/dist/{provider-DiJKWMsQ.d.mts → errors-wlT14tC4.d.mts} +227 -2
  3. package/dist/{google-genai-hX0X6CF3.d.mts → google-genai-xKK8lI_R.d.mts} +1 -2
  4. package/dist/index.d.mts +66 -12
  5. package/dist/index.mjs +945 -21
  6. package/dist/{openai-common-08qin3UI.mjs → openai-common-Dl42y_vn.mjs} +27 -2
  7. package/dist/{openai-common-B6cK2ig3.d.mts → openai-common-DwkxUSyI.d.mts} +7 -3
  8. package/dist/{openai-responses-BxOwxtd3.d.mts → openai-responses-DZ9mQ5RA.d.mts} +2 -2
  9. package/dist/providers/anthropic.d.mts +1 -1
  10. package/dist/providers/anthropic.mjs +56 -68
  11. package/dist/providers/google-genai.d.mts +1 -1
  12. package/dist/providers/google-genai.mjs +1 -2
  13. package/dist/providers/openai-common.d.mts +1 -1
  14. package/dist/providers/openai-common.mjs +1 -1
  15. package/dist/providers/openai-responses.d.mts +1 -1
  16. package/dist/providers/openai-responses.mjs +21 -4
  17. package/dist/request-auth-BMXt8jRu.mjs +341 -0
  18. package/package.json +1 -11
  19. package/dist/capability-registry-CMBuEYcf.mjs +0 -161
  20. package/dist/chat-completions-stream-BuMu_xr9.mjs +0 -62
  21. package/dist/errors-DweKbIOf.d.mts +0 -42
  22. package/dist/openai-compat-CMrIk-ib.d.mts +0 -132
  23. package/dist/openai-compat-CWbwO4b7.mjs +0 -801
  24. package/dist/openai-legacy-B6CVfLlr.d.mts +0 -71
  25. package/dist/providers/openai-compat.d.mts +0 -2
  26. package/dist/providers/openai-compat.mjs +0 -2
  27. package/dist/providers/openai-legacy.d.mts +0 -2
  28. package/dist/providers/openai-legacy.mjs +0 -248
  29. 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-08qin3UI.mjs";
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 { a as isUnknownCapability, i as UNKNOWN_CAPABILITY } from "./request-auth-DCWSyCKI.mjs";
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 OpenAILegacyChatProvider(config);
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-compat`,
284
- * `@byfriends/kosong/providers/openai-legacy`, etc.
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 };