@ljoukov/llm 5.0.4 → 7.0.0

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,11 +318,6 @@ function getGeminiImagePricing(modelId) {
318
318
  }
319
319
 
320
320
  // src/openai/pricing.ts
321
- var OPENAI_GPT_52_PRICING = {
322
- inputRate: 1.75 / 1e6,
323
- cachedRate: 0.175 / 1e6,
324
- outputRate: 14 / 1e6
325
- };
326
321
  var OPENAI_GPT_54_PRICING = {
327
322
  inputRate: 2.5 / 1e6,
328
323
  cachedRate: 0.25 / 1e6,
@@ -333,37 +328,31 @@ var OPENAI_GPT_54_PRIORITY_PRICING = {
333
328
  cachedRate: 0.5 / 1e6,
334
329
  outputRate: 30 / 1e6
335
330
  };
336
- var OPENAI_GPT_53_CODEX_PRICING = {
337
- inputRate: 1.25 / 1e6,
338
- cachedRate: 0.125 / 1e6,
339
- outputRate: 10 / 1e6
340
- };
341
- var OPENAI_GPT_5_MINI_PRICING = {
331
+ var OPENAI_GPT_54_MINI_PRICING = {
342
332
  inputRate: 0.25 / 1e6,
343
333
  cachedRate: 0.025 / 1e6,
344
334
  outputRate: 2 / 1e6
345
335
  };
336
+ var OPENAI_GPT_54_NANO_PRICING = {
337
+ inputRate: 0.05 / 1e6,
338
+ cachedRate: 5e-3 / 1e6,
339
+ outputRate: 0.4 / 1e6
340
+ };
346
341
  function getOpenAiPricing(modelId) {
347
342
  if (modelId.includes("gpt-5.4-fast")) {
348
343
  return OPENAI_GPT_54_PRIORITY_PRICING;
349
344
  }
350
- if (modelId.includes("gpt-5.4")) {
351
- return OPENAI_GPT_54_PRICING;
352
- }
353
- if (modelId.includes("gpt-5.3-codex-spark")) {
354
- return OPENAI_GPT_5_MINI_PRICING;
355
- }
356
- if (modelId.includes("gpt-5.3-codex")) {
357
- return OPENAI_GPT_53_CODEX_PRICING;
345
+ if (modelId.includes("gpt-5.4-mini")) {
346
+ return OPENAI_GPT_54_MINI_PRICING;
358
347
  }
359
- if (modelId.includes("gpt-5.2")) {
360
- return OPENAI_GPT_52_PRICING;
348
+ if (modelId.includes("gpt-5.4-nano")) {
349
+ return OPENAI_GPT_54_NANO_PRICING;
361
350
  }
362
- if (modelId.includes("gpt-5-mini")) {
363
- return OPENAI_GPT_5_MINI_PRICING;
351
+ if (modelId.includes("gpt-5.3-codex-spark")) {
352
+ return OPENAI_GPT_54_MINI_PRICING;
364
353
  }
365
- if (modelId.includes("gpt-5.1-codex-mini")) {
366
- return OPENAI_GPT_5_MINI_PRICING;
354
+ if (modelId.includes("gpt-5.4")) {
355
+ return OPENAI_GPT_54_PRICING;
367
356
  }
368
357
  return void 0;
369
358
  }
@@ -2832,22 +2821,15 @@ async function runOpenAiCall(fn, modelId, runOptions) {
2832
2821
  }
2833
2822
 
2834
2823
  // src/openai/models.ts
2835
- var OPENAI_MODEL_IDS = [
2836
- "gpt-5.4",
2837
- "gpt-5.3-codex",
2838
- "gpt-5.2",
2839
- "gpt-5.1-codex-mini"
2840
- ];
2824
+ var OPENAI_MODEL_IDS = ["gpt-5.4", "gpt-5.4-mini", "gpt-5.4-nano"];
2841
2825
  function isOpenAiModelId(value) {
2842
2826
  return OPENAI_MODEL_IDS.includes(value);
2843
2827
  }
2844
2828
  var CHATGPT_MODEL_IDS = [
2845
2829
  "chatgpt-gpt-5.4",
2846
2830
  "chatgpt-gpt-5.4-fast",
2847
- "chatgpt-gpt-5.3-codex",
2848
- "chatgpt-gpt-5.3-codex-spark",
2849
- "chatgpt-gpt-5.2",
2850
- "chatgpt-gpt-5.1-codex-mini"
2831
+ "chatgpt-gpt-5.4-mini",
2832
+ "chatgpt-gpt-5.3-codex-spark"
2851
2833
  ];
2852
2834
  function isChatGptModelId(value) {
2853
2835
  return CHATGPT_MODEL_IDS.includes(value);
@@ -3376,13 +3358,10 @@ var import_node_fs3 = require("fs");
3376
3358
  var import_promises2 = require("fs/promises");
3377
3359
  var import_node_os3 = __toESM(require("os"), 1);
3378
3360
  var import_node_path4 = __toESM(require("path"), 1);
3379
- var import_node_stream = require("stream");
3380
3361
  var import_promises3 = require("stream/promises");
3381
3362
  var import_storage = require("@google-cloud/storage");
3382
3363
  var import_mime = __toESM(require("mime"), 1);
3383
3364
  var DEFAULT_FILE_TTL_SECONDS = 48 * 60 * 60;
3384
- var OPENAI_FILE_CREATE_MAX_BYTES = 512 * 1024 * 1024;
3385
- var OPENAI_UPLOAD_PART_MAX_BYTES = 64 * 1024 * 1024;
3386
3365
  var GEMINI_FILE_POLL_INTERVAL_MS = 1e3;
3387
3366
  var GEMINI_FILE_POLL_TIMEOUT_MS = 6e4;
3388
3367
  var FILES_TEMP_ROOT = import_node_path4.default.join(import_node_os3.default.tmpdir(), "ljoukov-llm-files");
@@ -3391,7 +3370,7 @@ var FILES_CACHE_CONTENT_ROOT = import_node_path4.default.join(FILES_CACHE_ROOT,
3391
3370
  var FILES_CACHE_METADATA_ROOT = import_node_path4.default.join(FILES_CACHE_ROOT, "metadata");
3392
3371
  var filesState = getRuntimeSingleton(/* @__PURE__ */ Symbol.for("@ljoukov/llm.filesState"), () => ({
3393
3372
  metadataById: /* @__PURE__ */ new Map(),
3394
- openAiUploadCacheByKey: /* @__PURE__ */ new Map(),
3373
+ canonicalUploadCacheByKey: /* @__PURE__ */ new Map(),
3395
3374
  materializedById: /* @__PURE__ */ new Map(),
3396
3375
  geminiMirrorById: /* @__PURE__ */ new Map(),
3397
3376
  vertexMirrorById: /* @__PURE__ */ new Map(),
@@ -3472,7 +3451,7 @@ function formatUploadLogLine(event) {
3472
3451
  }
3473
3452
  function recordUploadEvent(event) {
3474
3453
  const scope = fileUploadScopeStorage.getStore();
3475
- const resolvedSource = event.source ?? scope?.source ?? (event.backend === "openai" ? "files_api" : "provider_mirror");
3454
+ const resolvedSource = event.source ?? scope?.source ?? (event.backend === "gcs" ? "files_api" : "provider_mirror");
3476
3455
  const timestampedEvent = {
3477
3456
  ...event,
3478
3457
  source: resolvedSource,
@@ -3519,16 +3498,117 @@ async function computeFileSha256Hex(filePath) {
3519
3498
  }
3520
3499
  return hash.digest("hex");
3521
3500
  }
3522
- function toStoredFile(file) {
3501
+ function buildCanonicalFileId(filename, mimeType, sha256Hex) {
3502
+ return `file_${(0, import_node_crypto.createHash)("sha256").update(filename).update("\0").update(mimeType).update("\0").update(sha256Hex).digest("hex")}`;
3503
+ }
3504
+ function resolveCanonicalFilesBucket() {
3505
+ const raw = process.env.LLM_FILES_GCS_BUCKET ?? process.env.VERTEX_GCS_BUCKET ?? process.env.LLM_VERTEX_GCS_BUCKET;
3506
+ const trimmed = raw?.trim();
3507
+ if (!trimmed) {
3508
+ throw new Error(
3509
+ "LLM_FILES_GCS_BUCKET (or VERTEX_GCS_BUCKET) must be set to use the canonical files API."
3510
+ );
3511
+ }
3512
+ return trimmed.replace(/^gs:\/\//u, "").replace(/\/+$/u, "");
3513
+ }
3514
+ function resolveCanonicalFilesPrefix() {
3515
+ const raw = process.env.LLM_FILES_GCS_PREFIX;
3516
+ const trimmed = raw?.trim().replace(/^\/+/u, "").replace(/\/+$/u, "");
3517
+ return trimmed ? `${trimmed}/` : "canonical-files/";
3518
+ }
3519
+ function isLatexLikeFile(filename, mimeType) {
3520
+ const extension = import_node_path4.default.extname(filename).trim().toLowerCase();
3521
+ const normalisedMimeType = mimeType.trim().toLowerCase();
3522
+ return extension === ".tex" || extension === ".ltx" || extension === ".latex" || normalisedMimeType === "application/x-tex" || normalisedMimeType === "text/x-tex";
3523
+ }
3524
+ function resolveCanonicalStorageContentType(filename, mimeType) {
3525
+ if (isLatexLikeFile(filename, mimeType)) {
3526
+ return "text/plain";
3527
+ }
3528
+ return mimeType;
3529
+ }
3530
+ function resolveCanonicalObjectExtension(filename, mimeType) {
3531
+ if (isLatexLikeFile(filename, mimeType)) {
3532
+ return "txt";
3533
+ }
3534
+ const fromFilename = import_node_path4.default.extname(filename).replace(/^\./u, "").trim().toLowerCase();
3535
+ if (fromFilename) {
3536
+ return fromFilename;
3537
+ }
3538
+ const fromMimeType = import_mime.default.getExtension(mimeType)?.trim().toLowerCase();
3539
+ if (fromMimeType) {
3540
+ return fromMimeType;
3541
+ }
3542
+ return "bin";
3543
+ }
3544
+ function buildCanonicalObjectName(fileId, filename, mimeType) {
3545
+ const extension = resolveCanonicalObjectExtension(filename, mimeType);
3546
+ return `${resolveCanonicalFilesPrefix()}${fileId}.${extension}`;
3547
+ }
3548
+ function toSafeStorageFilename(filename) {
3549
+ const normalized = normaliseFilename(filename).replace(/[^\w.-]+/gu, "-");
3550
+ return normalized.length > 0 ? normalized : "attachment.bin";
3551
+ }
3552
+ function parseUnixSeconds(value, fallback) {
3553
+ if (value) {
3554
+ const numeric = Number.parseInt(value, 10);
3555
+ if (Number.isFinite(numeric) && numeric > 0) {
3556
+ return numeric;
3557
+ }
3558
+ }
3559
+ if (fallback) {
3560
+ const millis = Date.parse(fallback);
3561
+ if (Number.isFinite(millis)) {
3562
+ return Math.floor(millis / 1e3);
3563
+ }
3564
+ }
3565
+ return Math.floor(Date.now() / 1e3);
3566
+ }
3567
+ function parseOptionalUnixSeconds(value) {
3568
+ if (!value) {
3569
+ return void 0;
3570
+ }
3571
+ const millis = Date.parse(value);
3572
+ if (Number.isFinite(millis)) {
3573
+ return Math.floor(millis / 1e3);
3574
+ }
3575
+ const numeric = Number.parseInt(value, 10);
3576
+ return Number.isFinite(numeric) && numeric > 0 ? numeric : void 0;
3577
+ }
3578
+ function toStoredFileFromCanonicalMetadata(options) {
3579
+ const metadata = options.objectMetadata.metadata;
3580
+ const filenameRaw = typeof metadata?.filename === "string" && metadata.filename.trim().length > 0 ? metadata.filename.trim() : import_node_path4.default.basename(options.objectName);
3581
+ const filename = normaliseFilename(filenameRaw);
3582
+ const bytesRaw = options.objectMetadata.size;
3583
+ const bytes = typeof bytesRaw === "string" ? Number.parseInt(bytesRaw, 10) : typeof bytesRaw === "number" ? bytesRaw : 0;
3584
+ const purpose = metadata?.purpose === "user_data" ? "user_data" : "user_data";
3585
+ const createdAt = parseUnixSeconds(
3586
+ typeof metadata?.createdAtUnix === "string" ? metadata.createdAtUnix : void 0,
3587
+ typeof options.objectMetadata.timeCreated === "string" ? options.objectMetadata.timeCreated : void 0
3588
+ );
3589
+ const expiresAt = parseOptionalUnixSeconds(
3590
+ typeof metadata?.expiresAt === "string" ? metadata.expiresAt : void 0
3591
+ );
3592
+ 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);
3593
+ const sha256Hex = typeof metadata?.sha256 === "string" && metadata.sha256.trim().length > 0 ? metadata.sha256.trim() : void 0;
3523
3594
  return {
3524
- id: file.id,
3525
- bytes: file.bytes,
3526
- created_at: file.created_at,
3527
- filename: file.filename,
3528
- object: "file",
3529
- purpose: file.purpose,
3530
- status: file.status,
3531
- expires_at: file.expires_at
3595
+ file: {
3596
+ id: options.fileId,
3597
+ bytes: Number.isFinite(bytes) ? bytes : 0,
3598
+ created_at: createdAt,
3599
+ filename,
3600
+ object: "file",
3601
+ purpose,
3602
+ status: "processed",
3603
+ ...expiresAt ? { expires_at: expiresAt } : {}
3604
+ },
3605
+ filename,
3606
+ bytes: Number.isFinite(bytes) ? bytes : 0,
3607
+ mimeType,
3608
+ sha256Hex,
3609
+ localPath: options.localPath,
3610
+ bucketName: options.bucketName,
3611
+ objectName: options.objectName
3532
3612
  };
3533
3613
  }
3534
3614
  function buildCacheKey(filename, mimeType, sha256Hex) {
@@ -3549,7 +3629,7 @@ function isFresh(file) {
3549
3629
  function recordMetadata(metadata) {
3550
3630
  filesState.metadataById.set(metadata.file.id, metadata);
3551
3631
  if (metadata.sha256Hex) {
3552
- filesState.openAiUploadCacheByKey.set(
3632
+ filesState.canonicalUploadCacheByKey.set(
3553
3633
  buildCacheKey(
3554
3634
  metadata.filename,
3555
3635
  metadata.mimeType ?? "application/octet-stream",
@@ -3598,7 +3678,9 @@ async function persistMetadataToDisk(metadata) {
3598
3678
  bytes: metadata.bytes,
3599
3679
  mimeType: metadata.mimeType,
3600
3680
  sha256Hex: metadata.sha256Hex,
3601
- localPath: metadata.localPath
3681
+ localPath: metadata.localPath,
3682
+ bucketName: metadata.bucketName,
3683
+ objectName: metadata.objectName
3602
3684
  };
3603
3685
  await (0, import_promises2.writeFile)(
3604
3686
  buildCachedMetadataPath(metadata.file.id),
@@ -3630,175 +3712,271 @@ async function loadPersistedMetadata(fileId) {
3630
3712
  bytes: payload.bytes,
3631
3713
  mimeType: payload.mimeType,
3632
3714
  sha256Hex: payload.sha256Hex,
3633
- localPath: payload.localPath
3715
+ localPath: payload.localPath,
3716
+ bucketName: payload.bucketName,
3717
+ objectName: payload.objectName
3634
3718
  });
3635
3719
  } catch {
3636
3720
  return void 0;
3637
3721
  }
3638
3722
  }
3639
- async function uploadOpenAiFileFromBytes(params) {
3640
- const cacheKey = buildCacheKey(params.filename, params.mimeType, params.sha256Hex);
3641
- const cached = filesState.openAiUploadCacheByKey.get(cacheKey);
3642
- if (cached && isFresh(cached.file)) {
3643
- return cached;
3723
+ async function writeCanonicalFileFromPath(options) {
3724
+ const file = getStorageClient().bucket(options.bucketName).file(options.objectName);
3725
+ const storageContentType = resolveCanonicalStorageContentType(
3726
+ options.metadata.filename ?? "attachment.bin",
3727
+ options.mimeType
3728
+ );
3729
+ try {
3730
+ await (0, import_promises3.pipeline)(
3731
+ (0, import_node_fs3.createReadStream)(options.filePath),
3732
+ file.createWriteStream({
3733
+ resumable: options.bytes >= 10 * 1024 * 1024,
3734
+ preconditionOpts: { ifGenerationMatch: 0 },
3735
+ metadata: {
3736
+ contentType: storageContentType,
3737
+ contentDisposition: `inline; filename="${toSafeStorageFilename(options.metadata.filename ?? "attachment.bin")}"`,
3738
+ metadata: options.metadata
3739
+ }
3740
+ })
3741
+ );
3742
+ return true;
3743
+ } catch (error) {
3744
+ const code = error.code;
3745
+ if (code === 412 || code === "412") {
3746
+ return false;
3747
+ }
3748
+ throw error;
3644
3749
  }
3645
- const client = getOpenAiClient();
3646
- const startedAtMs = Date.now();
3647
- let uploaded;
3648
- let mode;
3649
- if (params.bytes.byteLength <= OPENAI_FILE_CREATE_MAX_BYTES) {
3650
- mode = "files.create";
3651
- uploaded = await client.files.create({
3652
- file: new import_node_buffer3.File([new Uint8Array(params.bytes)], params.filename, {
3653
- type: params.mimeType
3654
- }),
3655
- purpose: params.purpose,
3656
- expires_after: {
3657
- anchor: "created_at",
3658
- seconds: params.expiresAfterSeconds
3750
+ }
3751
+ async function writeCanonicalFileFromBytes(options) {
3752
+ const file = getStorageClient().bucket(options.bucketName).file(options.objectName);
3753
+ const storageContentType = resolveCanonicalStorageContentType(
3754
+ options.metadata.filename ?? "attachment.bin",
3755
+ options.mimeType
3756
+ );
3757
+ try {
3758
+ await file.save(options.bytes, {
3759
+ resumable: options.bytes.byteLength >= 10 * 1024 * 1024,
3760
+ preconditionOpts: { ifGenerationMatch: 0 },
3761
+ metadata: {
3762
+ contentType: storageContentType,
3763
+ contentDisposition: `inline; filename="${toSafeStorageFilename(options.metadata.filename ?? "attachment.bin")}"`,
3764
+ metadata: options.metadata
3659
3765
  }
3660
3766
  });
3661
- } else {
3662
- mode = "uploads";
3663
- const upload = await client.uploads.create({
3664
- bytes: params.bytes.byteLength,
3665
- filename: params.filename,
3666
- mime_type: params.mimeType,
3667
- purpose: params.purpose
3668
- });
3669
- const partIds = [];
3670
- for (let offset = 0; offset < params.bytes.byteLength; offset += OPENAI_UPLOAD_PART_MAX_BYTES) {
3671
- const chunk = params.bytes.subarray(
3672
- offset,
3673
- Math.min(offset + OPENAI_UPLOAD_PART_MAX_BYTES, params.bytes.byteLength)
3674
- );
3675
- const uploadPart = await client.uploads.parts.create(upload.id, {
3676
- data: new import_node_buffer3.File([new Uint8Array(chunk)], `${params.sha256Hex}.part`, {
3677
- type: params.mimeType
3678
- })
3679
- });
3680
- partIds.push(uploadPart.id);
3681
- }
3682
- const completed = await client.uploads.complete(upload.id, { part_ids: partIds });
3683
- const fileId = completed.file?.id;
3684
- if (!fileId) {
3685
- throw new Error("OpenAI upload completed without a file id.");
3767
+ return true;
3768
+ } catch (error) {
3769
+ const code = error.code;
3770
+ if (code === 412 || code === "412") {
3771
+ return false;
3686
3772
  }
3687
- uploaded = await client.files.retrieve(fileId);
3773
+ throw error;
3688
3774
  }
3689
- const file = toStoredFile(uploaded);
3690
- const metadata = recordMetadata({
3691
- file,
3692
- filename: file.filename,
3693
- bytes: file.bytes,
3694
- mimeType: params.mimeType,
3695
- sha256Hex: params.sha256Hex
3775
+ }
3776
+ async function refreshCanonicalObjectMetadata(options) {
3777
+ const storageContentType = resolveCanonicalStorageContentType(
3778
+ options.metadata.filename ?? "attachment.bin",
3779
+ options.mimeType
3780
+ );
3781
+ await getStorageClient().bucket(options.bucketName).file(options.objectName).setMetadata({
3782
+ contentType: storageContentType,
3783
+ contentDisposition: `inline; filename="${toSafeStorageFilename(options.metadata.filename ?? "attachment.bin")}"`,
3784
+ metadata: options.metadata
3696
3785
  });
3697
- recordUploadEvent({
3698
- backend: "openai",
3699
- mode,
3700
- filename: metadata.filename,
3701
- bytes: metadata.bytes,
3702
- durationMs: Math.max(0, Date.now() - startedAtMs),
3703
- mimeType: params.mimeType,
3704
- fileId: metadata.file.id
3786
+ }
3787
+ async function createCanonicalMetadata(options) {
3788
+ const createdAt = Math.floor(Date.now() / 1e3);
3789
+ const expiresAt = createdAt + options.expiresAfterSeconds;
3790
+ const storedFile = {
3791
+ id: options.fileId,
3792
+ bytes: options.bytes,
3793
+ created_at: createdAt,
3794
+ filename: options.filename,
3795
+ object: "file",
3796
+ purpose: options.purpose,
3797
+ status: "processed",
3798
+ expires_at: expiresAt
3799
+ };
3800
+ const metadata = recordMetadata({
3801
+ file: storedFile,
3802
+ filename: options.filename,
3803
+ bytes: options.bytes,
3804
+ mimeType: options.mimeType,
3805
+ sha256Hex: options.sha256Hex,
3806
+ localPath: options.localPath,
3807
+ bucketName: options.bucketName,
3808
+ objectName: options.objectName
3705
3809
  });
3810
+ await persistMetadataToDisk(metadata);
3706
3811
  return metadata;
3707
3812
  }
3708
- async function uploadOpenAiFileFromPath(params) {
3813
+ async function uploadCanonicalFileFromBytes(params) {
3709
3814
  const cacheKey = buildCacheKey(params.filename, params.mimeType, params.sha256Hex);
3710
- const cached = filesState.openAiUploadCacheByKey.get(cacheKey);
3815
+ const cached = filesState.canonicalUploadCacheByKey.get(cacheKey);
3711
3816
  if (cached && isFresh(cached.file)) {
3712
3817
  return cached;
3713
3818
  }
3714
- const client = getOpenAiClient();
3819
+ const fileId = buildCanonicalFileId(params.filename, params.mimeType, params.sha256Hex);
3820
+ const bucketName = resolveCanonicalFilesBucket();
3821
+ const objectName = buildCanonicalObjectName(fileId, params.filename, params.mimeType);
3822
+ const metadataFields = {
3823
+ fileId,
3824
+ filename: params.filename,
3825
+ mimeType: params.mimeType,
3826
+ purpose: params.purpose,
3827
+ sha256: params.sha256Hex,
3828
+ createdAtUnix: Math.floor(Date.now() / 1e3).toString(),
3829
+ expiresAt: new Date(Date.now() + params.expiresAfterSeconds * 1e3).toISOString()
3830
+ };
3715
3831
  const startedAtMs = Date.now();
3716
- let uploaded;
3717
- let mode;
3718
- if (params.bytes <= OPENAI_FILE_CREATE_MAX_BYTES) {
3719
- mode = "files.create";
3720
- const blob = await (0, import_node_fs3.openAsBlob)(params.filePath, { type: params.mimeType });
3721
- uploaded = await client.files.create({
3722
- file: new import_node_buffer3.File([blob], params.filename, { type: params.mimeType }),
3723
- purpose: params.purpose,
3724
- expires_after: {
3725
- anchor: "created_at",
3726
- seconds: params.expiresAfterSeconds
3727
- }
3832
+ const uploaded = await writeCanonicalFileFromBytes({
3833
+ bytes: params.bytes,
3834
+ bucketName,
3835
+ objectName,
3836
+ mimeType: params.mimeType,
3837
+ metadata: metadataFields
3838
+ });
3839
+ if (!uploaded) {
3840
+ await refreshCanonicalObjectMetadata({
3841
+ bucketName,
3842
+ objectName,
3843
+ mimeType: params.mimeType,
3844
+ metadata: metadataFields
3728
3845
  });
3729
- } else {
3730
- mode = "uploads";
3731
- const upload = await client.uploads.create({
3732
- bytes: params.bytes,
3846
+ }
3847
+ const localPath = await cacheBufferLocally(params.bytes, params.sha256Hex);
3848
+ const canonical = await createCanonicalMetadata({
3849
+ fileId,
3850
+ filename: params.filename,
3851
+ mimeType: params.mimeType,
3852
+ purpose: params.purpose,
3853
+ expiresAfterSeconds: params.expiresAfterSeconds,
3854
+ sha256Hex: params.sha256Hex,
3855
+ bytes: params.bytes.byteLength,
3856
+ bucketName,
3857
+ objectName,
3858
+ localPath
3859
+ });
3860
+ if (uploaded) {
3861
+ recordUploadEvent({
3862
+ backend: "gcs",
3863
+ mode: "gcs",
3733
3864
  filename: params.filename,
3734
- mime_type: params.mimeType,
3735
- purpose: params.purpose
3736
- });
3737
- const partIds = [];
3738
- const stream = (0, import_node_fs3.createReadStream)(params.filePath, {
3739
- highWaterMark: OPENAI_UPLOAD_PART_MAX_BYTES
3865
+ bytes: params.bytes.byteLength,
3866
+ durationMs: Math.max(0, Date.now() - startedAtMs),
3867
+ mimeType: params.mimeType,
3868
+ fileId,
3869
+ fileUri: `gs://${bucketName}/${objectName}`
3740
3870
  });
3741
- let partIndex = 0;
3742
- for await (const chunk of stream) {
3743
- const buffer = import_node_buffer3.Buffer.isBuffer(chunk) ? chunk : import_node_buffer3.Buffer.from(chunk);
3744
- const uploadPart = await client.uploads.parts.create(upload.id, {
3745
- data: new import_node_buffer3.File(
3746
- [new Uint8Array(buffer)],
3747
- `${params.sha256Hex}.${partIndex.toString()}.part`,
3748
- {
3749
- type: params.mimeType
3750
- }
3751
- )
3752
- });
3753
- partIds.push(uploadPart.id);
3754
- partIndex += 1;
3755
- }
3756
- const completed = await client.uploads.complete(upload.id, { part_ids: partIds });
3757
- const fileId = completed.file?.id;
3758
- if (!fileId) {
3759
- throw new Error("OpenAI upload completed without a file id.");
3760
- }
3761
- uploaded = await client.files.retrieve(fileId);
3762
3871
  }
3763
- const file = toStoredFile(uploaded);
3764
- const metadata = recordMetadata({
3765
- file,
3766
- filename: file.filename,
3767
- bytes: file.bytes,
3872
+ return canonical;
3873
+ }
3874
+ async function uploadCanonicalFileFromPath(params) {
3875
+ const cacheKey = buildCacheKey(params.filename, params.mimeType, params.sha256Hex);
3876
+ const cached = filesState.canonicalUploadCacheByKey.get(cacheKey);
3877
+ if (cached && isFresh(cached.file)) {
3878
+ return cached;
3879
+ }
3880
+ const fileId = buildCanonicalFileId(params.filename, params.mimeType, params.sha256Hex);
3881
+ const bucketName = resolveCanonicalFilesBucket();
3882
+ const objectName = buildCanonicalObjectName(fileId, params.filename, params.mimeType);
3883
+ const metadataFields = {
3884
+ fileId,
3885
+ filename: params.filename,
3886
+ mimeType: params.mimeType,
3887
+ purpose: params.purpose,
3888
+ sha256: params.sha256Hex,
3889
+ createdAtUnix: Math.floor(Date.now() / 1e3).toString(),
3890
+ expiresAt: new Date(Date.now() + params.expiresAfterSeconds * 1e3).toISOString()
3891
+ };
3892
+ const startedAtMs = Date.now();
3893
+ const uploaded = await writeCanonicalFileFromPath({
3894
+ filePath: params.filePath,
3895
+ bucketName,
3896
+ objectName,
3897
+ bytes: params.bytes,
3768
3898
  mimeType: params.mimeType,
3769
- sha256Hex: params.sha256Hex
3899
+ metadata: metadataFields
3770
3900
  });
3771
- recordUploadEvent({
3772
- backend: "openai",
3773
- mode,
3774
- filename: metadata.filename,
3775
- bytes: metadata.bytes,
3776
- durationMs: Math.max(0, Date.now() - startedAtMs),
3901
+ if (!uploaded) {
3902
+ await refreshCanonicalObjectMetadata({
3903
+ bucketName,
3904
+ objectName,
3905
+ mimeType: params.mimeType,
3906
+ metadata: metadataFields
3907
+ });
3908
+ }
3909
+ const localPath = await cacheFileLocally(params.filePath, params.sha256Hex);
3910
+ const canonical = await createCanonicalMetadata({
3911
+ fileId,
3912
+ filename: params.filename,
3777
3913
  mimeType: params.mimeType,
3778
- fileId: metadata.file.id
3914
+ purpose: params.purpose,
3915
+ expiresAfterSeconds: params.expiresAfterSeconds,
3916
+ sha256Hex: params.sha256Hex,
3917
+ bytes: params.bytes,
3918
+ bucketName,
3919
+ objectName,
3920
+ localPath
3779
3921
  });
3780
- return metadata;
3922
+ if (uploaded) {
3923
+ recordUploadEvent({
3924
+ backend: "gcs",
3925
+ mode: "gcs",
3926
+ filename: params.filename,
3927
+ bytes: params.bytes,
3928
+ durationMs: Math.max(0, Date.now() - startedAtMs),
3929
+ mimeType: params.mimeType,
3930
+ fileId,
3931
+ fileUri: `gs://${bucketName}/${objectName}`
3932
+ });
3933
+ }
3934
+ return canonical;
3935
+ }
3936
+ async function resolveCanonicalStorageLocation(fileId) {
3937
+ const cached = filesState.metadataById.get(fileId) ?? await loadPersistedMetadata(fileId);
3938
+ if (cached?.bucketName && cached.objectName) {
3939
+ return {
3940
+ bucketName: cached.bucketName,
3941
+ objectName: cached.objectName
3942
+ };
3943
+ }
3944
+ const bucketName = resolveCanonicalFilesBucket();
3945
+ const [files2] = await getStorageClient().bucket(bucketName).getFiles({
3946
+ prefix: `${resolveCanonicalFilesPrefix()}${fileId}.`,
3947
+ maxResults: 1,
3948
+ autoPaginate: false
3949
+ });
3950
+ const file = files2[0];
3951
+ if (!file) {
3952
+ throw new Error(`Canonical file ${fileId} was not found in GCS.`);
3953
+ }
3954
+ return {
3955
+ bucketName,
3956
+ objectName: file.name
3957
+ };
3781
3958
  }
3782
- async function retrieveOpenAiFile(fileId) {
3959
+ async function retrieveCanonicalFile(fileId) {
3783
3960
  const cached = filesState.metadataById.get(fileId);
3784
- if (cached && isFresh(cached.file)) {
3961
+ if (cached && isFresh(cached.file) && cached.bucketName && cached.objectName) {
3785
3962
  return cached;
3786
3963
  }
3787
3964
  const persisted = await loadPersistedMetadata(fileId);
3788
- if (persisted && isFresh(persisted.file)) {
3965
+ if (persisted && isFresh(persisted.file) && persisted.bucketName && persisted.objectName) {
3789
3966
  return persisted;
3790
3967
  }
3791
- const client = getOpenAiClient();
3792
- const retrieved = await client.files.retrieve(fileId);
3793
- const file = toStoredFile(retrieved);
3794
- const metadata = recordMetadata({
3795
- file,
3796
- filename: file.filename,
3797
- bytes: file.bytes,
3798
- mimeType: cached?.mimeType ?? persisted?.mimeType ?? resolveMimeType(file.filename, void 0),
3799
- sha256Hex: cached?.sha256Hex ?? persisted?.sha256Hex,
3800
- localPath: cached?.localPath ?? persisted?.localPath
3801
- });
3968
+ const existingLocalPath = cached?.localPath ?? persisted?.localPath;
3969
+ const { bucketName, objectName } = await resolveCanonicalStorageLocation(fileId);
3970
+ const [objectMetadata] = await getStorageClient().bucket(bucketName).file(objectName).getMetadata();
3971
+ const metadata = recordMetadata(
3972
+ toStoredFileFromCanonicalMetadata({
3973
+ fileId,
3974
+ bucketName,
3975
+ objectName,
3976
+ objectMetadata,
3977
+ localPath: existingLocalPath
3978
+ })
3979
+ );
3802
3980
  await persistMetadataToDisk(metadata);
3803
3981
  return metadata;
3804
3982
  }
@@ -3826,7 +4004,7 @@ function resolveVertexMirrorBucket() {
3826
4004
  const trimmed = raw?.trim();
3827
4005
  if (!trimmed) {
3828
4006
  throw new Error(
3829
- "VERTEX_GCS_BUCKET must be set to use OpenAI-backed file ids with Vertex Gemini models."
4007
+ "VERTEX_GCS_BUCKET must be set to use canonical file ids with Vertex Gemini models."
3830
4008
  );
3831
4009
  }
3832
4010
  return trimmed.replace(/^gs:\/\//u, "").replace(/\/+$/u, "");
@@ -3856,61 +4034,41 @@ function getGeminiMirrorClient() {
3856
4034
  }
3857
4035
  return filesState.geminiClientPromise;
3858
4036
  }
3859
- async function materializeOpenAiFile(fileId) {
4037
+ async function materializeCanonicalFile(fileId) {
3860
4038
  const cachedPromise = filesState.materializedById.get(fileId);
3861
4039
  if (cachedPromise) {
3862
4040
  return await cachedPromise;
3863
4041
  }
3864
4042
  const promise = (async () => {
3865
- const metadata = await retrieveOpenAiFile(fileId);
3866
- if (metadata.localPath && metadata.sha256Hex && metadata.mimeType) {
4043
+ const metadata = await retrieveCanonicalFile(fileId);
4044
+ if (metadata.localPath && metadata.sha256Hex && metadata.mimeType && metadata.bucketName && metadata.objectName) {
3867
4045
  return {
3868
4046
  file: metadata.file,
3869
4047
  filename: metadata.filename,
3870
4048
  bytes: metadata.bytes,
3871
4049
  mimeType: metadata.mimeType,
3872
4050
  sha256Hex: metadata.sha256Hex,
3873
- localPath: metadata.localPath
4051
+ localPath: metadata.localPath,
4052
+ bucketName: metadata.bucketName,
4053
+ objectName: metadata.objectName
3874
4054
  };
3875
4055
  }
3876
- await (0, import_promises2.mkdir)(FILES_TEMP_ROOT, { recursive: true });
3877
- const tempDir = await (0, import_promises2.mkdtemp)(
3878
- import_node_path4.default.join(FILES_TEMP_ROOT, `${fileId.replace(/[^a-z0-9_-]/giu, "")}-`)
3879
- );
3880
- const localPath = import_node_path4.default.join(tempDir, normaliseFilename(metadata.filename, `${fileId}.bin`));
3881
- const response = await getOpenAiClient().files.content(fileId);
3882
- if (!response.ok) {
3883
- throw new Error(
3884
- `Failed to download OpenAI file ${fileId}: ${response.status} ${response.statusText}`
3885
- );
4056
+ if (!metadata.bucketName || !metadata.objectName) {
4057
+ throw new Error(`Canonical file ${fileId} is missing GCS location metadata.`);
3886
4058
  }
3887
- const responseMimeType = response.headers.get("content-type")?.trim() || void 0;
3888
- const mimeType = resolveMimeType(metadata.filename, responseMimeType);
3889
- const hash = (0, import_node_crypto.createHash)("sha256");
3890
- let bytes = 0;
3891
- if (response.body) {
3892
- const source = import_node_stream.Readable.fromWeb(response.body);
3893
- const writable = (0, import_node_fs3.createWriteStream)(localPath, { flags: "wx" });
3894
- source.on("data", (chunk) => {
3895
- const buffer = import_node_buffer3.Buffer.isBuffer(chunk) ? chunk : import_node_buffer3.Buffer.from(chunk);
3896
- hash.update(buffer);
3897
- bytes += buffer.byteLength;
3898
- });
3899
- await (0, import_promises3.pipeline)(source, writable);
3900
- } else {
3901
- const buffer = import_node_buffer3.Buffer.from(await response.arrayBuffer());
3902
- hash.update(buffer);
3903
- bytes = buffer.byteLength;
3904
- await (0, import_promises2.writeFile)(localPath, buffer);
3905
- }
3906
- const sha256Hex = hash.digest("hex");
4059
+ const [downloadedBytes] = await getStorageClient().bucket(metadata.bucketName).file(metadata.objectName).download();
4060
+ const mimeType = metadata.mimeType ?? resolveMimeType(metadata.filename, void 0);
4061
+ const sha256Hex = metadata.sha256Hex ?? computeSha256Hex(downloadedBytes);
4062
+ const localPath = await cacheBufferLocally(downloadedBytes, sha256Hex);
3907
4063
  const updated = recordMetadata({
3908
4064
  file: metadata.file,
3909
4065
  filename: metadata.filename,
3910
- bytes: bytes || metadata.bytes,
4066
+ bytes: downloadedBytes.byteLength || metadata.bytes,
3911
4067
  mimeType,
3912
4068
  sha256Hex,
3913
- localPath
4069
+ localPath,
4070
+ bucketName: metadata.bucketName,
4071
+ objectName: metadata.objectName
3914
4072
  });
3915
4073
  await persistMetadataToDisk(updated);
3916
4074
  return {
@@ -3919,7 +4077,9 @@ async function materializeOpenAiFile(fileId) {
3919
4077
  bytes: updated.bytes,
3920
4078
  mimeType: updated.mimeType ?? mimeType,
3921
4079
  sha256Hex,
3922
- localPath
4080
+ localPath,
4081
+ bucketName: metadata.bucketName,
4082
+ objectName: metadata.objectName
3923
4083
  };
3924
4084
  })();
3925
4085
  filesState.materializedById.set(fileId, promise);
@@ -3935,14 +4095,14 @@ async function ensureGeminiFileMirror(fileId) {
3935
4095
  if (cached) {
3936
4096
  return cached;
3937
4097
  }
3938
- const materialized = await materializeOpenAiFile(fileId);
4098
+ const materialized = await materializeCanonicalFile(fileId);
3939
4099
  const client = await getGeminiMirrorClient();
3940
4100
  const name = buildGeminiMirrorName(materialized.sha256Hex);
3941
4101
  try {
3942
4102
  const existing = await client.files.get({ name });
3943
4103
  if (existing.name && existing.uri && existing.mimeType) {
3944
4104
  const mirror2 = {
3945
- openAiFileId: fileId,
4105
+ canonicalFileId: fileId,
3946
4106
  name: existing.name,
3947
4107
  uri: existing.uri,
3948
4108
  mimeType: existing.mimeType,
@@ -3970,7 +4130,7 @@ async function ensureGeminiFileMirror(fileId) {
3970
4130
  throw new Error("Gemini file upload completed without a usable URI.");
3971
4131
  }
3972
4132
  const mirror = {
3973
- openAiFileId: fileId,
4133
+ canonicalFileId: fileId,
3974
4134
  name: resolved.name,
3975
4135
  uri: resolved.uri,
3976
4136
  mimeType: resolved.mimeType,
@@ -3995,7 +4155,7 @@ async function ensureVertexFileMirror(fileId) {
3995
4155
  if (cached) {
3996
4156
  return cached;
3997
4157
  }
3998
- const materialized = await materializeOpenAiFile(fileId);
4158
+ const materialized = await materializeCanonicalFile(fileId);
3999
4159
  const bucketName = resolveVertexMirrorBucket();
4000
4160
  const prefix = resolveVertexMirrorPrefix();
4001
4161
  const extension = import_mime.default.getExtension(materialized.mimeType) ?? import_node_path4.default.extname(materialized.filename).replace(/^\./u, "") ?? "bin";
@@ -4036,7 +4196,7 @@ async function ensureVertexFileMirror(fileId) {
4036
4196
  }
4037
4197
  }
4038
4198
  const mirror = {
4039
- openAiFileId: fileId,
4199
+ canonicalFileId: fileId,
4040
4200
  bucket: bucketName,
4041
4201
  objectName,
4042
4202
  fileUri: `gs://${bucketName}/${objectName}`,
@@ -4067,7 +4227,7 @@ async function filesCreate(params) {
4067
4227
  const filename2 = normaliseFilename(params.filename, import_node_path4.default.basename(filePath));
4068
4228
  const mimeType2 = resolveMimeType(filename2, params.mimeType);
4069
4229
  const sha256Hex2 = await computeFileSha256Hex(filePath);
4070
- const uploaded2 = await uploadOpenAiFileFromPath({
4230
+ const uploaded2 = await uploadCanonicalFileFromPath({
4071
4231
  filePath,
4072
4232
  filename: filename2,
4073
4233
  mimeType: mimeType2,
@@ -4076,19 +4236,13 @@ async function filesCreate(params) {
4076
4236
  sha256Hex: sha256Hex2,
4077
4237
  bytes: info.size
4078
4238
  });
4079
- const localPath2 = await cacheFileLocally(filePath, sha256Hex2);
4080
- const cached2 = recordMetadata({
4081
- ...uploaded2,
4082
- localPath: localPath2
4083
- });
4084
- await persistMetadataToDisk(cached2);
4085
- return cached2.file;
4239
+ return uploaded2.file;
4086
4240
  }
4087
4241
  const filename = normaliseFilename(params.filename);
4088
4242
  const bytes = toBuffer(params.data);
4089
4243
  const mimeType = resolveMimeType(filename, params.mimeType, "text/plain");
4090
4244
  const sha256Hex = computeSha256Hex(bytes);
4091
- const uploaded = await uploadOpenAiFileFromBytes({
4245
+ const uploaded = await uploadCanonicalFileFromBytes({
4092
4246
  bytes,
4093
4247
  filename,
4094
4248
  mimeType,
@@ -4096,16 +4250,10 @@ async function filesCreate(params) {
4096
4250
  expiresAfterSeconds,
4097
4251
  sha256Hex
4098
4252
  });
4099
- const localPath = await cacheBufferLocally(bytes, sha256Hex);
4100
- const cached = recordMetadata({
4101
- ...uploaded,
4102
- localPath
4103
- });
4104
- await persistMetadataToDisk(cached);
4105
- return cached.file;
4253
+ return uploaded.file;
4106
4254
  }
4107
4255
  async function filesRetrieve(fileId) {
4108
- return (await retrieveOpenAiFile(fileId)).file;
4256
+ return (await retrieveCanonicalFile(fileId)).file;
4109
4257
  }
4110
4258
  async function filesDelete(fileId) {
4111
4259
  const cachedGemini = filesState.geminiMirrorById.get(fileId);
@@ -4132,34 +4280,73 @@ async function filesDelete(fileId) {
4132
4280
  } catch {
4133
4281
  }
4134
4282
  }
4135
- const response = await getOpenAiClient().files.delete(fileId);
4283
+ try {
4284
+ const { bucketName, objectName } = await resolveCanonicalStorageLocation(fileId);
4285
+ await getStorageClient().bucket(bucketName).file(objectName).delete({ ignoreNotFound: true });
4286
+ } catch {
4287
+ }
4136
4288
  filesState.metadataById.delete(fileId);
4289
+ filesState.canonicalUploadCacheByKey.forEach((value, key) => {
4290
+ if (value.file.id === fileId) {
4291
+ filesState.canonicalUploadCacheByKey.delete(key);
4292
+ }
4293
+ });
4137
4294
  filesState.materializedById.delete(fileId);
4138
4295
  try {
4139
4296
  await (0, import_promises2.unlink)(buildCachedMetadataPath(fileId));
4140
4297
  } catch {
4141
4298
  }
4142
4299
  return {
4143
- id: response.id,
4144
- deleted: response.deleted,
4300
+ id: fileId,
4301
+ deleted: true,
4145
4302
  object: "file"
4146
4303
  };
4147
4304
  }
4148
4305
  async function filesContent(fileId) {
4149
- return await getOpenAiClient().files.content(fileId);
4306
+ const metadata = await retrieveCanonicalFile(fileId);
4307
+ if (!metadata.bucketName || !metadata.objectName) {
4308
+ throw new Error(`Canonical file ${fileId} is missing GCS location metadata.`);
4309
+ }
4310
+ const [bytes] = await getStorageClient().bucket(metadata.bucketName).file(metadata.objectName).download();
4311
+ const headers = new Headers();
4312
+ headers.set("content-type", metadata.mimeType ?? resolveMimeType(metadata.filename, void 0));
4313
+ headers.set("content-length", bytes.byteLength.toString());
4314
+ headers.set(
4315
+ "content-disposition",
4316
+ `inline; filename="${toSafeStorageFilename(metadata.filename)}"`
4317
+ );
4318
+ return new Response(bytes, {
4319
+ status: 200,
4320
+ headers
4321
+ });
4150
4322
  }
4151
4323
  async function getCanonicalFileMetadata(fileId) {
4152
- const metadata = await retrieveOpenAiFile(fileId);
4324
+ const metadata = await retrieveCanonicalFile(fileId);
4153
4325
  const mimeType = metadata.mimeType ?? resolveMimeType(metadata.filename, void 0);
4154
4326
  const updated = metadata.mimeType === mimeType ? metadata : recordMetadata({
4155
4327
  ...metadata,
4156
4328
  mimeType
4157
4329
  });
4330
+ if (!updated.bucketName || !updated.objectName) {
4331
+ throw new Error(`Canonical file ${fileId} is missing GCS location metadata.`);
4332
+ }
4158
4333
  return {
4159
4334
  ...updated,
4160
- mimeType
4335
+ mimeType,
4336
+ bucketName: updated.bucketName,
4337
+ objectName: updated.objectName
4161
4338
  };
4162
4339
  }
4340
+ async function getCanonicalFileSignedUrl(options) {
4341
+ const metadata = await getCanonicalFileMetadata(options.fileId);
4342
+ const [signedUrl] = await getStorageClient().bucket(metadata.bucketName).file(metadata.objectName).getSignedUrl({
4343
+ version: "v4",
4344
+ action: "read",
4345
+ expires: Date.now() + (options.expiresAfterSeconds ?? 15 * 60) * 1e3,
4346
+ responseType: resolveCanonicalStorageContentType(metadata.filename, metadata.mimeType)
4347
+ });
4348
+ return signedUrl;
4349
+ }
4163
4350
  var files = {
4164
4351
  create: filesCreate,
4165
4352
  retrieve: filesRetrieve,
@@ -4521,6 +4708,7 @@ function isJsonSchemaObject(schema) {
4521
4708
  return false;
4522
4709
  }
4523
4710
  var CANONICAL_GEMINI_FILE_URI_PREFIX = "openai://file/";
4711
+ var CANONICAL_LLM_FILE_ID_PATTERN = /^file_[a-f0-9]{64}$/u;
4524
4712
  function buildCanonicalGeminiFileUri(fileId) {
4525
4713
  return `${CANONICAL_GEMINI_FILE_URI_PREFIX}${fileId}`;
4526
4714
  }
@@ -4531,6 +4719,75 @@ function parseCanonicalGeminiFileId(fileUri) {
4531
4719
  const fileId = fileUri.slice(CANONICAL_GEMINI_FILE_URI_PREFIX.length).trim();
4532
4720
  return fileId.length > 0 ? fileId : void 0;
4533
4721
  }
4722
+ function isCanonicalLlmFileId(fileId) {
4723
+ return typeof fileId === "string" && CANONICAL_LLM_FILE_ID_PATTERN.test(fileId.trim());
4724
+ }
4725
+ function isLlmMediaResolution(value) {
4726
+ return value === "auto" || value === "low" || value === "medium" || value === "high" || value === "original";
4727
+ }
4728
+ function resolveEffectiveMediaResolution(detail, fallback) {
4729
+ return detail ?? fallback;
4730
+ }
4731
+ function supportsOpenAiOriginalImageDetail(model) {
4732
+ if (!model) {
4733
+ return false;
4734
+ }
4735
+ const providerModel = isChatGptModelId(model) ? resolveChatGptProviderModel(model) : model;
4736
+ const match = /^gpt-(\d+)(?:\.(\d+))?/u.exec(providerModel);
4737
+ if (!match) {
4738
+ return false;
4739
+ }
4740
+ const major = Number(match[1]);
4741
+ const minor = Number(match[2] ?? "0");
4742
+ if (!Number.isFinite(major) || !Number.isFinite(minor)) {
4743
+ return false;
4744
+ }
4745
+ return major > 5 || major === 5 && minor >= 4;
4746
+ }
4747
+ function toOpenAiImageDetail(mediaResolution, model) {
4748
+ switch (mediaResolution) {
4749
+ case "low":
4750
+ return "low";
4751
+ case "medium":
4752
+ return "high";
4753
+ case "high":
4754
+ return "high";
4755
+ case "original":
4756
+ return supportsOpenAiOriginalImageDetail(model) ? "original" : "high";
4757
+ case "auto":
4758
+ default:
4759
+ return "auto";
4760
+ }
4761
+ }
4762
+ function toGeminiMediaResolution(mediaResolution) {
4763
+ switch (mediaResolution) {
4764
+ case "low":
4765
+ return import_genai2.MediaResolution.MEDIA_RESOLUTION_LOW;
4766
+ case "medium":
4767
+ return import_genai2.MediaResolution.MEDIA_RESOLUTION_MEDIUM;
4768
+ case "high":
4769
+ case "original":
4770
+ return import_genai2.MediaResolution.MEDIA_RESOLUTION_HIGH;
4771
+ case "auto":
4772
+ default:
4773
+ return void 0;
4774
+ }
4775
+ }
4776
+ function toGeminiPartMediaResolution(mediaResolution) {
4777
+ switch (mediaResolution) {
4778
+ case "low":
4779
+ return import_genai2.PartMediaResolutionLevel.MEDIA_RESOLUTION_LOW;
4780
+ case "medium":
4781
+ return import_genai2.PartMediaResolutionLevel.MEDIA_RESOLUTION_MEDIUM;
4782
+ case "high":
4783
+ return import_genai2.PartMediaResolutionLevel.MEDIA_RESOLUTION_HIGH;
4784
+ case "original":
4785
+ return import_genai2.PartMediaResolutionLevel.MEDIA_RESOLUTION_ULTRA_HIGH;
4786
+ case "auto":
4787
+ default:
4788
+ return void 0;
4789
+ }
4790
+ }
4534
4791
  function cloneContentPart(part) {
4535
4792
  switch (part.type) {
4536
4793
  case "text":
@@ -4659,7 +4916,8 @@ function convertGeminiContentToLlmContent(content) {
4659
4916
  parts: convertGooglePartsToLlmParts(content.parts ?? [])
4660
4917
  };
4661
4918
  }
4662
- function toGeminiPart(part) {
4919
+ function toGeminiPart(part, options) {
4920
+ const defaultMediaResolution = options?.defaultMediaResolution;
4663
4921
  switch (part.type) {
4664
4922
  case "text":
4665
4923
  return {
@@ -4667,6 +4925,18 @@ function toGeminiPart(part) {
4667
4925
  thought: part.thought === true ? true : void 0
4668
4926
  };
4669
4927
  case "inlineData": {
4928
+ if (isInlineImageMime(part.mimeType)) {
4929
+ const mimeType = part.mimeType ?? "application/octet-stream";
4930
+ const geminiPart = (0, import_genai2.createPartFromBase64)(
4931
+ part.data,
4932
+ mimeType,
4933
+ toGeminiPartMediaResolution(defaultMediaResolution)
4934
+ );
4935
+ if (part.filename && geminiPart.inlineData) {
4936
+ geminiPart.inlineData.displayName = part.filename;
4937
+ }
4938
+ return geminiPart;
4939
+ }
4670
4940
  const inlineData = {
4671
4941
  data: part.data,
4672
4942
  mimeType: part.mimeType
@@ -4679,31 +4949,35 @@ function toGeminiPart(part) {
4679
4949
  };
4680
4950
  }
4681
4951
  case "input_image": {
4952
+ const mediaResolution = resolveEffectiveMediaResolution(part.detail, defaultMediaResolution);
4953
+ const geminiPartMediaResolution = toGeminiPartMediaResolution(mediaResolution);
4682
4954
  if (part.file_id) {
4683
- return {
4684
- fileData: {
4685
- fileUri: buildCanonicalGeminiFileUri(part.file_id),
4686
- mimeType: inferToolOutputMimeTypeFromFilename(part.filename) ?? "application/octet-stream"
4687
- }
4688
- };
4955
+ return (0, import_genai2.createPartFromUri)(
4956
+ buildCanonicalGeminiFileUri(part.file_id),
4957
+ inferToolOutputMimeTypeFromFilename(part.filename) ?? "application/octet-stream",
4958
+ geminiPartMediaResolution
4959
+ );
4689
4960
  }
4690
4961
  if (typeof part.image_url !== "string" || part.image_url.trim().length === 0) {
4691
4962
  throw new Error("input_image requires image_url or file_id.");
4692
4963
  }
4693
4964
  const parsed = parseDataUrlPayload(part.image_url);
4694
4965
  if (parsed) {
4695
- const geminiPart = (0, import_genai2.createPartFromBase64)(parsed.dataBase64, parsed.mimeType);
4966
+ const geminiPart = (0, import_genai2.createPartFromBase64)(
4967
+ parsed.dataBase64,
4968
+ parsed.mimeType,
4969
+ geminiPartMediaResolution
4970
+ );
4696
4971
  if (part.filename && geminiPart.inlineData) {
4697
4972
  geminiPart.inlineData.displayName = part.filename;
4698
4973
  }
4699
4974
  return geminiPart;
4700
4975
  }
4701
- return {
4702
- fileData: {
4703
- fileUri: part.image_url,
4704
- mimeType: inferToolOutputMimeTypeFromFilename(part.filename) ?? "application/octet-stream"
4705
- }
4706
- };
4976
+ return (0, import_genai2.createPartFromUri)(
4977
+ part.image_url,
4978
+ inferToolOutputMimeTypeFromFilename(part.filename) ?? "application/octet-stream",
4979
+ geminiPartMediaResolution
4980
+ );
4707
4981
  }
4708
4982
  case "input_file": {
4709
4983
  if (part.file_id) {
@@ -4746,11 +5020,11 @@ function toGeminiPart(part) {
4746
5020
  throw new Error("Unsupported LLM content part");
4747
5021
  }
4748
5022
  }
4749
- function convertLlmContentToGeminiContent(content) {
5023
+ function convertLlmContentToGeminiContent(content, options) {
4750
5024
  const role = content.role === "assistant" ? "model" : "user";
4751
5025
  return {
4752
5026
  role,
4753
- parts: content.parts.map(toGeminiPart)
5027
+ parts: content.parts.map((part) => toGeminiPart(part, options))
4754
5028
  };
4755
5029
  }
4756
5030
  function resolveProvider(model) {
@@ -4931,11 +5205,25 @@ async function storeCanonicalPromptFile(options) {
4931
5205
  mimeType: options.mimeType
4932
5206
  };
4933
5207
  }
4934
- async function prepareOpenAiPromptContentItem(item) {
5208
+ async function prepareOpenAiPromptContentItem(item, options) {
4935
5209
  if (!isOpenAiNativeContentItem(item)) {
4936
5210
  return item;
4937
5211
  }
4938
- if (item.type === "input_image" && typeof item.image_url === "string" && item.image_url.trim().toLowerCase().startsWith("data:")) {
5212
+ if (item.type === "input_image") {
5213
+ if (isCanonicalLlmFileId(item.file_id)) {
5214
+ const signedUrl2 = await getCanonicalFileSignedUrl({ fileId: item.file_id });
5215
+ return {
5216
+ type: "input_image",
5217
+ image_url: signedUrl2,
5218
+ detail: toOpenAiImageDetail(
5219
+ isLlmMediaResolution(item.detail) ? item.detail : void 0,
5220
+ options?.model
5221
+ )
5222
+ };
5223
+ }
5224
+ if (options?.offloadInlineData !== true || typeof item.image_url !== "string" || !item.image_url.trim().toLowerCase().startsWith("data:")) {
5225
+ return item;
5226
+ }
4939
5227
  const parsed = parseDataUrlPayload(item.image_url);
4940
5228
  if (!parsed) {
4941
5229
  return item;
@@ -4948,13 +5236,27 @@ async function prepareOpenAiPromptContentItem(item) {
4948
5236
  guessInlineDataFilename(parsed.mimeType)
4949
5237
  )
4950
5238
  });
5239
+ const signedUrl = await getCanonicalFileSignedUrl({ fileId: uploaded.fileId });
4951
5240
  return {
4952
5241
  type: "input_image",
4953
- detail: item.detail === "high" || item.detail === "low" ? item.detail : "auto",
4954
- file_id: uploaded.fileId
5242
+ image_url: signedUrl,
5243
+ detail: toOpenAiImageDetail(
5244
+ isLlmMediaResolution(item.detail) ? item.detail : void 0,
5245
+ options?.model
5246
+ )
5247
+ };
5248
+ }
5249
+ if (item.type !== "input_file") {
5250
+ return item;
5251
+ }
5252
+ if (isCanonicalLlmFileId(item.file_id)) {
5253
+ const signedUrl = await getCanonicalFileSignedUrl({ fileId: item.file_id });
5254
+ return {
5255
+ type: "input_file",
5256
+ file_url: signedUrl
4955
5257
  };
4956
5258
  }
4957
- if (item.type !== "input_file" || item.file_id) {
5259
+ if (options?.offloadInlineData !== true) {
4958
5260
  return item;
4959
5261
  }
4960
5262
  if (typeof item.file_data === "string" && item.file_data.trim().length > 0) {
@@ -4968,7 +5270,11 @@ async function prepareOpenAiPromptContentItem(item) {
4968
5270
  mimeType,
4969
5271
  filename
4970
5272
  });
4971
- return { type: "input_file", file_id: uploaded.fileId };
5273
+ const signedUrl = await getCanonicalFileSignedUrl({ fileId: uploaded.fileId });
5274
+ return {
5275
+ type: "input_file",
5276
+ file_url: signedUrl
5277
+ };
4972
5278
  }
4973
5279
  if (typeof item.file_url === "string" && item.file_url.trim().toLowerCase().startsWith("data:")) {
4974
5280
  const parsed = parseDataUrlPayload(item.file_url);
@@ -4983,11 +5289,15 @@ async function prepareOpenAiPromptContentItem(item) {
4983
5289
  guessInlineDataFilename(parsed.mimeType)
4984
5290
  )
4985
5291
  });
4986
- return { type: "input_file", file_id: uploaded.fileId };
5292
+ const signedUrl = await getCanonicalFileSignedUrl({ fileId: uploaded.fileId });
5293
+ return {
5294
+ type: "input_file",
5295
+ file_url: signedUrl
5296
+ };
4987
5297
  }
4988
5298
  return item;
4989
5299
  }
4990
- async function prepareOpenAiPromptInput(input) {
5300
+ async function prepareOpenAiPromptInput(input, options) {
4991
5301
  const prepareItem = async (item) => {
4992
5302
  if (!item || typeof item !== "object") {
4993
5303
  return item;
@@ -4997,7 +5307,7 @@ async function prepareOpenAiPromptInput(input) {
4997
5307
  return {
4998
5308
  ...record,
4999
5309
  content: await Promise.all(
5000
- record.content.map((part) => prepareOpenAiPromptContentItem(part))
5310
+ record.content.map((part) => prepareOpenAiPromptContentItem(part, options))
5001
5311
  )
5002
5312
  };
5003
5313
  }
@@ -5005,19 +5315,48 @@ async function prepareOpenAiPromptInput(input) {
5005
5315
  return {
5006
5316
  ...record,
5007
5317
  output: await Promise.all(
5008
- record.output.map((part) => prepareOpenAiPromptContentItem(part))
5318
+ record.output.map((part) => prepareOpenAiPromptContentItem(part, options))
5009
5319
  )
5010
5320
  };
5011
5321
  }
5012
- return await prepareOpenAiPromptContentItem(item);
5322
+ return await prepareOpenAiPromptContentItem(item, options);
5013
5323
  };
5014
5324
  return await Promise.all(input.map((item) => prepareItem(item)));
5015
5325
  }
5016
- async function maybePrepareOpenAiPromptInput(input) {
5017
- if (estimateOpenAiInlinePromptBytes(input) <= INLINE_ATTACHMENT_PROMPT_THRESHOLD_BYTES) {
5326
+ function hasCanonicalOpenAiFileReferences(input) {
5327
+ let found = false;
5328
+ const visitItems = (items) => {
5329
+ for (const item of items) {
5330
+ if (found || !item || typeof item !== "object") {
5331
+ continue;
5332
+ }
5333
+ if (Array.isArray(item.content)) {
5334
+ visitItems(item.content);
5335
+ }
5336
+ if (Array.isArray(item.output)) {
5337
+ visitItems(item.output);
5338
+ }
5339
+ if (!isOpenAiNativeContentItem(item)) {
5340
+ continue;
5341
+ }
5342
+ if ((item.type === "input_image" || item.type === "input_file") && isCanonicalLlmFileId(item.file_id)) {
5343
+ found = true;
5344
+ return;
5345
+ }
5346
+ }
5347
+ };
5348
+ visitItems(input);
5349
+ return found;
5350
+ }
5351
+ async function maybePrepareOpenAiPromptInput(input, options) {
5352
+ const offloadInlineData = estimateOpenAiInlinePromptBytes(input) > INLINE_ATTACHMENT_PROMPT_THRESHOLD_BYTES;
5353
+ if (!offloadInlineData && !hasCanonicalOpenAiFileReferences(input)) {
5018
5354
  return Array.from(input);
5019
5355
  }
5020
- return await prepareOpenAiPromptInput(input);
5356
+ return await prepareOpenAiPromptInput(input, {
5357
+ ...options,
5358
+ offloadInlineData
5359
+ });
5021
5360
  }
5022
5361
  function estimateGeminiInlinePromptBytes(contents) {
5023
5362
  let total = 0;
@@ -5048,22 +5387,25 @@ async function prepareGeminiPromptContents(contents) {
5048
5387
  for (const part of content.parts ?? []) {
5049
5388
  const canonicalFileId = parseCanonicalGeminiFileId(part.fileData?.fileUri);
5050
5389
  if (canonicalFileId) {
5390
+ const mediaResolution = part.mediaResolution?.level;
5051
5391
  await getCanonicalFileMetadata(canonicalFileId);
5052
5392
  if (backend === "api") {
5053
5393
  const mirrored = await ensureGeminiFileMirror(canonicalFileId);
5054
- parts.push((0, import_genai2.createPartFromUri)(mirrored.uri, mirrored.mimeType));
5394
+ parts.push((0, import_genai2.createPartFromUri)(mirrored.uri, mirrored.mimeType, mediaResolution));
5055
5395
  } else {
5056
5396
  const mirrored = await ensureVertexFileMirror(canonicalFileId);
5057
5397
  parts.push({
5058
5398
  fileData: {
5059
5399
  fileUri: mirrored.fileUri,
5060
5400
  mimeType: mirrored.mimeType
5061
- }
5401
+ },
5402
+ ...mediaResolution ? { mediaResolution: { level: mediaResolution } } : {}
5062
5403
  });
5063
5404
  }
5064
5405
  continue;
5065
5406
  }
5066
5407
  if (part.inlineData?.data) {
5408
+ const mediaResolution = part.mediaResolution?.level;
5067
5409
  const mimeType = part.inlineData.mimeType ?? "application/octet-stream";
5068
5410
  const filename = normaliseAttachmentFilename(
5069
5411
  getInlineAttachmentFilename(part.inlineData) ?? part.inlineData.displayName ?? guessInlineDataFilename(mimeType),
@@ -5076,14 +5418,15 @@ async function prepareGeminiPromptContents(contents) {
5076
5418
  });
5077
5419
  if (backend === "api") {
5078
5420
  const mirrored = await ensureGeminiFileMirror(stored.fileId);
5079
- parts.push((0, import_genai2.createPartFromUri)(mirrored.uri, mirrored.mimeType));
5421
+ parts.push((0, import_genai2.createPartFromUri)(mirrored.uri, mirrored.mimeType, mediaResolution));
5080
5422
  } else {
5081
5423
  const mirrored = await ensureVertexFileMirror(stored.fileId);
5082
5424
  parts.push({
5083
5425
  fileData: {
5084
5426
  fileUri: mirrored.fileUri,
5085
5427
  mimeType: mirrored.mimeType
5086
- }
5428
+ },
5429
+ ...mediaResolution ? { mediaResolution: { level: mediaResolution } } : {}
5087
5430
  });
5088
5431
  }
5089
5432
  continue;
@@ -5546,7 +5889,7 @@ function resolveTextContents(input) {
5546
5889
  }
5547
5890
  return contents;
5548
5891
  }
5549
- function toOpenAiInput(contents) {
5892
+ function toOpenAiInput(contents, options) {
5550
5893
  const OPENAI_ROLE_FROM_LLM = {
5551
5894
  user: "user",
5552
5895
  assistant: "assistant",
@@ -5554,6 +5897,8 @@ function toOpenAiInput(contents) {
5554
5897
  developer: "developer",
5555
5898
  tool: "assistant"
5556
5899
  };
5900
+ const defaultMediaResolution = options?.defaultMediaResolution;
5901
+ const model = options?.model;
5557
5902
  return contents.map((content) => {
5558
5903
  const parts = [];
5559
5904
  for (const part of content.parts) {
@@ -5568,7 +5913,7 @@ function toOpenAiInput(contents) {
5568
5913
  const imagePart = {
5569
5914
  type: "input_image",
5570
5915
  image_url: dataUrl,
5571
- detail: "auto"
5916
+ detail: toOpenAiImageDetail(defaultMediaResolution, model)
5572
5917
  };
5573
5918
  setInlineAttachmentFilename(
5574
5919
  imagePart,
@@ -5585,11 +5930,15 @@ function toOpenAiInput(contents) {
5585
5930
  break;
5586
5931
  }
5587
5932
  case "input_image": {
5933
+ const mediaResolution = resolveEffectiveMediaResolution(
5934
+ part.detail,
5935
+ defaultMediaResolution
5936
+ );
5588
5937
  const imagePart = {
5589
5938
  type: "input_image",
5590
5939
  ...part.file_id ? { file_id: part.file_id } : {},
5591
5940
  ...part.image_url ? { image_url: part.image_url } : {},
5592
- detail: part.detail === "high" || part.detail === "low" ? part.detail : "auto"
5941
+ detail: toOpenAiImageDetail(mediaResolution, model)
5593
5942
  };
5594
5943
  if (part.filename) {
5595
5944
  setInlineAttachmentFilename(imagePart, part.filename);
@@ -5622,9 +5971,11 @@ function toOpenAiInput(contents) {
5622
5971
  };
5623
5972
  });
5624
5973
  }
5625
- function toChatGptInput(contents) {
5974
+ function toChatGptInput(contents, options) {
5626
5975
  const instructionsParts = [];
5627
5976
  const input = [];
5977
+ const defaultMediaResolution = options?.defaultMediaResolution;
5978
+ const model = options?.model;
5628
5979
  for (const content of contents) {
5629
5980
  if (content.role === "system" || content.role === "developer") {
5630
5981
  for (const part of content.parts) {
@@ -5660,7 +6011,7 @@ function toChatGptInput(contents) {
5660
6011
  parts.push({
5661
6012
  type: "input_image",
5662
6013
  image_url: dataUrl,
5663
- detail: "auto"
6014
+ detail: toOpenAiImageDetail(defaultMediaResolution, model)
5664
6015
  });
5665
6016
  } else {
5666
6017
  parts.push({
@@ -5674,14 +6025,19 @@ function toChatGptInput(contents) {
5674
6025
  }
5675
6026
  break;
5676
6027
  }
5677
- case "input_image":
6028
+ case "input_image": {
6029
+ const mediaResolution = resolveEffectiveMediaResolution(
6030
+ part.detail,
6031
+ defaultMediaResolution
6032
+ );
5678
6033
  parts.push({
5679
6034
  type: "input_image",
5680
6035
  ...part.file_id ? { file_id: part.file_id } : {},
5681
6036
  ...part.image_url ? { image_url: part.image_url } : {},
5682
- detail: part.detail === "high" || part.detail === "low" ? part.detail : "auto"
6037
+ detail: toOpenAiImageDetail(mediaResolution, model)
5683
6038
  });
5684
6039
  break;
6040
+ }
5685
6041
  case "input_file":
5686
6042
  parts.push({
5687
6043
  type: "input_file",
@@ -6074,6 +6430,9 @@ function isLlmToolOutputContentItem(value) {
6074
6430
  return false;
6075
6431
  }
6076
6432
  }
6433
+ if (value.detail !== void 0 && value.detail !== null && !isLlmMediaResolution(value.detail)) {
6434
+ return false;
6435
+ }
6077
6436
  return value.image_url !== void 0 || value.file_id !== void 0;
6078
6437
  }
6079
6438
  if (itemType === "input_file") {
@@ -6088,17 +6447,30 @@ function isLlmToolOutputContentItem(value) {
6088
6447
  }
6089
6448
  return false;
6090
6449
  }
6091
- function toOpenAiToolOutput(value) {
6450
+ function toOpenAiToolOutput(value, options) {
6451
+ const normalizeImageItem = (item) => {
6452
+ if (item.type !== "input_image") {
6453
+ return item;
6454
+ }
6455
+ const mediaResolution = resolveEffectiveMediaResolution(
6456
+ item.detail,
6457
+ options?.defaultMediaResolution
6458
+ );
6459
+ return {
6460
+ ...item,
6461
+ detail: toOpenAiImageDetail(mediaResolution, options?.model)
6462
+ };
6463
+ };
6092
6464
  if (isLlmToolOutputContentItem(value)) {
6093
- return [value];
6465
+ return [normalizeImageItem(value)];
6094
6466
  }
6095
6467
  if (Array.isArray(value) && value.every((item) => isLlmToolOutputContentItem(item))) {
6096
- return value;
6468
+ return value.map((item) => normalizeImageItem(item));
6097
6469
  }
6098
6470
  return mergeToolOutput(value);
6099
6471
  }
6100
- function toChatGptToolOutput(value) {
6101
- const toolOutput = toOpenAiToolOutput(value);
6472
+ function toChatGptToolOutput(value, options) {
6473
+ const toolOutput = toOpenAiToolOutput(value, options);
6102
6474
  if (typeof toolOutput === "string") {
6103
6475
  return toolOutput;
6104
6476
  }
@@ -6110,7 +6482,12 @@ function toChatGptToolOutput(value) {
6110
6482
  type: "input_image",
6111
6483
  ...item.file_id ? { file_id: item.file_id } : {},
6112
6484
  ...item.image_url ? { image_url: item.image_url } : {},
6113
- ...item.detail ? { detail: item.detail } : {}
6485
+ ...item.detail ? {
6486
+ detail: toOpenAiImageDetail(
6487
+ resolveEffectiveMediaResolution(item.detail, options?.defaultMediaResolution),
6488
+ options?.model
6489
+ )
6490
+ } : {}
6114
6491
  };
6115
6492
  });
6116
6493
  }
@@ -6281,9 +6658,6 @@ async function maybeSpillToolOutputItem(item, toolName, options) {
6281
6658
  return item;
6282
6659
  }
6283
6660
  async function maybeSpillToolOutput(value, toolName, options) {
6284
- if (options?.provider === "chatgpt") {
6285
- return value;
6286
- }
6287
6661
  if (typeof value === "string") {
6288
6662
  if (options?.force !== true && import_node_buffer4.Buffer.byteLength(value, "utf8") <= TOOL_OUTPUT_SPILL_THRESHOLD_BYTES) {
6289
6663
  return value;
@@ -6369,34 +6743,41 @@ async function maybeSpillCombinedToolCallOutputs(callResults, options) {
6369
6743
  })
6370
6744
  );
6371
6745
  }
6372
- function buildGeminiToolOutputMediaPart(item) {
6746
+ function buildGeminiToolOutputMediaPart(item, options) {
6373
6747
  if (item.type === "input_image") {
6748
+ const mediaResolution = resolveEffectiveMediaResolution(
6749
+ item.detail,
6750
+ options?.defaultMediaResolution
6751
+ );
6752
+ const geminiPartMediaResolution = toGeminiPartMediaResolution(mediaResolution);
6374
6753
  if (typeof item.file_id === "string" && item.file_id.trim().length > 0) {
6375
- return {
6376
- fileData: {
6377
- fileUri: buildCanonicalGeminiFileUri(item.file_id),
6378
- mimeType: inferToolOutputMimeTypeFromFilename(item.filename) ?? "application/octet-stream"
6379
- }
6380
- };
6754
+ return (0, import_genai2.createPartFromUri)(
6755
+ buildCanonicalGeminiFileUri(item.file_id),
6756
+ inferToolOutputMimeTypeFromFilename(item.filename) ?? "application/octet-stream",
6757
+ geminiPartMediaResolution
6758
+ );
6381
6759
  }
6382
6760
  if (typeof item.image_url !== "string" || item.image_url.trim().length === 0) {
6383
6761
  return null;
6384
6762
  }
6385
6763
  const parsed = parseDataUrlPayload(item.image_url);
6386
6764
  if (parsed) {
6387
- const part = (0, import_genai2.createPartFromBase64)(parsed.dataBase64, parsed.mimeType);
6765
+ const part = (0, import_genai2.createPartFromBase64)(
6766
+ parsed.dataBase64,
6767
+ parsed.mimeType,
6768
+ geminiPartMediaResolution
6769
+ );
6388
6770
  const displayName = item.filename?.trim();
6389
6771
  if (displayName && part.inlineData) {
6390
6772
  part.inlineData.displayName = displayName;
6391
6773
  }
6392
6774
  return part;
6393
6775
  }
6394
- return {
6395
- fileData: {
6396
- fileUri: item.image_url,
6397
- mimeType: inferToolOutputMimeTypeFromFilename(item.filename) ?? "application/octet-stream"
6398
- }
6399
- };
6776
+ return (0, import_genai2.createPartFromUri)(
6777
+ item.image_url,
6778
+ inferToolOutputMimeTypeFromFilename(item.filename) ?? "application/octet-stream",
6779
+ geminiPartMediaResolution
6780
+ );
6400
6781
  }
6401
6782
  if (item.type === "input_file") {
6402
6783
  if (typeof item.file_id === "string" && item.file_id.trim().length > 0) {
@@ -6474,7 +6855,9 @@ function buildGeminiFunctionResponseParts(options) {
6474
6855
  }
6475
6856
  const responseOutput = outputItems.map((item) => toGeminiToolOutputPlaceholder(item));
6476
6857
  const responseParts = outputItems.flatMap((item) => {
6477
- const mediaPart = buildGeminiToolOutputMediaPart(item);
6858
+ const mediaPart = buildGeminiToolOutputMediaPart(item, {
6859
+ defaultMediaResolution: options.defaultMediaResolution
6860
+ });
6478
6861
  return mediaPart ? [mediaPart] : [];
6479
6862
  });
6480
6863
  const responsePayload = { output: responseOutput };
@@ -7241,6 +7624,7 @@ function startLlmCallLoggerFromContents(options) {
7241
7624
  ...options.request.imageAspectRatio ? { imageAspectRatio: options.request.imageAspectRatio } : {},
7242
7625
  ...options.request.imageSize ? { imageSize: options.request.imageSize } : {},
7243
7626
  ...options.request.thinkingLevel ? { thinkingLevel: options.request.thinkingLevel } : {},
7627
+ ...options.request.mediaResolution ? { mediaResolution: options.request.mediaResolution } : {},
7244
7628
  ...options.request.openAiTextFormat ? { openAiTextFormat: sanitiseLogValue(options.request.openAiTextFormat) } : {},
7245
7629
  ...getCurrentToolCallContext() ? { toolContext: getCurrentToolCallContext() } : {}
7246
7630
  },
@@ -7351,7 +7735,13 @@ async function runTextCall(params) {
7351
7735
  const { result } = await collectFileUploadMetrics(async () => {
7352
7736
  try {
7353
7737
  if (provider === "openai") {
7354
- const openAiInput = await maybePrepareOpenAiPromptInput(toOpenAiInput(contents));
7738
+ const openAiInput = await maybePrepareOpenAiPromptInput(
7739
+ toOpenAiInput(contents, {
7740
+ defaultMediaResolution: request.mediaResolution,
7741
+ model: request.model
7742
+ }),
7743
+ { model: request.model, provider: "openai" }
7744
+ );
7355
7745
  const openAiTools = toOpenAiTools(request.tools);
7356
7746
  const reasoningEffort = resolveOpenAiReasoningEffort(
7357
7747
  modelForProvider,
@@ -7425,7 +7815,14 @@ async function runTextCall(params) {
7425
7815
  }
7426
7816
  }, modelForProvider);
7427
7817
  } else if (provider === "chatgpt") {
7428
- const chatGptInput = toChatGptInput(contents);
7818
+ const chatGptInput = toChatGptInput(contents, {
7819
+ defaultMediaResolution: request.mediaResolution,
7820
+ model: request.model
7821
+ });
7822
+ const preparedChatGptInput = await maybePrepareOpenAiPromptInput(chatGptInput.input, {
7823
+ model: request.model,
7824
+ provider: "chatgpt"
7825
+ });
7429
7826
  const reasoningEffort = resolveOpenAiReasoningEffort(request.model, request.thinkingLevel);
7430
7827
  const openAiTools = toOpenAiTools(request.tools);
7431
7828
  const requestPayload = {
@@ -7434,7 +7831,7 @@ async function runTextCall(params) {
7434
7831
  stream: true,
7435
7832
  ...providerInfo.serviceTier ? { service_tier: providerInfo.serviceTier } : {},
7436
7833
  instructions: chatGptInput.instructions ?? "You are a helpful assistant.",
7437
- input: chatGptInput.input,
7834
+ input: preparedChatGptInput,
7438
7835
  include: ["reasoning.encrypted_content"],
7439
7836
  reasoning: {
7440
7837
  effort: toOpenAiReasoningEffort(reasoningEffort),
@@ -7522,12 +7919,18 @@ async function runTextCall(params) {
7522
7919
  }, modelForProvider);
7523
7920
  } else {
7524
7921
  const geminiContents = await maybePrepareGeminiPromptContents(
7525
- contents.map(convertLlmContentToGeminiContent)
7922
+ contents.map(
7923
+ (content2) => convertLlmContentToGeminiContent(content2, {
7924
+ defaultMediaResolution: request.mediaResolution
7925
+ })
7926
+ )
7526
7927
  );
7527
7928
  const thinkingConfig = resolveGeminiThinkingConfig(modelForProvider, request.thinkingLevel);
7929
+ const mediaResolution = toGeminiMediaResolution(request.mediaResolution);
7528
7930
  const config = {
7529
7931
  maxOutputTokens: 32e3,
7530
7932
  ...thinkingConfig ? { thinkingConfig } : {},
7933
+ ...mediaResolution ? { mediaResolution } : {},
7531
7934
  ...request.responseMimeType ? { responseMimeType: request.responseMimeType } : {},
7532
7935
  ...request.responseJsonSchema ? { responseJsonSchema: request.responseJsonSchema } : {},
7533
7936
  ...request.responseModalities ? { responseModalities: Array.from(request.responseModalities) } : {},
@@ -8205,7 +8608,10 @@ async function runToolLoop(request) {
8205
8608
  summary: "detailed"
8206
8609
  };
8207
8610
  let previousResponseId;
8208
- let input = toOpenAiInput(contents);
8611
+ let input = toOpenAiInput(contents, {
8612
+ defaultMediaResolution: request.mediaResolution,
8613
+ model: request.model
8614
+ });
8209
8615
  for (let stepIndex = 0; stepIndex < maxSteps; stepIndex += 1) {
8210
8616
  const turn = stepIndex + 1;
8211
8617
  const stepStartedAtMs = Date.now();
@@ -8232,7 +8638,10 @@ async function runToolLoop(request) {
8232
8638
  let reasoningSummary = "";
8233
8639
  let stepToolCallText;
8234
8640
  let stepToolCallPayload;
8235
- const preparedInput = await maybePrepareOpenAiPromptInput(input);
8641
+ const preparedInput = await maybePrepareOpenAiPromptInput(input, {
8642
+ model: request.model,
8643
+ provider: "openai"
8644
+ });
8236
8645
  const stepRequestPayload = {
8237
8646
  model: providerInfo.model,
8238
8647
  input: preparedInput,
@@ -8363,7 +8772,10 @@ async function runToolLoop(request) {
8363
8772
  const stepToolCalls = [];
8364
8773
  if (responseToolCalls.length === 0) {
8365
8774
  const steeringInput2 = steeringInternal?.drainPendingContents() ?? [];
8366
- const steeringItems2 = steeringInput2.length > 0 ? toOpenAiInput(steeringInput2) : [];
8775
+ const steeringItems2 = steeringInput2.length > 0 ? toOpenAiInput(steeringInput2, {
8776
+ defaultMediaResolution: request.mediaResolution,
8777
+ model: request.model
8778
+ }) : [];
8367
8779
  finalText = responseText;
8368
8780
  finalThoughts = reasoningSummary;
8369
8781
  const stepCompletedAtMs2 = Date.now();
@@ -8494,13 +8906,19 @@ async function runToolLoop(request) {
8494
8906
  toolOutputs.push({
8495
8907
  type: "custom_tool_call_output",
8496
8908
  call_id: entry.call.call_id,
8497
- output: toOpenAiToolOutput(outputPayload)
8909
+ output: toOpenAiToolOutput(outputPayload, {
8910
+ defaultMediaResolution: request.mediaResolution,
8911
+ model: request.model
8912
+ })
8498
8913
  });
8499
8914
  } else {
8500
8915
  toolOutputs.push({
8501
8916
  type: "function_call_output",
8502
8917
  call_id: entry.call.call_id,
8503
- output: toOpenAiToolOutput(outputPayload)
8918
+ output: toOpenAiToolOutput(outputPayload, {
8919
+ defaultMediaResolution: request.mediaResolution,
8920
+ model: request.model
8921
+ })
8504
8922
  });
8505
8923
  }
8506
8924
  }
@@ -8525,7 +8943,10 @@ async function runToolLoop(request) {
8525
8943
  timing
8526
8944
  });
8527
8945
  const steeringInput = steeringInternal?.drainPendingContents() ?? [];
8528
- const steeringItems = steeringInput.length > 0 ? toOpenAiInput(steeringInput) : [];
8946
+ const steeringItems = steeringInput.length > 0 ? toOpenAiInput(steeringInput, {
8947
+ defaultMediaResolution: request.mediaResolution,
8948
+ model: request.model
8949
+ }) : [];
8529
8950
  stepCallLogger?.complete({
8530
8951
  responseText,
8531
8952
  toolCallText: stepToolCallText,
@@ -8570,7 +8991,10 @@ async function runToolLoop(request) {
8570
8991
  const openAiNativeTools = toOpenAiTools(request.modelTools);
8571
8992
  const openAiTools = openAiNativeTools ? [...openAiNativeTools, ...openAiAgentTools] : [...openAiAgentTools];
8572
8993
  const reasoningEffort = resolveOpenAiReasoningEffort(request.model, request.thinkingLevel);
8573
- const toolLoopInput = toChatGptInput(contents);
8994
+ const toolLoopInput = toChatGptInput(contents, {
8995
+ defaultMediaResolution: request.mediaResolution,
8996
+ model: request.model
8997
+ });
8574
8998
  const conversationId = `tool-loop-${(0, import_node_crypto2.randomBytes)(8).toString("hex")}`;
8575
8999
  const promptCacheKey = conversationId;
8576
9000
  let input = [...toolLoopInput.input];
@@ -8586,6 +9010,10 @@ async function runToolLoop(request) {
8586
9010
  let reasoningSummaryText = "";
8587
9011
  let stepToolCallText;
8588
9012
  let stepToolCallPayload;
9013
+ const preparedInput = await maybePrepareOpenAiPromptInput(input, {
9014
+ model: request.model,
9015
+ provider: "chatgpt"
9016
+ });
8589
9017
  const markFirstModelEvent = () => {
8590
9018
  if (firstModelEventAtMs === void 0) {
8591
9019
  firstModelEventAtMs = Date.now();
@@ -8597,7 +9025,7 @@ async function runToolLoop(request) {
8597
9025
  stream: true,
8598
9026
  ...providerInfo.serviceTier ? { service_tier: providerInfo.serviceTier } : {},
8599
9027
  instructions: toolLoopInput.instructions ?? "You are a helpful assistant.",
8600
- input,
9028
+ input: preparedInput,
8601
9029
  prompt_cache_key: promptCacheKey,
8602
9030
  include: ["reasoning.encrypted_content"],
8603
9031
  tools: openAiTools,
@@ -8674,7 +9102,10 @@ async function runToolLoop(request) {
8674
9102
  stepToolCallText = serialiseLogArtifactText(stepToolCallPayload);
8675
9103
  if (responseToolCalls.length === 0) {
8676
9104
  const steeringInput2 = steeringInternal?.drainPendingContents() ?? [];
8677
- const steeringItems2 = steeringInput2.length > 0 ? toChatGptInput(steeringInput2).input : [];
9105
+ const steeringItems2 = steeringInput2.length > 0 ? toChatGptInput(steeringInput2, {
9106
+ defaultMediaResolution: request.mediaResolution,
9107
+ model: request.model
9108
+ }).input : [];
8678
9109
  finalText = responseText;
8679
9110
  finalThoughts = reasoningSummaryText;
8680
9111
  const stepCompletedAtMs2 = Date.now();
@@ -8806,7 +9237,10 @@ async function runToolLoop(request) {
8806
9237
  toolOutputs.push({
8807
9238
  type: "custom_tool_call_output",
8808
9239
  call_id: entry.ids.callId,
8809
- output: toChatGptToolOutput(outputPayload)
9240
+ output: toChatGptToolOutput(outputPayload, {
9241
+ defaultMediaResolution: request.mediaResolution,
9242
+ model: request.model
9243
+ })
8810
9244
  });
8811
9245
  } else {
8812
9246
  toolOutputs.push({
@@ -8820,7 +9254,10 @@ async function runToolLoop(request) {
8820
9254
  toolOutputs.push({
8821
9255
  type: "function_call_output",
8822
9256
  call_id: entry.ids.callId,
8823
- output: toChatGptToolOutput(outputPayload)
9257
+ output: toChatGptToolOutput(outputPayload, {
9258
+ defaultMediaResolution: request.mediaResolution,
9259
+ model: request.model
9260
+ })
8824
9261
  });
8825
9262
  }
8826
9263
  }
@@ -8844,7 +9281,10 @@ async function runToolLoop(request) {
8844
9281
  timing
8845
9282
  });
8846
9283
  const steeringInput = steeringInternal?.drainPendingContents() ?? [];
8847
- const steeringItems = steeringInput.length > 0 ? toChatGptInput(steeringInput).input : [];
9284
+ const steeringItems = steeringInput.length > 0 ? toChatGptInput(steeringInput, {
9285
+ defaultMediaResolution: request.mediaResolution,
9286
+ model: request.model
9287
+ }).input : [];
8848
9288
  stepCallLogger?.complete({
8849
9289
  responseText,
8850
9290
  toolCallText: stepToolCallText,
@@ -9175,7 +9615,11 @@ async function runToolLoop(request) {
9175
9615
  const geminiFunctionTools = buildGeminiFunctionDeclarations(request.tools);
9176
9616
  const geminiNativeTools = toGeminiTools(request.modelTools);
9177
9617
  const geminiTools = geminiNativeTools ? geminiNativeTools.concat(geminiFunctionTools) : geminiFunctionTools;
9178
- const geminiContents = contents.map(convertLlmContentToGeminiContent);
9618
+ const geminiContents = contents.map(
9619
+ (content) => convertLlmContentToGeminiContent(content, {
9620
+ defaultMediaResolution: request.mediaResolution
9621
+ })
9622
+ );
9179
9623
  for (let stepIndex = 0; stepIndex < maxSteps; stepIndex += 1) {
9180
9624
  const turn = stepIndex + 1;
9181
9625
  const stepStartedAtMs = Date.now();
@@ -9193,6 +9637,7 @@ async function runToolLoop(request) {
9193
9637
  }
9194
9638
  };
9195
9639
  const thinkingConfig = resolveGeminiThinkingConfig(request.model, request.thinkingLevel);
9640
+ const mediaResolution = toGeminiMediaResolution(request.mediaResolution);
9196
9641
  const config = {
9197
9642
  maxOutputTokens: 32e3,
9198
9643
  tools: geminiTools,
@@ -9201,7 +9646,8 @@ async function runToolLoop(request) {
9201
9646
  mode: import_genai2.FunctionCallingConfigMode.VALIDATED
9202
9647
  }
9203
9648
  },
9204
- ...thinkingConfig ? { thinkingConfig } : {}
9649
+ ...thinkingConfig ? { thinkingConfig } : {},
9650
+ ...mediaResolution ? { mediaResolution } : {}
9205
9651
  };
9206
9652
  const onEvent = request.onEvent;
9207
9653
  const preparedGeminiContents = await maybePrepareGeminiPromptContents(geminiContents);
@@ -9357,7 +9803,13 @@ async function runToolLoop(request) {
9357
9803
  } else if (response.responseText.length > 0) {
9358
9804
  geminiContents.push({ role: "model", parts: [{ text: response.responseText }] });
9359
9805
  }
9360
- geminiContents.push(...steeringInput2.map(convertLlmContentToGeminiContent));
9806
+ geminiContents.push(
9807
+ ...steeringInput2.map(
9808
+ (content) => convertLlmContentToGeminiContent(content, {
9809
+ defaultMediaResolution: request.mediaResolution
9810
+ })
9811
+ )
9812
+ );
9361
9813
  continue;
9362
9814
  }
9363
9815
  const toolCalls = [];
@@ -9449,7 +9901,8 @@ async function runToolLoop(request) {
9449
9901
  ...buildGeminiFunctionResponseParts({
9450
9902
  toolName: entry.toolName,
9451
9903
  callId: entry.call.id,
9452
- outputPayload
9904
+ outputPayload,
9905
+ defaultMediaResolution: request.mediaResolution
9453
9906
  })
9454
9907
  );
9455
9908
  }
@@ -9494,7 +9947,13 @@ async function runToolLoop(request) {
9494
9947
  geminiContents.push({ role: "user", parts: responseParts });
9495
9948
  const steeringInput = steeringInternal?.drainPendingContents() ?? [];
9496
9949
  if (steeringInput.length > 0) {
9497
- geminiContents.push(...steeringInput.map(convertLlmContentToGeminiContent));
9950
+ geminiContents.push(
9951
+ ...steeringInput.map(
9952
+ (content) => convertLlmContentToGeminiContent(content, {
9953
+ defaultMediaResolution: request.mediaResolution
9954
+ })
9955
+ )
9956
+ );
9498
9957
  }
9499
9958
  } catch (error) {
9500
9959
  stepCallLogger?.fail(error, {
@@ -9750,7 +10209,7 @@ async function generateImages(request) {
9750
10209
  }
9751
10210
  return image;
9752
10211
  })(),
9753
- model: "gpt-5.2"
10212
+ model: "gpt-5.4-mini"
9754
10213
  })
9755
10214
  )
9756
10215
  );
@@ -10056,7 +10515,6 @@ function resolveSubagentToolConfig(selection, currentDepth) {
10056
10515
  maxWaitTimeoutMs,
10057
10516
  promptPattern,
10058
10517
  ...instructions ? { instructions } : {},
10059
- ...config.model ? { model: config.model } : {},
10060
10518
  ...maxSteps ? { maxSteps } : {},
10061
10519
  inheritTools: config.inheritTools !== false,
10062
10520
  inheritFilesystemTool: config.inheritFilesystemTool !== false
@@ -10108,7 +10566,6 @@ function createSubagentToolController(options) {
10108
10566
  `Subagent depth limit reached (${options.config.maxDepth}). Cannot spawn at depth ${childDepth}.`
10109
10567
  );
10110
10568
  }
10111
- const model = options.config.model ?? options.parentModel;
10112
10569
  const id = `agent_${(0, import_node_crypto3.randomBytes)(6).toString("hex")}`;
10113
10570
  const now = Date.now();
10114
10571
  const { roleName, roleInstructions } = resolveAgentType(input.agent_type);
@@ -10128,7 +10585,7 @@ function createSubagentToolController(options) {
10128
10585
  const agent = {
10129
10586
  id,
10130
10587
  depth: childDepth,
10131
- model,
10588
+ model: options.parentModel,
10132
10589
  ...nickname ? { nickname } : {},
10133
10590
  agentRole: roleName,
10134
10591
  status: "idle",
@@ -11883,7 +12340,8 @@ async function viewImageCodex(input, options) {
11883
12340
  return [
11884
12341
  {
11885
12342
  type: "input_image",
11886
- image_url: `data:${mimeType};base64,${bytes.toString("base64")}`
12343
+ image_url: `data:${mimeType};base64,${bytes.toString("base64")}`,
12344
+ ...options.mediaResolution ? { detail: options.mediaResolution } : {}
11887
12345
  }
11888
12346
  ];
11889
12347
  }
@@ -12563,7 +13021,11 @@ async function runAgentLoopInternal(request, context) {
12563
13021
  const toolLoopRequestWithSteering = toolLoopRequest.steering === steeringChannel ? toolLoopRequest : { ...toolLoopRequest, steering: steeringChannel };
12564
13022
  const filesystemSelection = filesystemTool ?? filesystem_tool;
12565
13023
  const subagentSelection = subagentTool ?? subagent_tool ?? subagents;
12566
- const filesystemTools = resolveFilesystemTools(request.model, filesystemSelection);
13024
+ const filesystemTools = resolveFilesystemTools(
13025
+ request.model,
13026
+ filesystemSelection,
13027
+ request.mediaResolution
13028
+ );
12567
13029
  const resolvedSubagentConfig = resolveSubagentToolConfig(subagentSelection, context.depth);
12568
13030
  const subagentController = createSubagentController({
12569
13031
  runId,
@@ -12715,24 +13177,47 @@ async function runAgentLoopInternal(request, context) {
12715
13177
  await subagentController?.closeAll();
12716
13178
  }
12717
13179
  }
12718
- function resolveFilesystemTools(model, selection) {
13180
+ function resolveFilesystemTools(model, selection, defaultMediaResolution) {
13181
+ const withDefaultMediaResolution = (options) => {
13182
+ if (defaultMediaResolution === void 0) {
13183
+ return options;
13184
+ }
13185
+ return {
13186
+ mediaResolution: defaultMediaResolution,
13187
+ ...options ?? {}
13188
+ };
13189
+ };
12719
13190
  if (selection === void 0 || selection === false) {
12720
13191
  return {};
12721
13192
  }
12722
13193
  if (selection === true) {
12723
- return createFilesystemToolSetForModel(model, "auto");
13194
+ return createFilesystemToolSetForModel(model, withDefaultMediaResolution(void 0) ?? {});
12724
13195
  }
12725
13196
  if (typeof selection === "string") {
12726
- return createFilesystemToolSetForModel(model, selection);
13197
+ return createFilesystemToolSetForModel(model, selection, withDefaultMediaResolution(void 0));
12727
13198
  }
12728
13199
  if (selection.enabled === false) {
12729
13200
  return {};
12730
13201
  }
12731
13202
  if (selection.options && selection.profile !== void 0) {
12732
- return createFilesystemToolSetForModel(model, selection.profile, selection.options);
13203
+ return createFilesystemToolSetForModel(
13204
+ model,
13205
+ selection.profile,
13206
+ withDefaultMediaResolution(selection.options)
13207
+ );
12733
13208
  }
12734
13209
  if (selection.options) {
12735
- return createFilesystemToolSetForModel(model, selection.options);
13210
+ return createFilesystemToolSetForModel(
13211
+ model,
13212
+ withDefaultMediaResolution(selection.options) ?? {}
13213
+ );
13214
+ }
13215
+ if (defaultMediaResolution !== void 0) {
13216
+ return createFilesystemToolSetForModel(
13217
+ model,
13218
+ selection.profile ?? "auto",
13219
+ withDefaultMediaResolution(void 0)
13220
+ );
12736
13221
  }
12737
13222
  return createFilesystemToolSetForModel(model, selection.profile ?? "auto");
12738
13223
  }
@@ -12755,7 +13240,7 @@ function createSubagentController(params) {
12755
13240
  return createSubagentToolController({
12756
13241
  config: params.resolvedSubagentConfig,
12757
13242
  parentDepth: params.depth,
12758
- parentModel: params.resolvedSubagentConfig.model ?? params.model,
13243
+ parentModel: params.model,
12759
13244
  forkContextMessages: normalizeForkContextMessages(params.toolLoopRequest.input),
12760
13245
  onBackgroundMessage: (message) => {
12761
13246
  params.steering?.append({ role: "user", content: message });
@@ -12775,6 +13260,7 @@ function createSubagentController(params) {
12775
13260
  modelTools: params.toolLoopRequest.modelTools,
12776
13261
  maxSteps: subagentRequest.maxSteps,
12777
13262
  thinkingLevel: params.toolLoopRequest.thinkingLevel,
13263
+ mediaResolution: params.toolLoopRequest.mediaResolution,
12778
13264
  signal: subagentRequest.signal
12779
13265
  },
12780
13266
  {