@ljoukov/llm 6.0.0 → 7.0.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/README.md +19 -11
- package/dist/index.cjs +525 -237
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +528 -240
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -318,6 +318,14 @@ function getGeminiImagePricing(modelId) {
|
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
// src/openai/pricing.ts
|
|
321
|
+
var OPENAI_GPT_54_FAST_MODEL_IDS = ["gpt-5.4-fast", "chatgpt-gpt-5.4-fast"];
|
|
322
|
+
var OPENAI_GPT_54_MINI_MODEL_IDS = ["gpt-5.4-mini", "chatgpt-gpt-5.4-mini"];
|
|
323
|
+
var OPENAI_GPT_54_NANO_MODEL_IDS = ["gpt-5.4-nano"];
|
|
324
|
+
var OPENAI_GPT_53_CODEX_SPARK_MODEL_IDS = [
|
|
325
|
+
"gpt-5.3-codex-spark",
|
|
326
|
+
"chatgpt-gpt-5.3-codex-spark"
|
|
327
|
+
];
|
|
328
|
+
var OPENAI_GPT_54_STANDARD_MODEL_IDS = ["gpt-5.4", "chatgpt-gpt-5.4"];
|
|
321
329
|
var OPENAI_GPT_54_PRICING = {
|
|
322
330
|
inputRate: 2.5 / 1e6,
|
|
323
331
|
cachedRate: 0.25 / 1e6,
|
|
@@ -339,19 +347,19 @@ var OPENAI_GPT_54_NANO_PRICING = {
|
|
|
339
347
|
outputRate: 0.4 / 1e6
|
|
340
348
|
};
|
|
341
349
|
function getOpenAiPricing(modelId) {
|
|
342
|
-
if (
|
|
350
|
+
if (OPENAI_GPT_54_FAST_MODEL_IDS.includes(modelId)) {
|
|
343
351
|
return OPENAI_GPT_54_PRIORITY_PRICING;
|
|
344
352
|
}
|
|
345
|
-
if (
|
|
353
|
+
if (OPENAI_GPT_54_MINI_MODEL_IDS.includes(modelId)) {
|
|
346
354
|
return OPENAI_GPT_54_MINI_PRICING;
|
|
347
355
|
}
|
|
348
|
-
if (
|
|
356
|
+
if (OPENAI_GPT_54_NANO_MODEL_IDS.includes(modelId)) {
|
|
349
357
|
return OPENAI_GPT_54_NANO_PRICING;
|
|
350
358
|
}
|
|
351
|
-
if (
|
|
359
|
+
if (OPENAI_GPT_53_CODEX_SPARK_MODEL_IDS.includes(modelId)) {
|
|
352
360
|
return OPENAI_GPT_54_MINI_PRICING;
|
|
353
361
|
}
|
|
354
|
-
if (
|
|
362
|
+
if (OPENAI_GPT_54_STANDARD_MODEL_IDS.includes(modelId)) {
|
|
355
363
|
return OPENAI_GPT_54_PRICING;
|
|
356
364
|
}
|
|
357
365
|
return void 0;
|
|
@@ -2354,6 +2362,9 @@ function normaliseConfigValue(value) {
|
|
|
2354
2362
|
}
|
|
2355
2363
|
function resolveGeminiApiKey() {
|
|
2356
2364
|
loadLocalEnv();
|
|
2365
|
+
if (normaliseConfigValue(process.env.GOOGLE_SERVICE_ACCOUNT_JSON)) {
|
|
2366
|
+
return void 0;
|
|
2367
|
+
}
|
|
2357
2368
|
const raw = process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;
|
|
2358
2369
|
return normaliseConfigValue(raw);
|
|
2359
2370
|
}
|
|
@@ -3358,13 +3369,10 @@ var import_node_fs3 = require("fs");
|
|
|
3358
3369
|
var import_promises2 = require("fs/promises");
|
|
3359
3370
|
var import_node_os3 = __toESM(require("os"), 1);
|
|
3360
3371
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
3361
|
-
var import_node_stream = require("stream");
|
|
3362
3372
|
var import_promises3 = require("stream/promises");
|
|
3363
3373
|
var import_storage = require("@google-cloud/storage");
|
|
3364
3374
|
var import_mime = __toESM(require("mime"), 1);
|
|
3365
3375
|
var DEFAULT_FILE_TTL_SECONDS = 48 * 60 * 60;
|
|
3366
|
-
var OPENAI_FILE_CREATE_MAX_BYTES = 512 * 1024 * 1024;
|
|
3367
|
-
var OPENAI_UPLOAD_PART_MAX_BYTES = 64 * 1024 * 1024;
|
|
3368
3376
|
var GEMINI_FILE_POLL_INTERVAL_MS = 1e3;
|
|
3369
3377
|
var GEMINI_FILE_POLL_TIMEOUT_MS = 6e4;
|
|
3370
3378
|
var FILES_TEMP_ROOT = import_node_path4.default.join(import_node_os3.default.tmpdir(), "ljoukov-llm-files");
|
|
@@ -3373,7 +3381,7 @@ var FILES_CACHE_CONTENT_ROOT = import_node_path4.default.join(FILES_CACHE_ROOT,
|
|
|
3373
3381
|
var FILES_CACHE_METADATA_ROOT = import_node_path4.default.join(FILES_CACHE_ROOT, "metadata");
|
|
3374
3382
|
var filesState = getRuntimeSingleton(/* @__PURE__ */ Symbol.for("@ljoukov/llm.filesState"), () => ({
|
|
3375
3383
|
metadataById: /* @__PURE__ */ new Map(),
|
|
3376
|
-
|
|
3384
|
+
canonicalUploadCacheByKey: /* @__PURE__ */ new Map(),
|
|
3377
3385
|
materializedById: /* @__PURE__ */ new Map(),
|
|
3378
3386
|
geminiMirrorById: /* @__PURE__ */ new Map(),
|
|
3379
3387
|
vertexMirrorById: /* @__PURE__ */ new Map(),
|
|
@@ -3454,7 +3462,7 @@ function formatUploadLogLine(event) {
|
|
|
3454
3462
|
}
|
|
3455
3463
|
function recordUploadEvent(event) {
|
|
3456
3464
|
const scope = fileUploadScopeStorage.getStore();
|
|
3457
|
-
const resolvedSource = event.source ?? scope?.source ?? (event.backend === "
|
|
3465
|
+
const resolvedSource = event.source ?? scope?.source ?? (event.backend === "gcs" ? "files_api" : "provider_mirror");
|
|
3458
3466
|
const timestampedEvent = {
|
|
3459
3467
|
...event,
|
|
3460
3468
|
source: resolvedSource,
|
|
@@ -3501,16 +3509,117 @@ async function computeFileSha256Hex(filePath) {
|
|
|
3501
3509
|
}
|
|
3502
3510
|
return hash.digest("hex");
|
|
3503
3511
|
}
|
|
3504
|
-
function
|
|
3512
|
+
function buildCanonicalFileId(filename, mimeType, sha256Hex) {
|
|
3513
|
+
return `file_${(0, import_node_crypto.createHash)("sha256").update(filename).update("\0").update(mimeType).update("\0").update(sha256Hex).digest("hex")}`;
|
|
3514
|
+
}
|
|
3515
|
+
function resolveCanonicalFilesBucket() {
|
|
3516
|
+
const raw = process.env.LLM_FILES_GCS_BUCKET ?? process.env.VERTEX_GCS_BUCKET ?? process.env.LLM_VERTEX_GCS_BUCKET;
|
|
3517
|
+
const trimmed = raw?.trim();
|
|
3518
|
+
if (!trimmed) {
|
|
3519
|
+
throw new Error(
|
|
3520
|
+
"LLM_FILES_GCS_BUCKET (or VERTEX_GCS_BUCKET) must be set to use the canonical files API."
|
|
3521
|
+
);
|
|
3522
|
+
}
|
|
3523
|
+
return trimmed.replace(/^gs:\/\//u, "").replace(/\/+$/u, "");
|
|
3524
|
+
}
|
|
3525
|
+
function resolveCanonicalFilesPrefix() {
|
|
3526
|
+
const raw = process.env.LLM_FILES_GCS_PREFIX;
|
|
3527
|
+
const trimmed = raw?.trim().replace(/^\/+/u, "").replace(/\/+$/u, "");
|
|
3528
|
+
return trimmed ? `${trimmed}/` : "canonical-files/";
|
|
3529
|
+
}
|
|
3530
|
+
function isLatexLikeFile(filename, mimeType) {
|
|
3531
|
+
const extension = import_node_path4.default.extname(filename).trim().toLowerCase();
|
|
3532
|
+
const normalisedMimeType = mimeType.trim().toLowerCase();
|
|
3533
|
+
return extension === ".tex" || extension === ".ltx" || extension === ".latex" || normalisedMimeType === "application/x-tex" || normalisedMimeType === "text/x-tex";
|
|
3534
|
+
}
|
|
3535
|
+
function resolveCanonicalStorageContentType(filename, mimeType) {
|
|
3536
|
+
if (isLatexLikeFile(filename, mimeType)) {
|
|
3537
|
+
return "text/plain";
|
|
3538
|
+
}
|
|
3539
|
+
return mimeType;
|
|
3540
|
+
}
|
|
3541
|
+
function resolveCanonicalObjectExtension(filename, mimeType) {
|
|
3542
|
+
if (isLatexLikeFile(filename, mimeType)) {
|
|
3543
|
+
return "txt";
|
|
3544
|
+
}
|
|
3545
|
+
const fromFilename = import_node_path4.default.extname(filename).replace(/^\./u, "").trim().toLowerCase();
|
|
3546
|
+
if (fromFilename) {
|
|
3547
|
+
return fromFilename;
|
|
3548
|
+
}
|
|
3549
|
+
const fromMimeType = import_mime.default.getExtension(mimeType)?.trim().toLowerCase();
|
|
3550
|
+
if (fromMimeType) {
|
|
3551
|
+
return fromMimeType;
|
|
3552
|
+
}
|
|
3553
|
+
return "bin";
|
|
3554
|
+
}
|
|
3555
|
+
function buildCanonicalObjectName(fileId, filename, mimeType) {
|
|
3556
|
+
const extension = resolveCanonicalObjectExtension(filename, mimeType);
|
|
3557
|
+
return `${resolveCanonicalFilesPrefix()}${fileId}.${extension}`;
|
|
3558
|
+
}
|
|
3559
|
+
function toSafeStorageFilename(filename) {
|
|
3560
|
+
const normalized = normaliseFilename(filename).replace(/[^\w.-]+/gu, "-");
|
|
3561
|
+
return normalized.length > 0 ? normalized : "attachment.bin";
|
|
3562
|
+
}
|
|
3563
|
+
function parseUnixSeconds(value, fallback) {
|
|
3564
|
+
if (value) {
|
|
3565
|
+
const numeric = Number.parseInt(value, 10);
|
|
3566
|
+
if (Number.isFinite(numeric) && numeric > 0) {
|
|
3567
|
+
return numeric;
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
if (fallback) {
|
|
3571
|
+
const millis = Date.parse(fallback);
|
|
3572
|
+
if (Number.isFinite(millis)) {
|
|
3573
|
+
return Math.floor(millis / 1e3);
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
return Math.floor(Date.now() / 1e3);
|
|
3577
|
+
}
|
|
3578
|
+
function parseOptionalUnixSeconds(value) {
|
|
3579
|
+
if (!value) {
|
|
3580
|
+
return void 0;
|
|
3581
|
+
}
|
|
3582
|
+
const millis = Date.parse(value);
|
|
3583
|
+
if (Number.isFinite(millis)) {
|
|
3584
|
+
return Math.floor(millis / 1e3);
|
|
3585
|
+
}
|
|
3586
|
+
const numeric = Number.parseInt(value, 10);
|
|
3587
|
+
return Number.isFinite(numeric) && numeric > 0 ? numeric : void 0;
|
|
3588
|
+
}
|
|
3589
|
+
function toStoredFileFromCanonicalMetadata(options) {
|
|
3590
|
+
const metadata = options.objectMetadata.metadata;
|
|
3591
|
+
const filenameRaw = typeof metadata?.filename === "string" && metadata.filename.trim().length > 0 ? metadata.filename.trim() : import_node_path4.default.basename(options.objectName);
|
|
3592
|
+
const filename = normaliseFilename(filenameRaw);
|
|
3593
|
+
const bytesRaw = options.objectMetadata.size;
|
|
3594
|
+
const bytes = typeof bytesRaw === "string" ? Number.parseInt(bytesRaw, 10) : typeof bytesRaw === "number" ? bytesRaw : 0;
|
|
3595
|
+
const purpose = metadata?.purpose === "user_data" ? "user_data" : "user_data";
|
|
3596
|
+
const createdAt = parseUnixSeconds(
|
|
3597
|
+
typeof metadata?.createdAtUnix === "string" ? metadata.createdAtUnix : void 0,
|
|
3598
|
+
typeof options.objectMetadata.timeCreated === "string" ? options.objectMetadata.timeCreated : void 0
|
|
3599
|
+
);
|
|
3600
|
+
const expiresAt = parseOptionalUnixSeconds(
|
|
3601
|
+
typeof metadata?.expiresAt === "string" ? metadata.expiresAt : void 0
|
|
3602
|
+
);
|
|
3603
|
+
const mimeType = typeof metadata?.mimeType === "string" && metadata.mimeType.trim().length > 0 ? metadata.mimeType.trim() : typeof options.objectMetadata.contentType === "string" && options.objectMetadata.contentType.trim().length > 0 ? options.objectMetadata.contentType.trim() : resolveMimeType(filename, void 0);
|
|
3604
|
+
const sha256Hex = typeof metadata?.sha256 === "string" && metadata.sha256.trim().length > 0 ? metadata.sha256.trim() : void 0;
|
|
3505
3605
|
return {
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3606
|
+
file: {
|
|
3607
|
+
id: options.fileId,
|
|
3608
|
+
bytes: Number.isFinite(bytes) ? bytes : 0,
|
|
3609
|
+
created_at: createdAt,
|
|
3610
|
+
filename,
|
|
3611
|
+
object: "file",
|
|
3612
|
+
purpose,
|
|
3613
|
+
status: "processed",
|
|
3614
|
+
...expiresAt ? { expires_at: expiresAt } : {}
|
|
3615
|
+
},
|
|
3616
|
+
filename,
|
|
3617
|
+
bytes: Number.isFinite(bytes) ? bytes : 0,
|
|
3618
|
+
mimeType,
|
|
3619
|
+
sha256Hex,
|
|
3620
|
+
localPath: options.localPath,
|
|
3621
|
+
bucketName: options.bucketName,
|
|
3622
|
+
objectName: options.objectName
|
|
3514
3623
|
};
|
|
3515
3624
|
}
|
|
3516
3625
|
function buildCacheKey(filename, mimeType, sha256Hex) {
|
|
@@ -3531,7 +3640,7 @@ function isFresh(file) {
|
|
|
3531
3640
|
function recordMetadata(metadata) {
|
|
3532
3641
|
filesState.metadataById.set(metadata.file.id, metadata);
|
|
3533
3642
|
if (metadata.sha256Hex) {
|
|
3534
|
-
filesState.
|
|
3643
|
+
filesState.canonicalUploadCacheByKey.set(
|
|
3535
3644
|
buildCacheKey(
|
|
3536
3645
|
metadata.filename,
|
|
3537
3646
|
metadata.mimeType ?? "application/octet-stream",
|
|
@@ -3580,7 +3689,9 @@ async function persistMetadataToDisk(metadata) {
|
|
|
3580
3689
|
bytes: metadata.bytes,
|
|
3581
3690
|
mimeType: metadata.mimeType,
|
|
3582
3691
|
sha256Hex: metadata.sha256Hex,
|
|
3583
|
-
localPath: metadata.localPath
|
|
3692
|
+
localPath: metadata.localPath,
|
|
3693
|
+
bucketName: metadata.bucketName,
|
|
3694
|
+
objectName: metadata.objectName
|
|
3584
3695
|
};
|
|
3585
3696
|
await (0, import_promises2.writeFile)(
|
|
3586
3697
|
buildCachedMetadataPath(metadata.file.id),
|
|
@@ -3612,175 +3723,271 @@ async function loadPersistedMetadata(fileId) {
|
|
|
3612
3723
|
bytes: payload.bytes,
|
|
3613
3724
|
mimeType: payload.mimeType,
|
|
3614
3725
|
sha256Hex: payload.sha256Hex,
|
|
3615
|
-
localPath: payload.localPath
|
|
3726
|
+
localPath: payload.localPath,
|
|
3727
|
+
bucketName: payload.bucketName,
|
|
3728
|
+
objectName: payload.objectName
|
|
3616
3729
|
});
|
|
3617
3730
|
} catch {
|
|
3618
3731
|
return void 0;
|
|
3619
3732
|
}
|
|
3620
3733
|
}
|
|
3621
|
-
async function
|
|
3622
|
-
const
|
|
3623
|
-
const
|
|
3624
|
-
|
|
3625
|
-
|
|
3734
|
+
async function writeCanonicalFileFromPath(options) {
|
|
3735
|
+
const file = getStorageClient().bucket(options.bucketName).file(options.objectName);
|
|
3736
|
+
const storageContentType = resolveCanonicalStorageContentType(
|
|
3737
|
+
options.metadata.filename ?? "attachment.bin",
|
|
3738
|
+
options.mimeType
|
|
3739
|
+
);
|
|
3740
|
+
try {
|
|
3741
|
+
await (0, import_promises3.pipeline)(
|
|
3742
|
+
(0, import_node_fs3.createReadStream)(options.filePath),
|
|
3743
|
+
file.createWriteStream({
|
|
3744
|
+
resumable: options.bytes >= 10 * 1024 * 1024,
|
|
3745
|
+
preconditionOpts: { ifGenerationMatch: 0 },
|
|
3746
|
+
metadata: {
|
|
3747
|
+
contentType: storageContentType,
|
|
3748
|
+
contentDisposition: `inline; filename="${toSafeStorageFilename(options.metadata.filename ?? "attachment.bin")}"`,
|
|
3749
|
+
metadata: options.metadata
|
|
3750
|
+
}
|
|
3751
|
+
})
|
|
3752
|
+
);
|
|
3753
|
+
return true;
|
|
3754
|
+
} catch (error) {
|
|
3755
|
+
const code = error.code;
|
|
3756
|
+
if (code === 412 || code === "412") {
|
|
3757
|
+
return false;
|
|
3758
|
+
}
|
|
3759
|
+
throw error;
|
|
3626
3760
|
}
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3761
|
+
}
|
|
3762
|
+
async function writeCanonicalFileFromBytes(options) {
|
|
3763
|
+
const file = getStorageClient().bucket(options.bucketName).file(options.objectName);
|
|
3764
|
+
const storageContentType = resolveCanonicalStorageContentType(
|
|
3765
|
+
options.metadata.filename ?? "attachment.bin",
|
|
3766
|
+
options.mimeType
|
|
3767
|
+
);
|
|
3768
|
+
try {
|
|
3769
|
+
await file.save(options.bytes, {
|
|
3770
|
+
resumable: options.bytes.byteLength >= 10 * 1024 * 1024,
|
|
3771
|
+
preconditionOpts: { ifGenerationMatch: 0 },
|
|
3772
|
+
metadata: {
|
|
3773
|
+
contentType: storageContentType,
|
|
3774
|
+
contentDisposition: `inline; filename="${toSafeStorageFilename(options.metadata.filename ?? "attachment.bin")}"`,
|
|
3775
|
+
metadata: options.metadata
|
|
3641
3776
|
}
|
|
3642
3777
|
});
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
const
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
mime_type: params.mimeType,
|
|
3649
|
-
purpose: params.purpose
|
|
3650
|
-
});
|
|
3651
|
-
const partIds = [];
|
|
3652
|
-
for (let offset = 0; offset < params.bytes.byteLength; offset += OPENAI_UPLOAD_PART_MAX_BYTES) {
|
|
3653
|
-
const chunk = params.bytes.subarray(
|
|
3654
|
-
offset,
|
|
3655
|
-
Math.min(offset + OPENAI_UPLOAD_PART_MAX_BYTES, params.bytes.byteLength)
|
|
3656
|
-
);
|
|
3657
|
-
const uploadPart = await client.uploads.parts.create(upload.id, {
|
|
3658
|
-
data: new import_node_buffer3.File([new Uint8Array(chunk)], `${params.sha256Hex}.part`, {
|
|
3659
|
-
type: params.mimeType
|
|
3660
|
-
})
|
|
3661
|
-
});
|
|
3662
|
-
partIds.push(uploadPart.id);
|
|
3663
|
-
}
|
|
3664
|
-
const completed = await client.uploads.complete(upload.id, { part_ids: partIds });
|
|
3665
|
-
const fileId = completed.file?.id;
|
|
3666
|
-
if (!fileId) {
|
|
3667
|
-
throw new Error("OpenAI upload completed without a file id.");
|
|
3778
|
+
return true;
|
|
3779
|
+
} catch (error) {
|
|
3780
|
+
const code = error.code;
|
|
3781
|
+
if (code === 412 || code === "412") {
|
|
3782
|
+
return false;
|
|
3668
3783
|
}
|
|
3669
|
-
|
|
3784
|
+
throw error;
|
|
3670
3785
|
}
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
filename
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3786
|
+
}
|
|
3787
|
+
async function refreshCanonicalObjectMetadata(options) {
|
|
3788
|
+
const storageContentType = resolveCanonicalStorageContentType(
|
|
3789
|
+
options.metadata.filename ?? "attachment.bin",
|
|
3790
|
+
options.mimeType
|
|
3791
|
+
);
|
|
3792
|
+
await getStorageClient().bucket(options.bucketName).file(options.objectName).setMetadata({
|
|
3793
|
+
contentType: storageContentType,
|
|
3794
|
+
contentDisposition: `inline; filename="${toSafeStorageFilename(options.metadata.filename ?? "attachment.bin")}"`,
|
|
3795
|
+
metadata: options.metadata
|
|
3678
3796
|
});
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3797
|
+
}
|
|
3798
|
+
async function createCanonicalMetadata(options) {
|
|
3799
|
+
const createdAt = Math.floor(Date.now() / 1e3);
|
|
3800
|
+
const expiresAt = createdAt + options.expiresAfterSeconds;
|
|
3801
|
+
const storedFile = {
|
|
3802
|
+
id: options.fileId,
|
|
3803
|
+
bytes: options.bytes,
|
|
3804
|
+
created_at: createdAt,
|
|
3805
|
+
filename: options.filename,
|
|
3806
|
+
object: "file",
|
|
3807
|
+
purpose: options.purpose,
|
|
3808
|
+
status: "processed",
|
|
3809
|
+
expires_at: expiresAt
|
|
3810
|
+
};
|
|
3811
|
+
const metadata = recordMetadata({
|
|
3812
|
+
file: storedFile,
|
|
3813
|
+
filename: options.filename,
|
|
3814
|
+
bytes: options.bytes,
|
|
3815
|
+
mimeType: options.mimeType,
|
|
3816
|
+
sha256Hex: options.sha256Hex,
|
|
3817
|
+
localPath: options.localPath,
|
|
3818
|
+
bucketName: options.bucketName,
|
|
3819
|
+
objectName: options.objectName
|
|
3687
3820
|
});
|
|
3821
|
+
await persistMetadataToDisk(metadata);
|
|
3688
3822
|
return metadata;
|
|
3689
3823
|
}
|
|
3690
|
-
async function
|
|
3824
|
+
async function uploadCanonicalFileFromBytes(params) {
|
|
3691
3825
|
const cacheKey = buildCacheKey(params.filename, params.mimeType, params.sha256Hex);
|
|
3692
|
-
const cached = filesState.
|
|
3826
|
+
const cached = filesState.canonicalUploadCacheByKey.get(cacheKey);
|
|
3693
3827
|
if (cached && isFresh(cached.file)) {
|
|
3694
3828
|
return cached;
|
|
3695
3829
|
}
|
|
3696
|
-
const
|
|
3830
|
+
const fileId = buildCanonicalFileId(params.filename, params.mimeType, params.sha256Hex);
|
|
3831
|
+
const bucketName = resolveCanonicalFilesBucket();
|
|
3832
|
+
const objectName = buildCanonicalObjectName(fileId, params.filename, params.mimeType);
|
|
3833
|
+
const metadataFields = {
|
|
3834
|
+
fileId,
|
|
3835
|
+
filename: params.filename,
|
|
3836
|
+
mimeType: params.mimeType,
|
|
3837
|
+
purpose: params.purpose,
|
|
3838
|
+
sha256: params.sha256Hex,
|
|
3839
|
+
createdAtUnix: Math.floor(Date.now() / 1e3).toString(),
|
|
3840
|
+
expiresAt: new Date(Date.now() + params.expiresAfterSeconds * 1e3).toISOString()
|
|
3841
|
+
};
|
|
3697
3842
|
const startedAtMs = Date.now();
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3843
|
+
const uploaded = await writeCanonicalFileFromBytes({
|
|
3844
|
+
bytes: params.bytes,
|
|
3845
|
+
bucketName,
|
|
3846
|
+
objectName,
|
|
3847
|
+
mimeType: params.mimeType,
|
|
3848
|
+
metadata: metadataFields
|
|
3849
|
+
});
|
|
3850
|
+
if (!uploaded) {
|
|
3851
|
+
await refreshCanonicalObjectMetadata({
|
|
3852
|
+
bucketName,
|
|
3853
|
+
objectName,
|
|
3854
|
+
mimeType: params.mimeType,
|
|
3855
|
+
metadata: metadataFields
|
|
3710
3856
|
});
|
|
3711
|
-
}
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3857
|
+
}
|
|
3858
|
+
const localPath = await cacheBufferLocally(params.bytes, params.sha256Hex);
|
|
3859
|
+
const canonical = await createCanonicalMetadata({
|
|
3860
|
+
fileId,
|
|
3861
|
+
filename: params.filename,
|
|
3862
|
+
mimeType: params.mimeType,
|
|
3863
|
+
purpose: params.purpose,
|
|
3864
|
+
expiresAfterSeconds: params.expiresAfterSeconds,
|
|
3865
|
+
sha256Hex: params.sha256Hex,
|
|
3866
|
+
bytes: params.bytes.byteLength,
|
|
3867
|
+
bucketName,
|
|
3868
|
+
objectName,
|
|
3869
|
+
localPath
|
|
3870
|
+
});
|
|
3871
|
+
if (uploaded) {
|
|
3872
|
+
recordUploadEvent({
|
|
3873
|
+
backend: "gcs",
|
|
3874
|
+
mode: "gcs",
|
|
3715
3875
|
filename: params.filename,
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
highWaterMark: OPENAI_UPLOAD_PART_MAX_BYTES
|
|
3876
|
+
bytes: params.bytes.byteLength,
|
|
3877
|
+
durationMs: Math.max(0, Date.now() - startedAtMs),
|
|
3878
|
+
mimeType: params.mimeType,
|
|
3879
|
+
fileId,
|
|
3880
|
+
fileUri: `gs://${bucketName}/${objectName}`
|
|
3722
3881
|
});
|
|
3723
|
-
let partIndex = 0;
|
|
3724
|
-
for await (const chunk of stream) {
|
|
3725
|
-
const buffer = import_node_buffer3.Buffer.isBuffer(chunk) ? chunk : import_node_buffer3.Buffer.from(chunk);
|
|
3726
|
-
const uploadPart = await client.uploads.parts.create(upload.id, {
|
|
3727
|
-
data: new import_node_buffer3.File(
|
|
3728
|
-
[new Uint8Array(buffer)],
|
|
3729
|
-
`${params.sha256Hex}.${partIndex.toString()}.part`,
|
|
3730
|
-
{
|
|
3731
|
-
type: params.mimeType
|
|
3732
|
-
}
|
|
3733
|
-
)
|
|
3734
|
-
});
|
|
3735
|
-
partIds.push(uploadPart.id);
|
|
3736
|
-
partIndex += 1;
|
|
3737
|
-
}
|
|
3738
|
-
const completed = await client.uploads.complete(upload.id, { part_ids: partIds });
|
|
3739
|
-
const fileId = completed.file?.id;
|
|
3740
|
-
if (!fileId) {
|
|
3741
|
-
throw new Error("OpenAI upload completed without a file id.");
|
|
3742
|
-
}
|
|
3743
|
-
uploaded = await client.files.retrieve(fileId);
|
|
3744
3882
|
}
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3883
|
+
return canonical;
|
|
3884
|
+
}
|
|
3885
|
+
async function uploadCanonicalFileFromPath(params) {
|
|
3886
|
+
const cacheKey = buildCacheKey(params.filename, params.mimeType, params.sha256Hex);
|
|
3887
|
+
const cached = filesState.canonicalUploadCacheByKey.get(cacheKey);
|
|
3888
|
+
if (cached && isFresh(cached.file)) {
|
|
3889
|
+
return cached;
|
|
3890
|
+
}
|
|
3891
|
+
const fileId = buildCanonicalFileId(params.filename, params.mimeType, params.sha256Hex);
|
|
3892
|
+
const bucketName = resolveCanonicalFilesBucket();
|
|
3893
|
+
const objectName = buildCanonicalObjectName(fileId, params.filename, params.mimeType);
|
|
3894
|
+
const metadataFields = {
|
|
3895
|
+
fileId,
|
|
3896
|
+
filename: params.filename,
|
|
3897
|
+
mimeType: params.mimeType,
|
|
3898
|
+
purpose: params.purpose,
|
|
3899
|
+
sha256: params.sha256Hex,
|
|
3900
|
+
createdAtUnix: Math.floor(Date.now() / 1e3).toString(),
|
|
3901
|
+
expiresAt: new Date(Date.now() + params.expiresAfterSeconds * 1e3).toISOString()
|
|
3902
|
+
};
|
|
3903
|
+
const startedAtMs = Date.now();
|
|
3904
|
+
const uploaded = await writeCanonicalFileFromPath({
|
|
3905
|
+
filePath: params.filePath,
|
|
3906
|
+
bucketName,
|
|
3907
|
+
objectName,
|
|
3908
|
+
bytes: params.bytes,
|
|
3750
3909
|
mimeType: params.mimeType,
|
|
3751
|
-
|
|
3910
|
+
metadata: metadataFields
|
|
3752
3911
|
});
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3912
|
+
if (!uploaded) {
|
|
3913
|
+
await refreshCanonicalObjectMetadata({
|
|
3914
|
+
bucketName,
|
|
3915
|
+
objectName,
|
|
3916
|
+
mimeType: params.mimeType,
|
|
3917
|
+
metadata: metadataFields
|
|
3918
|
+
});
|
|
3919
|
+
}
|
|
3920
|
+
const localPath = await cacheFileLocally(params.filePath, params.sha256Hex);
|
|
3921
|
+
const canonical = await createCanonicalMetadata({
|
|
3922
|
+
fileId,
|
|
3923
|
+
filename: params.filename,
|
|
3759
3924
|
mimeType: params.mimeType,
|
|
3760
|
-
|
|
3925
|
+
purpose: params.purpose,
|
|
3926
|
+
expiresAfterSeconds: params.expiresAfterSeconds,
|
|
3927
|
+
sha256Hex: params.sha256Hex,
|
|
3928
|
+
bytes: params.bytes,
|
|
3929
|
+
bucketName,
|
|
3930
|
+
objectName,
|
|
3931
|
+
localPath
|
|
3761
3932
|
});
|
|
3762
|
-
|
|
3933
|
+
if (uploaded) {
|
|
3934
|
+
recordUploadEvent({
|
|
3935
|
+
backend: "gcs",
|
|
3936
|
+
mode: "gcs",
|
|
3937
|
+
filename: params.filename,
|
|
3938
|
+
bytes: params.bytes,
|
|
3939
|
+
durationMs: Math.max(0, Date.now() - startedAtMs),
|
|
3940
|
+
mimeType: params.mimeType,
|
|
3941
|
+
fileId,
|
|
3942
|
+
fileUri: `gs://${bucketName}/${objectName}`
|
|
3943
|
+
});
|
|
3944
|
+
}
|
|
3945
|
+
return canonical;
|
|
3946
|
+
}
|
|
3947
|
+
async function resolveCanonicalStorageLocation(fileId) {
|
|
3948
|
+
const cached = filesState.metadataById.get(fileId) ?? await loadPersistedMetadata(fileId);
|
|
3949
|
+
if (cached?.bucketName && cached.objectName) {
|
|
3950
|
+
return {
|
|
3951
|
+
bucketName: cached.bucketName,
|
|
3952
|
+
objectName: cached.objectName
|
|
3953
|
+
};
|
|
3954
|
+
}
|
|
3955
|
+
const bucketName = resolveCanonicalFilesBucket();
|
|
3956
|
+
const [files2] = await getStorageClient().bucket(bucketName).getFiles({
|
|
3957
|
+
prefix: `${resolveCanonicalFilesPrefix()}${fileId}.`,
|
|
3958
|
+
maxResults: 1,
|
|
3959
|
+
autoPaginate: false
|
|
3960
|
+
});
|
|
3961
|
+
const file = files2[0];
|
|
3962
|
+
if (!file) {
|
|
3963
|
+
throw new Error(`Canonical file ${fileId} was not found in GCS.`);
|
|
3964
|
+
}
|
|
3965
|
+
return {
|
|
3966
|
+
bucketName,
|
|
3967
|
+
objectName: file.name
|
|
3968
|
+
};
|
|
3763
3969
|
}
|
|
3764
|
-
async function
|
|
3970
|
+
async function retrieveCanonicalFile(fileId) {
|
|
3765
3971
|
const cached = filesState.metadataById.get(fileId);
|
|
3766
|
-
if (cached && isFresh(cached.file)) {
|
|
3972
|
+
if (cached && isFresh(cached.file) && cached.bucketName && cached.objectName) {
|
|
3767
3973
|
return cached;
|
|
3768
3974
|
}
|
|
3769
3975
|
const persisted = await loadPersistedMetadata(fileId);
|
|
3770
|
-
if (persisted && isFresh(persisted.file)) {
|
|
3976
|
+
if (persisted && isFresh(persisted.file) && persisted.bucketName && persisted.objectName) {
|
|
3771
3977
|
return persisted;
|
|
3772
3978
|
}
|
|
3773
|
-
const
|
|
3774
|
-
const
|
|
3775
|
-
const
|
|
3776
|
-
const metadata = recordMetadata(
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3979
|
+
const existingLocalPath = cached?.localPath ?? persisted?.localPath;
|
|
3980
|
+
const { bucketName, objectName } = await resolveCanonicalStorageLocation(fileId);
|
|
3981
|
+
const [objectMetadata] = await getStorageClient().bucket(bucketName).file(objectName).getMetadata();
|
|
3982
|
+
const metadata = recordMetadata(
|
|
3983
|
+
toStoredFileFromCanonicalMetadata({
|
|
3984
|
+
fileId,
|
|
3985
|
+
bucketName,
|
|
3986
|
+
objectName,
|
|
3987
|
+
objectMetadata,
|
|
3988
|
+
localPath: existingLocalPath
|
|
3989
|
+
})
|
|
3990
|
+
);
|
|
3784
3991
|
await persistMetadataToDisk(metadata);
|
|
3785
3992
|
return metadata;
|
|
3786
3993
|
}
|
|
@@ -3808,7 +4015,7 @@ function resolveVertexMirrorBucket() {
|
|
|
3808
4015
|
const trimmed = raw?.trim();
|
|
3809
4016
|
if (!trimmed) {
|
|
3810
4017
|
throw new Error(
|
|
3811
|
-
"VERTEX_GCS_BUCKET must be set to use
|
|
4018
|
+
"VERTEX_GCS_BUCKET must be set to use canonical file ids with Vertex Gemini models."
|
|
3812
4019
|
);
|
|
3813
4020
|
}
|
|
3814
4021
|
return trimmed.replace(/^gs:\/\//u, "").replace(/\/+$/u, "");
|
|
@@ -3838,61 +4045,41 @@ function getGeminiMirrorClient() {
|
|
|
3838
4045
|
}
|
|
3839
4046
|
return filesState.geminiClientPromise;
|
|
3840
4047
|
}
|
|
3841
|
-
async function
|
|
4048
|
+
async function materializeCanonicalFile(fileId) {
|
|
3842
4049
|
const cachedPromise = filesState.materializedById.get(fileId);
|
|
3843
4050
|
if (cachedPromise) {
|
|
3844
4051
|
return await cachedPromise;
|
|
3845
4052
|
}
|
|
3846
4053
|
const promise = (async () => {
|
|
3847
|
-
const metadata = await
|
|
3848
|
-
if (metadata.localPath && metadata.sha256Hex && metadata.mimeType) {
|
|
4054
|
+
const metadata = await retrieveCanonicalFile(fileId);
|
|
4055
|
+
if (metadata.localPath && metadata.sha256Hex && metadata.mimeType && metadata.bucketName && metadata.objectName) {
|
|
3849
4056
|
return {
|
|
3850
4057
|
file: metadata.file,
|
|
3851
4058
|
filename: metadata.filename,
|
|
3852
4059
|
bytes: metadata.bytes,
|
|
3853
4060
|
mimeType: metadata.mimeType,
|
|
3854
4061
|
sha256Hex: metadata.sha256Hex,
|
|
3855
|
-
localPath: metadata.localPath
|
|
4062
|
+
localPath: metadata.localPath,
|
|
4063
|
+
bucketName: metadata.bucketName,
|
|
4064
|
+
objectName: metadata.objectName
|
|
3856
4065
|
};
|
|
3857
4066
|
}
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
import_node_path4.default.join(FILES_TEMP_ROOT, `${fileId.replace(/[^a-z0-9_-]/giu, "")}-`)
|
|
3861
|
-
);
|
|
3862
|
-
const localPath = import_node_path4.default.join(tempDir, normaliseFilename(metadata.filename, `${fileId}.bin`));
|
|
3863
|
-
const response = await getOpenAiClient().files.content(fileId);
|
|
3864
|
-
if (!response.ok) {
|
|
3865
|
-
throw new Error(
|
|
3866
|
-
`Failed to download OpenAI file ${fileId}: ${response.status} ${response.statusText}`
|
|
3867
|
-
);
|
|
3868
|
-
}
|
|
3869
|
-
const responseMimeType = response.headers.get("content-type")?.trim() || void 0;
|
|
3870
|
-
const mimeType = resolveMimeType(metadata.filename, responseMimeType);
|
|
3871
|
-
const hash = (0, import_node_crypto.createHash)("sha256");
|
|
3872
|
-
let bytes = 0;
|
|
3873
|
-
if (response.body) {
|
|
3874
|
-
const source = import_node_stream.Readable.fromWeb(response.body);
|
|
3875
|
-
const writable = (0, import_node_fs3.createWriteStream)(localPath, { flags: "wx" });
|
|
3876
|
-
source.on("data", (chunk) => {
|
|
3877
|
-
const buffer = import_node_buffer3.Buffer.isBuffer(chunk) ? chunk : import_node_buffer3.Buffer.from(chunk);
|
|
3878
|
-
hash.update(buffer);
|
|
3879
|
-
bytes += buffer.byteLength;
|
|
3880
|
-
});
|
|
3881
|
-
await (0, import_promises3.pipeline)(source, writable);
|
|
3882
|
-
} else {
|
|
3883
|
-
const buffer = import_node_buffer3.Buffer.from(await response.arrayBuffer());
|
|
3884
|
-
hash.update(buffer);
|
|
3885
|
-
bytes = buffer.byteLength;
|
|
3886
|
-
await (0, import_promises2.writeFile)(localPath, buffer);
|
|
4067
|
+
if (!metadata.bucketName || !metadata.objectName) {
|
|
4068
|
+
throw new Error(`Canonical file ${fileId} is missing GCS location metadata.`);
|
|
3887
4069
|
}
|
|
3888
|
-
const
|
|
4070
|
+
const [downloadedBytes] = await getStorageClient().bucket(metadata.bucketName).file(metadata.objectName).download();
|
|
4071
|
+
const mimeType = metadata.mimeType ?? resolveMimeType(metadata.filename, void 0);
|
|
4072
|
+
const sha256Hex = metadata.sha256Hex ?? computeSha256Hex(downloadedBytes);
|
|
4073
|
+
const localPath = await cacheBufferLocally(downloadedBytes, sha256Hex);
|
|
3889
4074
|
const updated = recordMetadata({
|
|
3890
4075
|
file: metadata.file,
|
|
3891
4076
|
filename: metadata.filename,
|
|
3892
|
-
bytes:
|
|
4077
|
+
bytes: downloadedBytes.byteLength || metadata.bytes,
|
|
3893
4078
|
mimeType,
|
|
3894
4079
|
sha256Hex,
|
|
3895
|
-
localPath
|
|
4080
|
+
localPath,
|
|
4081
|
+
bucketName: metadata.bucketName,
|
|
4082
|
+
objectName: metadata.objectName
|
|
3896
4083
|
});
|
|
3897
4084
|
await persistMetadataToDisk(updated);
|
|
3898
4085
|
return {
|
|
@@ -3901,7 +4088,9 @@ async function materializeOpenAiFile(fileId) {
|
|
|
3901
4088
|
bytes: updated.bytes,
|
|
3902
4089
|
mimeType: updated.mimeType ?? mimeType,
|
|
3903
4090
|
sha256Hex,
|
|
3904
|
-
localPath
|
|
4091
|
+
localPath,
|
|
4092
|
+
bucketName: metadata.bucketName,
|
|
4093
|
+
objectName: metadata.objectName
|
|
3905
4094
|
};
|
|
3906
4095
|
})();
|
|
3907
4096
|
filesState.materializedById.set(fileId, promise);
|
|
@@ -3917,14 +4106,14 @@ async function ensureGeminiFileMirror(fileId) {
|
|
|
3917
4106
|
if (cached) {
|
|
3918
4107
|
return cached;
|
|
3919
4108
|
}
|
|
3920
|
-
const materialized = await
|
|
4109
|
+
const materialized = await materializeCanonicalFile(fileId);
|
|
3921
4110
|
const client = await getGeminiMirrorClient();
|
|
3922
4111
|
const name = buildGeminiMirrorName(materialized.sha256Hex);
|
|
3923
4112
|
try {
|
|
3924
4113
|
const existing = await client.files.get({ name });
|
|
3925
4114
|
if (existing.name && existing.uri && existing.mimeType) {
|
|
3926
4115
|
const mirror2 = {
|
|
3927
|
-
|
|
4116
|
+
canonicalFileId: fileId,
|
|
3928
4117
|
name: existing.name,
|
|
3929
4118
|
uri: existing.uri,
|
|
3930
4119
|
mimeType: existing.mimeType,
|
|
@@ -3952,7 +4141,7 @@ async function ensureGeminiFileMirror(fileId) {
|
|
|
3952
4141
|
throw new Error("Gemini file upload completed without a usable URI.");
|
|
3953
4142
|
}
|
|
3954
4143
|
const mirror = {
|
|
3955
|
-
|
|
4144
|
+
canonicalFileId: fileId,
|
|
3956
4145
|
name: resolved.name,
|
|
3957
4146
|
uri: resolved.uri,
|
|
3958
4147
|
mimeType: resolved.mimeType,
|
|
@@ -3977,7 +4166,7 @@ async function ensureVertexFileMirror(fileId) {
|
|
|
3977
4166
|
if (cached) {
|
|
3978
4167
|
return cached;
|
|
3979
4168
|
}
|
|
3980
|
-
const materialized = await
|
|
4169
|
+
const materialized = await materializeCanonicalFile(fileId);
|
|
3981
4170
|
const bucketName = resolveVertexMirrorBucket();
|
|
3982
4171
|
const prefix = resolveVertexMirrorPrefix();
|
|
3983
4172
|
const extension = import_mime.default.getExtension(materialized.mimeType) ?? import_node_path4.default.extname(materialized.filename).replace(/^\./u, "") ?? "bin";
|
|
@@ -4018,7 +4207,7 @@ async function ensureVertexFileMirror(fileId) {
|
|
|
4018
4207
|
}
|
|
4019
4208
|
}
|
|
4020
4209
|
const mirror = {
|
|
4021
|
-
|
|
4210
|
+
canonicalFileId: fileId,
|
|
4022
4211
|
bucket: bucketName,
|
|
4023
4212
|
objectName,
|
|
4024
4213
|
fileUri: `gs://${bucketName}/${objectName}`,
|
|
@@ -4049,7 +4238,7 @@ async function filesCreate(params) {
|
|
|
4049
4238
|
const filename2 = normaliseFilename(params.filename, import_node_path4.default.basename(filePath));
|
|
4050
4239
|
const mimeType2 = resolveMimeType(filename2, params.mimeType);
|
|
4051
4240
|
const sha256Hex2 = await computeFileSha256Hex(filePath);
|
|
4052
|
-
const uploaded2 = await
|
|
4241
|
+
const uploaded2 = await uploadCanonicalFileFromPath({
|
|
4053
4242
|
filePath,
|
|
4054
4243
|
filename: filename2,
|
|
4055
4244
|
mimeType: mimeType2,
|
|
@@ -4058,19 +4247,13 @@ async function filesCreate(params) {
|
|
|
4058
4247
|
sha256Hex: sha256Hex2,
|
|
4059
4248
|
bytes: info.size
|
|
4060
4249
|
});
|
|
4061
|
-
|
|
4062
|
-
const cached2 = recordMetadata({
|
|
4063
|
-
...uploaded2,
|
|
4064
|
-
localPath: localPath2
|
|
4065
|
-
});
|
|
4066
|
-
await persistMetadataToDisk(cached2);
|
|
4067
|
-
return cached2.file;
|
|
4250
|
+
return uploaded2.file;
|
|
4068
4251
|
}
|
|
4069
4252
|
const filename = normaliseFilename(params.filename);
|
|
4070
4253
|
const bytes = toBuffer(params.data);
|
|
4071
4254
|
const mimeType = resolveMimeType(filename, params.mimeType, "text/plain");
|
|
4072
4255
|
const sha256Hex = computeSha256Hex(bytes);
|
|
4073
|
-
const uploaded = await
|
|
4256
|
+
const uploaded = await uploadCanonicalFileFromBytes({
|
|
4074
4257
|
bytes,
|
|
4075
4258
|
filename,
|
|
4076
4259
|
mimeType,
|
|
@@ -4078,16 +4261,10 @@ async function filesCreate(params) {
|
|
|
4078
4261
|
expiresAfterSeconds,
|
|
4079
4262
|
sha256Hex
|
|
4080
4263
|
});
|
|
4081
|
-
|
|
4082
|
-
const cached = recordMetadata({
|
|
4083
|
-
...uploaded,
|
|
4084
|
-
localPath
|
|
4085
|
-
});
|
|
4086
|
-
await persistMetadataToDisk(cached);
|
|
4087
|
-
return cached.file;
|
|
4264
|
+
return uploaded.file;
|
|
4088
4265
|
}
|
|
4089
4266
|
async function filesRetrieve(fileId) {
|
|
4090
|
-
return (await
|
|
4267
|
+
return (await retrieveCanonicalFile(fileId)).file;
|
|
4091
4268
|
}
|
|
4092
4269
|
async function filesDelete(fileId) {
|
|
4093
4270
|
const cachedGemini = filesState.geminiMirrorById.get(fileId);
|
|
@@ -4114,34 +4291,73 @@ async function filesDelete(fileId) {
|
|
|
4114
4291
|
} catch {
|
|
4115
4292
|
}
|
|
4116
4293
|
}
|
|
4117
|
-
|
|
4294
|
+
try {
|
|
4295
|
+
const { bucketName, objectName } = await resolveCanonicalStorageLocation(fileId);
|
|
4296
|
+
await getStorageClient().bucket(bucketName).file(objectName).delete({ ignoreNotFound: true });
|
|
4297
|
+
} catch {
|
|
4298
|
+
}
|
|
4118
4299
|
filesState.metadataById.delete(fileId);
|
|
4300
|
+
filesState.canonicalUploadCacheByKey.forEach((value, key) => {
|
|
4301
|
+
if (value.file.id === fileId) {
|
|
4302
|
+
filesState.canonicalUploadCacheByKey.delete(key);
|
|
4303
|
+
}
|
|
4304
|
+
});
|
|
4119
4305
|
filesState.materializedById.delete(fileId);
|
|
4120
4306
|
try {
|
|
4121
4307
|
await (0, import_promises2.unlink)(buildCachedMetadataPath(fileId));
|
|
4122
4308
|
} catch {
|
|
4123
4309
|
}
|
|
4124
4310
|
return {
|
|
4125
|
-
id:
|
|
4126
|
-
deleted:
|
|
4311
|
+
id: fileId,
|
|
4312
|
+
deleted: true,
|
|
4127
4313
|
object: "file"
|
|
4128
4314
|
};
|
|
4129
4315
|
}
|
|
4130
4316
|
async function filesContent(fileId) {
|
|
4131
|
-
|
|
4317
|
+
const metadata = await retrieveCanonicalFile(fileId);
|
|
4318
|
+
if (!metadata.bucketName || !metadata.objectName) {
|
|
4319
|
+
throw new Error(`Canonical file ${fileId} is missing GCS location metadata.`);
|
|
4320
|
+
}
|
|
4321
|
+
const [bytes] = await getStorageClient().bucket(metadata.bucketName).file(metadata.objectName).download();
|
|
4322
|
+
const headers = new Headers();
|
|
4323
|
+
headers.set("content-type", metadata.mimeType ?? resolveMimeType(metadata.filename, void 0));
|
|
4324
|
+
headers.set("content-length", bytes.byteLength.toString());
|
|
4325
|
+
headers.set(
|
|
4326
|
+
"content-disposition",
|
|
4327
|
+
`inline; filename="${toSafeStorageFilename(metadata.filename)}"`
|
|
4328
|
+
);
|
|
4329
|
+
return new Response(bytes, {
|
|
4330
|
+
status: 200,
|
|
4331
|
+
headers
|
|
4332
|
+
});
|
|
4132
4333
|
}
|
|
4133
4334
|
async function getCanonicalFileMetadata(fileId) {
|
|
4134
|
-
const metadata = await
|
|
4335
|
+
const metadata = await retrieveCanonicalFile(fileId);
|
|
4135
4336
|
const mimeType = metadata.mimeType ?? resolveMimeType(metadata.filename, void 0);
|
|
4136
4337
|
const updated = metadata.mimeType === mimeType ? metadata : recordMetadata({
|
|
4137
4338
|
...metadata,
|
|
4138
4339
|
mimeType
|
|
4139
4340
|
});
|
|
4341
|
+
if (!updated.bucketName || !updated.objectName) {
|
|
4342
|
+
throw new Error(`Canonical file ${fileId} is missing GCS location metadata.`);
|
|
4343
|
+
}
|
|
4140
4344
|
return {
|
|
4141
4345
|
...updated,
|
|
4142
|
-
mimeType
|
|
4346
|
+
mimeType,
|
|
4347
|
+
bucketName: updated.bucketName,
|
|
4348
|
+
objectName: updated.objectName
|
|
4143
4349
|
};
|
|
4144
4350
|
}
|
|
4351
|
+
async function getCanonicalFileSignedUrl(options) {
|
|
4352
|
+
const metadata = await getCanonicalFileMetadata(options.fileId);
|
|
4353
|
+
const [signedUrl] = await getStorageClient().bucket(metadata.bucketName).file(metadata.objectName).getSignedUrl({
|
|
4354
|
+
version: "v4",
|
|
4355
|
+
action: "read",
|
|
4356
|
+
expires: Date.now() + (options.expiresAfterSeconds ?? 15 * 60) * 1e3,
|
|
4357
|
+
responseType: resolveCanonicalStorageContentType(metadata.filename, metadata.mimeType)
|
|
4358
|
+
});
|
|
4359
|
+
return signedUrl;
|
|
4360
|
+
}
|
|
4145
4361
|
var files = {
|
|
4146
4362
|
create: filesCreate,
|
|
4147
4363
|
retrieve: filesRetrieve,
|
|
@@ -4503,6 +4719,7 @@ function isJsonSchemaObject(schema) {
|
|
|
4503
4719
|
return false;
|
|
4504
4720
|
}
|
|
4505
4721
|
var CANONICAL_GEMINI_FILE_URI_PREFIX = "openai://file/";
|
|
4722
|
+
var CANONICAL_LLM_FILE_ID_PATTERN = /^file_[a-f0-9]{64}$/u;
|
|
4506
4723
|
function buildCanonicalGeminiFileUri(fileId) {
|
|
4507
4724
|
return `${CANONICAL_GEMINI_FILE_URI_PREFIX}${fileId}`;
|
|
4508
4725
|
}
|
|
@@ -4513,6 +4730,9 @@ function parseCanonicalGeminiFileId(fileUri) {
|
|
|
4513
4730
|
const fileId = fileUri.slice(CANONICAL_GEMINI_FILE_URI_PREFIX.length).trim();
|
|
4514
4731
|
return fileId.length > 0 ? fileId : void 0;
|
|
4515
4732
|
}
|
|
4733
|
+
function isCanonicalLlmFileId(fileId) {
|
|
4734
|
+
return typeof fileId === "string" && CANONICAL_LLM_FILE_ID_PATTERN.test(fileId.trim());
|
|
4735
|
+
}
|
|
4516
4736
|
function isLlmMediaResolution(value) {
|
|
4517
4737
|
return value === "auto" || value === "low" || value === "medium" || value === "high" || value === "original";
|
|
4518
4738
|
}
|
|
@@ -5000,7 +5220,21 @@ async function prepareOpenAiPromptContentItem(item, options) {
|
|
|
5000
5220
|
if (!isOpenAiNativeContentItem(item)) {
|
|
5001
5221
|
return item;
|
|
5002
5222
|
}
|
|
5003
|
-
if (item.type === "input_image"
|
|
5223
|
+
if (item.type === "input_image") {
|
|
5224
|
+
if (isCanonicalLlmFileId(item.file_id)) {
|
|
5225
|
+
const signedUrl2 = await getCanonicalFileSignedUrl({ fileId: item.file_id });
|
|
5226
|
+
return {
|
|
5227
|
+
type: "input_image",
|
|
5228
|
+
image_url: signedUrl2,
|
|
5229
|
+
detail: toOpenAiImageDetail(
|
|
5230
|
+
isLlmMediaResolution(item.detail) ? item.detail : void 0,
|
|
5231
|
+
options?.model
|
|
5232
|
+
)
|
|
5233
|
+
};
|
|
5234
|
+
}
|
|
5235
|
+
if (options?.offloadInlineData !== true || typeof item.image_url !== "string" || !item.image_url.trim().toLowerCase().startsWith("data:")) {
|
|
5236
|
+
return item;
|
|
5237
|
+
}
|
|
5004
5238
|
const parsed = parseDataUrlPayload(item.image_url);
|
|
5005
5239
|
if (!parsed) {
|
|
5006
5240
|
return item;
|
|
@@ -5013,16 +5247,27 @@ async function prepareOpenAiPromptContentItem(item, options) {
|
|
|
5013
5247
|
guessInlineDataFilename(parsed.mimeType)
|
|
5014
5248
|
)
|
|
5015
5249
|
});
|
|
5250
|
+
const signedUrl = await getCanonicalFileSignedUrl({ fileId: uploaded.fileId });
|
|
5016
5251
|
return {
|
|
5017
5252
|
type: "input_image",
|
|
5253
|
+
image_url: signedUrl,
|
|
5018
5254
|
detail: toOpenAiImageDetail(
|
|
5019
5255
|
isLlmMediaResolution(item.detail) ? item.detail : void 0,
|
|
5020
5256
|
options?.model
|
|
5021
|
-
)
|
|
5022
|
-
file_id: uploaded.fileId
|
|
5257
|
+
)
|
|
5023
5258
|
};
|
|
5024
5259
|
}
|
|
5025
|
-
if (item.type !== "input_file"
|
|
5260
|
+
if (item.type !== "input_file") {
|
|
5261
|
+
return item;
|
|
5262
|
+
}
|
|
5263
|
+
if (isCanonicalLlmFileId(item.file_id)) {
|
|
5264
|
+
const signedUrl = await getCanonicalFileSignedUrl({ fileId: item.file_id });
|
|
5265
|
+
return {
|
|
5266
|
+
type: "input_file",
|
|
5267
|
+
file_url: signedUrl
|
|
5268
|
+
};
|
|
5269
|
+
}
|
|
5270
|
+
if (options?.offloadInlineData !== true) {
|
|
5026
5271
|
return item;
|
|
5027
5272
|
}
|
|
5028
5273
|
if (typeof item.file_data === "string" && item.file_data.trim().length > 0) {
|
|
@@ -5036,7 +5281,11 @@ async function prepareOpenAiPromptContentItem(item, options) {
|
|
|
5036
5281
|
mimeType,
|
|
5037
5282
|
filename
|
|
5038
5283
|
});
|
|
5039
|
-
|
|
5284
|
+
const signedUrl = await getCanonicalFileSignedUrl({ fileId: uploaded.fileId });
|
|
5285
|
+
return {
|
|
5286
|
+
type: "input_file",
|
|
5287
|
+
file_url: signedUrl
|
|
5288
|
+
};
|
|
5040
5289
|
}
|
|
5041
5290
|
if (typeof item.file_url === "string" && item.file_url.trim().toLowerCase().startsWith("data:")) {
|
|
5042
5291
|
const parsed = parseDataUrlPayload(item.file_url);
|
|
@@ -5051,7 +5300,11 @@ async function prepareOpenAiPromptContentItem(item, options) {
|
|
|
5051
5300
|
guessInlineDataFilename(parsed.mimeType)
|
|
5052
5301
|
)
|
|
5053
5302
|
});
|
|
5054
|
-
|
|
5303
|
+
const signedUrl = await getCanonicalFileSignedUrl({ fileId: uploaded.fileId });
|
|
5304
|
+
return {
|
|
5305
|
+
type: "input_file",
|
|
5306
|
+
file_url: signedUrl
|
|
5307
|
+
};
|
|
5055
5308
|
}
|
|
5056
5309
|
return item;
|
|
5057
5310
|
}
|
|
@@ -5081,11 +5334,40 @@ async function prepareOpenAiPromptInput(input, options) {
|
|
|
5081
5334
|
};
|
|
5082
5335
|
return await Promise.all(input.map((item) => prepareItem(item)));
|
|
5083
5336
|
}
|
|
5337
|
+
function hasCanonicalOpenAiFileReferences(input) {
|
|
5338
|
+
let found = false;
|
|
5339
|
+
const visitItems = (items) => {
|
|
5340
|
+
for (const item of items) {
|
|
5341
|
+
if (found || !item || typeof item !== "object") {
|
|
5342
|
+
continue;
|
|
5343
|
+
}
|
|
5344
|
+
if (Array.isArray(item.content)) {
|
|
5345
|
+
visitItems(item.content);
|
|
5346
|
+
}
|
|
5347
|
+
if (Array.isArray(item.output)) {
|
|
5348
|
+
visitItems(item.output);
|
|
5349
|
+
}
|
|
5350
|
+
if (!isOpenAiNativeContentItem(item)) {
|
|
5351
|
+
continue;
|
|
5352
|
+
}
|
|
5353
|
+
if ((item.type === "input_image" || item.type === "input_file") && isCanonicalLlmFileId(item.file_id)) {
|
|
5354
|
+
found = true;
|
|
5355
|
+
return;
|
|
5356
|
+
}
|
|
5357
|
+
}
|
|
5358
|
+
};
|
|
5359
|
+
visitItems(input);
|
|
5360
|
+
return found;
|
|
5361
|
+
}
|
|
5084
5362
|
async function maybePrepareOpenAiPromptInput(input, options) {
|
|
5085
|
-
|
|
5363
|
+
const offloadInlineData = estimateOpenAiInlinePromptBytes(input) > INLINE_ATTACHMENT_PROMPT_THRESHOLD_BYTES;
|
|
5364
|
+
if (!offloadInlineData && !hasCanonicalOpenAiFileReferences(input)) {
|
|
5086
5365
|
return Array.from(input);
|
|
5087
5366
|
}
|
|
5088
|
-
return await prepareOpenAiPromptInput(input,
|
|
5367
|
+
return await prepareOpenAiPromptInput(input, {
|
|
5368
|
+
...options,
|
|
5369
|
+
offloadInlineData
|
|
5370
|
+
});
|
|
5089
5371
|
}
|
|
5090
5372
|
function estimateGeminiInlinePromptBytes(contents) {
|
|
5091
5373
|
let total = 0;
|
|
@@ -6387,9 +6669,6 @@ async function maybeSpillToolOutputItem(item, toolName, options) {
|
|
|
6387
6669
|
return item;
|
|
6388
6670
|
}
|
|
6389
6671
|
async function maybeSpillToolOutput(value, toolName, options) {
|
|
6390
|
-
if (options?.provider === "chatgpt") {
|
|
6391
|
-
return value;
|
|
6392
|
-
}
|
|
6393
6672
|
if (typeof value === "string") {
|
|
6394
6673
|
if (options?.force !== true && import_node_buffer4.Buffer.byteLength(value, "utf8") <= TOOL_OUTPUT_SPILL_THRESHOLD_BYTES) {
|
|
6395
6674
|
return value;
|
|
@@ -7472,7 +7751,7 @@ async function runTextCall(params) {
|
|
|
7472
7751
|
defaultMediaResolution: request.mediaResolution,
|
|
7473
7752
|
model: request.model
|
|
7474
7753
|
}),
|
|
7475
|
-
{ model: request.model }
|
|
7754
|
+
{ model: request.model, provider: "openai" }
|
|
7476
7755
|
);
|
|
7477
7756
|
const openAiTools = toOpenAiTools(request.tools);
|
|
7478
7757
|
const reasoningEffort = resolveOpenAiReasoningEffort(
|
|
@@ -7551,6 +7830,10 @@ async function runTextCall(params) {
|
|
|
7551
7830
|
defaultMediaResolution: request.mediaResolution,
|
|
7552
7831
|
model: request.model
|
|
7553
7832
|
});
|
|
7833
|
+
const preparedChatGptInput = await maybePrepareOpenAiPromptInput(chatGptInput.input, {
|
|
7834
|
+
model: request.model,
|
|
7835
|
+
provider: "chatgpt"
|
|
7836
|
+
});
|
|
7554
7837
|
const reasoningEffort = resolveOpenAiReasoningEffort(request.model, request.thinkingLevel);
|
|
7555
7838
|
const openAiTools = toOpenAiTools(request.tools);
|
|
7556
7839
|
const requestPayload = {
|
|
@@ -7559,7 +7842,7 @@ async function runTextCall(params) {
|
|
|
7559
7842
|
stream: true,
|
|
7560
7843
|
...providerInfo.serviceTier ? { service_tier: providerInfo.serviceTier } : {},
|
|
7561
7844
|
instructions: chatGptInput.instructions ?? "You are a helpful assistant.",
|
|
7562
|
-
input:
|
|
7845
|
+
input: preparedChatGptInput,
|
|
7563
7846
|
include: ["reasoning.encrypted_content"],
|
|
7564
7847
|
reasoning: {
|
|
7565
7848
|
effort: toOpenAiReasoningEffort(reasoningEffort),
|
|
@@ -8367,7 +8650,8 @@ async function runToolLoop(request) {
|
|
|
8367
8650
|
let stepToolCallText;
|
|
8368
8651
|
let stepToolCallPayload;
|
|
8369
8652
|
const preparedInput = await maybePrepareOpenAiPromptInput(input, {
|
|
8370
|
-
model: request.model
|
|
8653
|
+
model: request.model,
|
|
8654
|
+
provider: "openai"
|
|
8371
8655
|
});
|
|
8372
8656
|
const stepRequestPayload = {
|
|
8373
8657
|
model: providerInfo.model,
|
|
@@ -8737,6 +9021,10 @@ async function runToolLoop(request) {
|
|
|
8737
9021
|
let reasoningSummaryText = "";
|
|
8738
9022
|
let stepToolCallText;
|
|
8739
9023
|
let stepToolCallPayload;
|
|
9024
|
+
const preparedInput = await maybePrepareOpenAiPromptInput(input, {
|
|
9025
|
+
model: request.model,
|
|
9026
|
+
provider: "chatgpt"
|
|
9027
|
+
});
|
|
8740
9028
|
const markFirstModelEvent = () => {
|
|
8741
9029
|
if (firstModelEventAtMs === void 0) {
|
|
8742
9030
|
firstModelEventAtMs = Date.now();
|
|
@@ -8748,7 +9036,7 @@ async function runToolLoop(request) {
|
|
|
8748
9036
|
stream: true,
|
|
8749
9037
|
...providerInfo.serviceTier ? { service_tier: providerInfo.serviceTier } : {},
|
|
8750
9038
|
instructions: toolLoopInput.instructions ?? "You are a helpful assistant.",
|
|
8751
|
-
input,
|
|
9039
|
+
input: preparedInput,
|
|
8752
9040
|
prompt_cache_key: promptCacheKey,
|
|
8753
9041
|
include: ["reasoning.encrypted_content"],
|
|
8754
9042
|
tools: openAiTools,
|