@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/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 (modelId.includes("gpt-5.4-fast")) {
350
+ if (OPENAI_GPT_54_FAST_MODEL_IDS.includes(modelId)) {
343
351
  return OPENAI_GPT_54_PRIORITY_PRICING;
344
352
  }
345
- if (modelId.includes("gpt-5.4-mini")) {
353
+ if (OPENAI_GPT_54_MINI_MODEL_IDS.includes(modelId)) {
346
354
  return OPENAI_GPT_54_MINI_PRICING;
347
355
  }
348
- if (modelId.includes("gpt-5.4-nano")) {
356
+ if (OPENAI_GPT_54_NANO_MODEL_IDS.includes(modelId)) {
349
357
  return OPENAI_GPT_54_NANO_PRICING;
350
358
  }
351
- if (modelId.includes("gpt-5.3-codex-spark")) {
359
+ if (OPENAI_GPT_53_CODEX_SPARK_MODEL_IDS.includes(modelId)) {
352
360
  return OPENAI_GPT_54_MINI_PRICING;
353
361
  }
354
- if (modelId.includes("gpt-5.4")) {
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
- openAiUploadCacheByKey: /* @__PURE__ */ new Map(),
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 === "openai" ? "files_api" : "provider_mirror");
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 toStoredFile(file) {
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
- id: file.id,
3507
- bytes: file.bytes,
3508
- created_at: file.created_at,
3509
- filename: file.filename,
3510
- object: "file",
3511
- purpose: file.purpose,
3512
- status: file.status,
3513
- expires_at: file.expires_at
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.openAiUploadCacheByKey.set(
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 uploadOpenAiFileFromBytes(params) {
3622
- const cacheKey = buildCacheKey(params.filename, params.mimeType, params.sha256Hex);
3623
- const cached = filesState.openAiUploadCacheByKey.get(cacheKey);
3624
- if (cached && isFresh(cached.file)) {
3625
- return cached;
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
- const client = getOpenAiClient();
3628
- const startedAtMs = Date.now();
3629
- let uploaded;
3630
- let mode;
3631
- if (params.bytes.byteLength <= OPENAI_FILE_CREATE_MAX_BYTES) {
3632
- mode = "files.create";
3633
- uploaded = await client.files.create({
3634
- file: new import_node_buffer3.File([new Uint8Array(params.bytes)], params.filename, {
3635
- type: params.mimeType
3636
- }),
3637
- purpose: params.purpose,
3638
- expires_after: {
3639
- anchor: "created_at",
3640
- seconds: params.expiresAfterSeconds
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
- } else {
3644
- mode = "uploads";
3645
- const upload = await client.uploads.create({
3646
- bytes: params.bytes.byteLength,
3647
- filename: params.filename,
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
- uploaded = await client.files.retrieve(fileId);
3784
+ throw error;
3670
3785
  }
3671
- const file = toStoredFile(uploaded);
3672
- const metadata = recordMetadata({
3673
- file,
3674
- filename: file.filename,
3675
- bytes: file.bytes,
3676
- mimeType: params.mimeType,
3677
- sha256Hex: params.sha256Hex
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
- recordUploadEvent({
3680
- backend: "openai",
3681
- mode,
3682
- filename: metadata.filename,
3683
- bytes: metadata.bytes,
3684
- durationMs: Math.max(0, Date.now() - startedAtMs),
3685
- mimeType: params.mimeType,
3686
- fileId: metadata.file.id
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 uploadOpenAiFileFromPath(params) {
3824
+ async function uploadCanonicalFileFromBytes(params) {
3691
3825
  const cacheKey = buildCacheKey(params.filename, params.mimeType, params.sha256Hex);
3692
- const cached = filesState.openAiUploadCacheByKey.get(cacheKey);
3826
+ const cached = filesState.canonicalUploadCacheByKey.get(cacheKey);
3693
3827
  if (cached && isFresh(cached.file)) {
3694
3828
  return cached;
3695
3829
  }
3696
- const client = getOpenAiClient();
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
- let uploaded;
3699
- let mode;
3700
- if (params.bytes <= OPENAI_FILE_CREATE_MAX_BYTES) {
3701
- mode = "files.create";
3702
- const blob = await (0, import_node_fs3.openAsBlob)(params.filePath, { type: params.mimeType });
3703
- uploaded = await client.files.create({
3704
- file: new import_node_buffer3.File([blob], params.filename, { type: params.mimeType }),
3705
- purpose: params.purpose,
3706
- expires_after: {
3707
- anchor: "created_at",
3708
- seconds: params.expiresAfterSeconds
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
- } else {
3712
- mode = "uploads";
3713
- const upload = await client.uploads.create({
3714
- bytes: params.bytes,
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
- mime_type: params.mimeType,
3717
- purpose: params.purpose
3718
- });
3719
- const partIds = [];
3720
- const stream = (0, import_node_fs3.createReadStream)(params.filePath, {
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
- const file = toStoredFile(uploaded);
3746
- const metadata = recordMetadata({
3747
- file,
3748
- filename: file.filename,
3749
- bytes: file.bytes,
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
- sha256Hex: params.sha256Hex
3910
+ metadata: metadataFields
3752
3911
  });
3753
- recordUploadEvent({
3754
- backend: "openai",
3755
- mode,
3756
- filename: metadata.filename,
3757
- bytes: metadata.bytes,
3758
- durationMs: Math.max(0, Date.now() - startedAtMs),
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
- fileId: metadata.file.id
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
- return metadata;
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 retrieveOpenAiFile(fileId) {
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 client = getOpenAiClient();
3774
- const retrieved = await client.files.retrieve(fileId);
3775
- const file = toStoredFile(retrieved);
3776
- const metadata = recordMetadata({
3777
- file,
3778
- filename: file.filename,
3779
- bytes: file.bytes,
3780
- mimeType: cached?.mimeType ?? persisted?.mimeType ?? resolveMimeType(file.filename, void 0),
3781
- sha256Hex: cached?.sha256Hex ?? persisted?.sha256Hex,
3782
- localPath: cached?.localPath ?? persisted?.localPath
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 OpenAI-backed file ids with Vertex Gemini models."
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 materializeOpenAiFile(fileId) {
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 retrieveOpenAiFile(fileId);
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
- await (0, import_promises2.mkdir)(FILES_TEMP_ROOT, { recursive: true });
3859
- const tempDir = await (0, import_promises2.mkdtemp)(
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 sha256Hex = hash.digest("hex");
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: bytes || metadata.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 materializeOpenAiFile(fileId);
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
- openAiFileId: fileId,
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
- openAiFileId: fileId,
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 materializeOpenAiFile(fileId);
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
- openAiFileId: fileId,
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 uploadOpenAiFileFromPath({
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
- const localPath2 = await cacheFileLocally(filePath, sha256Hex2);
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 uploadOpenAiFileFromBytes({
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
- const localPath = await cacheBufferLocally(bytes, sha256Hex);
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 retrieveOpenAiFile(fileId)).file;
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
- const response = await getOpenAiClient().files.delete(fileId);
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: response.id,
4126
- deleted: response.deleted,
4311
+ id: fileId,
4312
+ deleted: true,
4127
4313
  object: "file"
4128
4314
  };
4129
4315
  }
4130
4316
  async function filesContent(fileId) {
4131
- return await getOpenAiClient().files.content(fileId);
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 retrieveOpenAiFile(fileId);
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" && typeof item.image_url === "string" && item.image_url.trim().toLowerCase().startsWith("data:")) {
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" || item.file_id) {
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
- return { type: "input_file", file_id: uploaded.fileId };
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
- return { type: "input_file", file_id: uploaded.fileId };
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
- if (estimateOpenAiInlinePromptBytes(input) <= INLINE_ATTACHMENT_PROMPT_THRESHOLD_BYTES) {
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, options);
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: chatGptInput.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,