@ljoukov/llm 4.0.12 → 4.1.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.js CHANGED
@@ -1,7 +1,8 @@
1
1
  // src/llm.ts
2
- import { Buffer as Buffer4 } from "buffer";
3
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
2
+ import { Buffer as Buffer5 } from "buffer";
3
+ import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
4
4
  import { randomBytes } from "crypto";
5
+ import path5 from "path";
5
6
  import {
6
7
  FinishReason,
7
8
  FunctionCallingConfigMode,
@@ -2248,6 +2249,14 @@ function normaliseConfigValue(value) {
2248
2249
  const trimmed = value.trim();
2249
2250
  return trimmed.length > 0 ? trimmed : void 0;
2250
2251
  }
2252
+ function resolveGeminiApiKey() {
2253
+ loadLocalEnv();
2254
+ const raw = process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY;
2255
+ return normaliseConfigValue(raw);
2256
+ }
2257
+ function getGeminiBackend() {
2258
+ return resolveGeminiApiKey() ? "api" : "vertex";
2259
+ }
2251
2260
  function configureGemini(options = {}) {
2252
2261
  const nextProjectId = normaliseConfigValue(options.projectId);
2253
2262
  const nextLocation = normaliseConfigValue(options.location);
@@ -2275,6 +2284,10 @@ function resolveLocation() {
2275
2284
  async function getGeminiClient() {
2276
2285
  if (!geminiClientState.clientPromise) {
2277
2286
  geminiClientState.clientPromise = Promise.resolve().then(() => {
2287
+ const apiKey = resolveGeminiApiKey();
2288
+ if (apiKey) {
2289
+ return new GoogleGenAI({ apiKey });
2290
+ }
2278
2291
  const projectId = resolveProjectId();
2279
2292
  const location = resolveLocation();
2280
2293
  const googleAuthOptions = getGoogleAuthOptions(CLOUD_PLATFORM_SCOPE);
@@ -3241,14 +3254,708 @@ function getCurrentAgentLoggingSession() {
3241
3254
  return loggingSessionStorage.getStore();
3242
3255
  }
3243
3256
 
3257
+ // src/files.ts
3258
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
3259
+ import { Buffer as Buffer4, File as NodeFile } from "buffer";
3260
+ import { createHash } from "crypto";
3261
+ import { createReadStream, createWriteStream, openAsBlob } from "fs";
3262
+ import { mkdir as mkdir2, mkdtemp, stat, unlink, writeFile as writeFile2 } from "fs/promises";
3263
+ import os3 from "os";
3264
+ import path4 from "path";
3265
+ import { Readable } from "stream";
3266
+ import { pipeline } from "stream/promises";
3267
+ import { Storage } from "@google-cloud/storage";
3268
+ import mime from "mime";
3269
+ var DEFAULT_FILE_TTL_SECONDS = 48 * 60 * 60;
3270
+ var OPENAI_FILE_CREATE_MAX_BYTES = 512 * 1024 * 1024;
3271
+ var OPENAI_UPLOAD_PART_MAX_BYTES = 64 * 1024 * 1024;
3272
+ var GEMINI_FILE_POLL_INTERVAL_MS = 1e3;
3273
+ var GEMINI_FILE_POLL_TIMEOUT_MS = 6e4;
3274
+ var FILES_TEMP_ROOT = path4.join(os3.tmpdir(), "ljoukov-llm-files");
3275
+ var filesState = getRuntimeSingleton(/* @__PURE__ */ Symbol.for("@ljoukov/llm.filesState"), () => ({
3276
+ metadataById: /* @__PURE__ */ new Map(),
3277
+ openAiUploadCacheByKey: /* @__PURE__ */ new Map(),
3278
+ materializedById: /* @__PURE__ */ new Map(),
3279
+ geminiMirrorById: /* @__PURE__ */ new Map(),
3280
+ vertexMirrorById: /* @__PURE__ */ new Map(),
3281
+ storageClient: void 0,
3282
+ geminiClientPromise: void 0
3283
+ }));
3284
+ var fileUploadScopeStorage = getRuntimeSingleton(
3285
+ /* @__PURE__ */ Symbol.for("@ljoukov/llm.fileUploadScopeStorage"),
3286
+ () => new AsyncLocalStorage2()
3287
+ );
3288
+ function summarizeUploadEvents(events) {
3289
+ let totalBytes = 0;
3290
+ let totalLatencyMs = 0;
3291
+ for (const event of events) {
3292
+ totalBytes += Math.max(0, event.bytes);
3293
+ totalLatencyMs += Math.max(0, event.durationMs);
3294
+ }
3295
+ return {
3296
+ count: events.length,
3297
+ totalBytes,
3298
+ totalLatencyMs,
3299
+ events: Array.from(events)
3300
+ };
3301
+ }
3302
+ function emptyFileUploadMetrics() {
3303
+ return summarizeUploadEvents([]);
3304
+ }
3305
+ function getCurrentFileUploadMetrics() {
3306
+ const collector = fileUploadScopeStorage.getStore()?.collectors.at(-1);
3307
+ return summarizeUploadEvents(collector?.events ?? []);
3308
+ }
3309
+ async function collectFileUploadMetrics(fn) {
3310
+ const parent = fileUploadScopeStorage.getStore();
3311
+ const collector = { events: [] };
3312
+ const scope = {
3313
+ collectors: [...parent?.collectors ?? [], collector],
3314
+ source: parent?.source
3315
+ };
3316
+ return await fileUploadScopeStorage.run(scope, async () => {
3317
+ const result = await fn();
3318
+ return {
3319
+ result,
3320
+ uploads: summarizeUploadEvents(collector.events)
3321
+ };
3322
+ });
3323
+ }
3324
+ async function runWithFileUploadSource(source, fn) {
3325
+ const parent = fileUploadScopeStorage.getStore();
3326
+ const scope = {
3327
+ collectors: parent?.collectors ?? [],
3328
+ source
3329
+ };
3330
+ return await fileUploadScopeStorage.run(scope, fn);
3331
+ }
3332
+ function formatUploadLogLine(event) {
3333
+ const parts = [
3334
+ "[upload]",
3335
+ `source=${event.source}`,
3336
+ `backend=${event.backend}`,
3337
+ `mode=${event.mode}`,
3338
+ `filename=${JSON.stringify(event.filename)}`,
3339
+ `bytes=${event.bytes.toString()}`,
3340
+ `durationMs=${event.durationMs.toString()}`
3341
+ ];
3342
+ if (event.mimeType) {
3343
+ parts.push(`mimeType=${event.mimeType}`);
3344
+ }
3345
+ if (event.fileId) {
3346
+ parts.push(`fileId=${event.fileId}`);
3347
+ }
3348
+ if (event.mirrorId) {
3349
+ parts.push(`mirrorId=${event.mirrorId}`);
3350
+ }
3351
+ if (event.fileUri) {
3352
+ parts.push(`fileUri=${JSON.stringify(event.fileUri)}`);
3353
+ }
3354
+ return parts.join(" ");
3355
+ }
3356
+ function recordUploadEvent(event) {
3357
+ const scope = fileUploadScopeStorage.getStore();
3358
+ const resolvedSource = event.source ?? scope?.source ?? (event.backend === "openai" ? "files_api" : "provider_mirror");
3359
+ const timestampedEvent = {
3360
+ ...event,
3361
+ source: resolvedSource,
3362
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3363
+ };
3364
+ for (const collector of scope?.collectors ?? []) {
3365
+ collector.events.push(timestampedEvent);
3366
+ }
3367
+ getCurrentAgentLoggingSession()?.logLine(formatUploadLogLine(timestampedEvent));
3368
+ }
3369
+ function normaliseFilename(filename, fallback = "attachment.bin") {
3370
+ const trimmed = filename?.trim();
3371
+ if (!trimmed) {
3372
+ return fallback;
3373
+ }
3374
+ const basename = path4.basename(trimmed);
3375
+ return basename.length > 0 ? basename : fallback;
3376
+ }
3377
+ function resolveMimeType(filename, explicitMimeType, fallback = "application/octet-stream") {
3378
+ const trimmed = explicitMimeType?.trim();
3379
+ if (trimmed) {
3380
+ return trimmed;
3381
+ }
3382
+ const inferred = mime.getType(filename);
3383
+ return typeof inferred === "string" && inferred.length > 0 ? inferred : fallback;
3384
+ }
3385
+ function toBuffer(data) {
3386
+ if (typeof data === "string") {
3387
+ return Buffer4.from(data, "utf8");
3388
+ }
3389
+ if (ArrayBuffer.isView(data)) {
3390
+ return Buffer4.from(data.buffer, data.byteOffset, data.byteLength);
3391
+ }
3392
+ return Buffer4.from(data);
3393
+ }
3394
+ function computeSha256Hex(buffer) {
3395
+ return createHash("sha256").update(buffer).digest("hex");
3396
+ }
3397
+ async function computeFileSha256Hex(filePath) {
3398
+ const hash = createHash("sha256");
3399
+ const stream = createReadStream(filePath);
3400
+ for await (const chunk of stream) {
3401
+ hash.update(chunk);
3402
+ }
3403
+ return hash.digest("hex");
3404
+ }
3405
+ function toStoredFile(file) {
3406
+ return {
3407
+ id: file.id,
3408
+ bytes: file.bytes,
3409
+ created_at: file.created_at,
3410
+ filename: file.filename,
3411
+ object: "file",
3412
+ purpose: file.purpose,
3413
+ status: file.status,
3414
+ expires_at: file.expires_at
3415
+ };
3416
+ }
3417
+ function buildCacheKey(filename, mimeType, sha256Hex) {
3418
+ return `${sha256Hex}\0${filename}\0${mimeType}`;
3419
+ }
3420
+ function isFresh(file) {
3421
+ if (!file.expires_at) {
3422
+ return true;
3423
+ }
3424
+ return file.expires_at * 1e3 > Date.now() + 3e4;
3425
+ }
3426
+ function recordMetadata(metadata) {
3427
+ filesState.metadataById.set(metadata.file.id, metadata);
3428
+ if (metadata.sha256Hex) {
3429
+ filesState.openAiUploadCacheByKey.set(
3430
+ buildCacheKey(
3431
+ metadata.filename,
3432
+ metadata.mimeType ?? "application/octet-stream",
3433
+ metadata.sha256Hex
3434
+ ),
3435
+ metadata
3436
+ );
3437
+ }
3438
+ return metadata;
3439
+ }
3440
+ async function uploadOpenAiFileFromBytes(params) {
3441
+ const cacheKey = buildCacheKey(params.filename, params.mimeType, params.sha256Hex);
3442
+ const cached = filesState.openAiUploadCacheByKey.get(cacheKey);
3443
+ if (cached && isFresh(cached.file)) {
3444
+ return cached;
3445
+ }
3446
+ const client = getOpenAiClient();
3447
+ const startedAtMs = Date.now();
3448
+ let uploaded;
3449
+ let mode;
3450
+ if (params.bytes.byteLength <= OPENAI_FILE_CREATE_MAX_BYTES) {
3451
+ mode = "files.create";
3452
+ uploaded = await client.files.create({
3453
+ file: new NodeFile([new Uint8Array(params.bytes)], params.filename, {
3454
+ type: params.mimeType
3455
+ }),
3456
+ purpose: params.purpose,
3457
+ expires_after: {
3458
+ anchor: "created_at",
3459
+ seconds: params.expiresAfterSeconds
3460
+ }
3461
+ });
3462
+ } else {
3463
+ mode = "uploads";
3464
+ const upload = await client.uploads.create({
3465
+ bytes: params.bytes.byteLength,
3466
+ filename: params.filename,
3467
+ mime_type: params.mimeType,
3468
+ purpose: params.purpose
3469
+ });
3470
+ const partIds = [];
3471
+ for (let offset = 0; offset < params.bytes.byteLength; offset += OPENAI_UPLOAD_PART_MAX_BYTES) {
3472
+ const chunk = params.bytes.subarray(
3473
+ offset,
3474
+ Math.min(offset + OPENAI_UPLOAD_PART_MAX_BYTES, params.bytes.byteLength)
3475
+ );
3476
+ const uploadPart = await client.uploads.parts.create(upload.id, {
3477
+ data: new NodeFile([new Uint8Array(chunk)], `${params.sha256Hex}.part`, {
3478
+ type: params.mimeType
3479
+ })
3480
+ });
3481
+ partIds.push(uploadPart.id);
3482
+ }
3483
+ const completed = await client.uploads.complete(upload.id, { part_ids: partIds });
3484
+ const fileId = completed.file?.id;
3485
+ if (!fileId) {
3486
+ throw new Error("OpenAI upload completed without a file id.");
3487
+ }
3488
+ uploaded = await client.files.retrieve(fileId);
3489
+ }
3490
+ const file = toStoredFile(uploaded);
3491
+ const metadata = recordMetadata({
3492
+ file,
3493
+ filename: file.filename,
3494
+ bytes: file.bytes,
3495
+ mimeType: params.mimeType,
3496
+ sha256Hex: params.sha256Hex
3497
+ });
3498
+ recordUploadEvent({
3499
+ backend: "openai",
3500
+ mode,
3501
+ filename: metadata.filename,
3502
+ bytes: metadata.bytes,
3503
+ durationMs: Math.max(0, Date.now() - startedAtMs),
3504
+ mimeType: params.mimeType,
3505
+ fileId: metadata.file.id
3506
+ });
3507
+ return metadata;
3508
+ }
3509
+ async function uploadOpenAiFileFromPath(params) {
3510
+ const cacheKey = buildCacheKey(params.filename, params.mimeType, params.sha256Hex);
3511
+ const cached = filesState.openAiUploadCacheByKey.get(cacheKey);
3512
+ if (cached && isFresh(cached.file)) {
3513
+ return cached;
3514
+ }
3515
+ const client = getOpenAiClient();
3516
+ const startedAtMs = Date.now();
3517
+ let uploaded;
3518
+ let mode;
3519
+ if (params.bytes <= OPENAI_FILE_CREATE_MAX_BYTES) {
3520
+ mode = "files.create";
3521
+ const blob = await openAsBlob(params.filePath, { type: params.mimeType });
3522
+ uploaded = await client.files.create({
3523
+ file: new NodeFile([blob], params.filename, { type: params.mimeType }),
3524
+ purpose: params.purpose,
3525
+ expires_after: {
3526
+ anchor: "created_at",
3527
+ seconds: params.expiresAfterSeconds
3528
+ }
3529
+ });
3530
+ } else {
3531
+ mode = "uploads";
3532
+ const upload = await client.uploads.create({
3533
+ bytes: params.bytes,
3534
+ filename: params.filename,
3535
+ mime_type: params.mimeType,
3536
+ purpose: params.purpose
3537
+ });
3538
+ const partIds = [];
3539
+ const stream = createReadStream(params.filePath, {
3540
+ highWaterMark: OPENAI_UPLOAD_PART_MAX_BYTES
3541
+ });
3542
+ let partIndex = 0;
3543
+ for await (const chunk of stream) {
3544
+ const buffer = Buffer4.isBuffer(chunk) ? chunk : Buffer4.from(chunk);
3545
+ const uploadPart = await client.uploads.parts.create(upload.id, {
3546
+ data: new NodeFile(
3547
+ [new Uint8Array(buffer)],
3548
+ `${params.sha256Hex}.${partIndex.toString()}.part`,
3549
+ {
3550
+ type: params.mimeType
3551
+ }
3552
+ )
3553
+ });
3554
+ partIds.push(uploadPart.id);
3555
+ partIndex += 1;
3556
+ }
3557
+ const completed = await client.uploads.complete(upload.id, { part_ids: partIds });
3558
+ const fileId = completed.file?.id;
3559
+ if (!fileId) {
3560
+ throw new Error("OpenAI upload completed without a file id.");
3561
+ }
3562
+ uploaded = await client.files.retrieve(fileId);
3563
+ }
3564
+ const file = toStoredFile(uploaded);
3565
+ const metadata = recordMetadata({
3566
+ file,
3567
+ filename: file.filename,
3568
+ bytes: file.bytes,
3569
+ mimeType: params.mimeType,
3570
+ sha256Hex: params.sha256Hex
3571
+ });
3572
+ recordUploadEvent({
3573
+ backend: "openai",
3574
+ mode,
3575
+ filename: metadata.filename,
3576
+ bytes: metadata.bytes,
3577
+ durationMs: Math.max(0, Date.now() - startedAtMs),
3578
+ mimeType: params.mimeType,
3579
+ fileId: metadata.file.id
3580
+ });
3581
+ return metadata;
3582
+ }
3583
+ async function retrieveOpenAiFile(fileId) {
3584
+ const cached = filesState.metadataById.get(fileId);
3585
+ if (cached && isFresh(cached.file)) {
3586
+ return cached;
3587
+ }
3588
+ const client = getOpenAiClient();
3589
+ const retrieved = await client.files.retrieve(fileId);
3590
+ const file = toStoredFile(retrieved);
3591
+ return recordMetadata({
3592
+ file,
3593
+ filename: file.filename,
3594
+ bytes: file.bytes,
3595
+ mimeType: cached?.mimeType ?? resolveMimeType(file.filename, void 0),
3596
+ sha256Hex: cached?.sha256Hex,
3597
+ localPath: cached?.localPath
3598
+ });
3599
+ }
3600
+ function buildGeminiMirrorName(sha256Hex) {
3601
+ return `files/${sha256Hex.slice(0, 40)}`;
3602
+ }
3603
+ async function waitForGeminiFileActive(client, name) {
3604
+ const startedAt = Date.now();
3605
+ while (true) {
3606
+ const file = await client.files.get({ name });
3607
+ if (!file.state || file.state === "ACTIVE") {
3608
+ return;
3609
+ }
3610
+ if (file.state === "FAILED") {
3611
+ throw new Error(file.error?.message ?? `Gemini file ${name} failed processing.`);
3612
+ }
3613
+ if (Date.now() - startedAt >= GEMINI_FILE_POLL_TIMEOUT_MS) {
3614
+ throw new Error(`Timed out waiting for Gemini file ${name} to become active.`);
3615
+ }
3616
+ await new Promise((resolve) => setTimeout(resolve, GEMINI_FILE_POLL_INTERVAL_MS));
3617
+ }
3618
+ }
3619
+ function resolveVertexMirrorBucket() {
3620
+ const raw = process.env.VERTEX_GCS_BUCKET ?? process.env.LLM_VERTEX_GCS_BUCKET;
3621
+ const trimmed = raw?.trim();
3622
+ if (!trimmed) {
3623
+ throw new Error(
3624
+ "VERTEX_GCS_BUCKET must be set to use OpenAI-backed file ids with Vertex Gemini models."
3625
+ );
3626
+ }
3627
+ return trimmed.replace(/^gs:\/\//u, "").replace(/\/+$/u, "");
3628
+ }
3629
+ function resolveVertexMirrorPrefix() {
3630
+ const raw = process.env.VERTEX_GCS_PREFIX ?? process.env.LLM_VERTEX_GCS_PREFIX;
3631
+ const trimmed = raw?.trim().replace(/^\/+/u, "").replace(/\/+$/u, "");
3632
+ return trimmed ? `${trimmed}/` : "";
3633
+ }
3634
+ function getStorageClient() {
3635
+ if (filesState.storageClient) {
3636
+ return filesState.storageClient;
3637
+ }
3638
+ const serviceAccount = getGoogleServiceAccount();
3639
+ filesState.storageClient = new Storage({
3640
+ projectId: serviceAccount.projectId,
3641
+ credentials: {
3642
+ client_email: serviceAccount.clientEmail,
3643
+ private_key: serviceAccount.privateKey
3644
+ }
3645
+ });
3646
+ return filesState.storageClient;
3647
+ }
3648
+ function getGeminiMirrorClient() {
3649
+ if (!filesState.geminiClientPromise) {
3650
+ filesState.geminiClientPromise = getGeminiClient();
3651
+ }
3652
+ return filesState.geminiClientPromise;
3653
+ }
3654
+ async function materializeOpenAiFile(fileId) {
3655
+ const cachedPromise = filesState.materializedById.get(fileId);
3656
+ if (cachedPromise) {
3657
+ return await cachedPromise;
3658
+ }
3659
+ const promise = (async () => {
3660
+ const metadata = await retrieveOpenAiFile(fileId);
3661
+ if (metadata.localPath && metadata.sha256Hex && metadata.mimeType) {
3662
+ return {
3663
+ file: metadata.file,
3664
+ filename: metadata.filename,
3665
+ bytes: metadata.bytes,
3666
+ mimeType: metadata.mimeType,
3667
+ sha256Hex: metadata.sha256Hex,
3668
+ localPath: metadata.localPath
3669
+ };
3670
+ }
3671
+ await mkdir2(FILES_TEMP_ROOT, { recursive: true });
3672
+ const tempDir = await mkdtemp(
3673
+ path4.join(FILES_TEMP_ROOT, `${fileId.replace(/[^a-z0-9_-]/giu, "")}-`)
3674
+ );
3675
+ const localPath = path4.join(tempDir, normaliseFilename(metadata.filename, `${fileId}.bin`));
3676
+ const response = await getOpenAiClient().files.content(fileId);
3677
+ if (!response.ok) {
3678
+ throw new Error(
3679
+ `Failed to download OpenAI file ${fileId}: ${response.status} ${response.statusText}`
3680
+ );
3681
+ }
3682
+ const responseMimeType = response.headers.get("content-type")?.trim() || void 0;
3683
+ const mimeType = resolveMimeType(metadata.filename, responseMimeType);
3684
+ const hash = createHash("sha256");
3685
+ let bytes = 0;
3686
+ if (response.body) {
3687
+ const source = Readable.fromWeb(response.body);
3688
+ const writable = createWriteStream(localPath, { flags: "wx" });
3689
+ source.on("data", (chunk) => {
3690
+ const buffer = Buffer4.isBuffer(chunk) ? chunk : Buffer4.from(chunk);
3691
+ hash.update(buffer);
3692
+ bytes += buffer.byteLength;
3693
+ });
3694
+ await pipeline(source, writable);
3695
+ } else {
3696
+ const buffer = Buffer4.from(await response.arrayBuffer());
3697
+ hash.update(buffer);
3698
+ bytes = buffer.byteLength;
3699
+ await writeFile2(localPath, buffer);
3700
+ }
3701
+ const sha256Hex = hash.digest("hex");
3702
+ const updated = recordMetadata({
3703
+ file: metadata.file,
3704
+ filename: metadata.filename,
3705
+ bytes: bytes || metadata.bytes,
3706
+ mimeType,
3707
+ sha256Hex,
3708
+ localPath
3709
+ });
3710
+ return {
3711
+ file: updated.file,
3712
+ filename: updated.filename,
3713
+ bytes: updated.bytes,
3714
+ mimeType: updated.mimeType ?? mimeType,
3715
+ sha256Hex,
3716
+ localPath
3717
+ };
3718
+ })();
3719
+ filesState.materializedById.set(fileId, promise);
3720
+ try {
3721
+ return await promise;
3722
+ } catch (error) {
3723
+ filesState.materializedById.delete(fileId);
3724
+ throw error;
3725
+ }
3726
+ }
3727
+ async function ensureGeminiFileMirror(fileId) {
3728
+ const cached = filesState.geminiMirrorById.get(fileId);
3729
+ if (cached) {
3730
+ return cached;
3731
+ }
3732
+ const materialized = await materializeOpenAiFile(fileId);
3733
+ const client = await getGeminiMirrorClient();
3734
+ const name = buildGeminiMirrorName(materialized.sha256Hex);
3735
+ try {
3736
+ const existing = await client.files.get({ name });
3737
+ if (existing.name && existing.uri && existing.mimeType) {
3738
+ const mirror2 = {
3739
+ openAiFileId: fileId,
3740
+ name: existing.name,
3741
+ uri: existing.uri,
3742
+ mimeType: existing.mimeType,
3743
+ displayName: existing.displayName ?? materialized.filename
3744
+ };
3745
+ filesState.geminiMirrorById.set(fileId, mirror2);
3746
+ return mirror2;
3747
+ }
3748
+ } catch {
3749
+ }
3750
+ const startedAtMs = Date.now();
3751
+ const uploaded = await client.files.upload({
3752
+ file: materialized.localPath,
3753
+ config: {
3754
+ name,
3755
+ mimeType: materialized.mimeType,
3756
+ displayName: materialized.filename
3757
+ }
3758
+ });
3759
+ if (uploaded.name && uploaded.state && uploaded.state !== "ACTIVE") {
3760
+ await waitForGeminiFileActive(client, uploaded.name);
3761
+ }
3762
+ const resolved = await client.files.get({ name: uploaded.name ?? name });
3763
+ if (!resolved.name || !resolved.uri || !resolved.mimeType) {
3764
+ throw new Error("Gemini file upload completed without a usable URI.");
3765
+ }
3766
+ const mirror = {
3767
+ openAiFileId: fileId,
3768
+ name: resolved.name,
3769
+ uri: resolved.uri,
3770
+ mimeType: resolved.mimeType,
3771
+ displayName: resolved.displayName ?? materialized.filename
3772
+ };
3773
+ filesState.geminiMirrorById.set(fileId, mirror);
3774
+ recordUploadEvent({
3775
+ backend: "gemini",
3776
+ mode: "mirror",
3777
+ filename: materialized.filename,
3778
+ bytes: materialized.bytes,
3779
+ durationMs: Math.max(0, Date.now() - startedAtMs),
3780
+ mimeType: materialized.mimeType,
3781
+ fileId,
3782
+ mirrorId: mirror.name,
3783
+ fileUri: mirror.uri
3784
+ });
3785
+ return mirror;
3786
+ }
3787
+ async function ensureVertexFileMirror(fileId) {
3788
+ const cached = filesState.vertexMirrorById.get(fileId);
3789
+ if (cached) {
3790
+ return cached;
3791
+ }
3792
+ const materialized = await materializeOpenAiFile(fileId);
3793
+ const bucketName = resolveVertexMirrorBucket();
3794
+ const prefix = resolveVertexMirrorPrefix();
3795
+ const extension = mime.getExtension(materialized.mimeType) ?? path4.extname(materialized.filename).replace(/^\./u, "") ?? "bin";
3796
+ const objectName = `${prefix}${materialized.sha256Hex}.${extension}`;
3797
+ const file = getStorageClient().bucket(bucketName).file(objectName);
3798
+ let uploaded = false;
3799
+ const startedAtMs = Date.now();
3800
+ try {
3801
+ await file.getMetadata();
3802
+ } catch (error) {
3803
+ const code = error.code;
3804
+ if (code !== 404 && code !== "404") {
3805
+ throw error;
3806
+ }
3807
+ try {
3808
+ await pipeline(
3809
+ createReadStream(materialized.localPath),
3810
+ file.createWriteStream({
3811
+ resumable: materialized.bytes >= 10 * 1024 * 1024,
3812
+ preconditionOpts: { ifGenerationMatch: 0 },
3813
+ metadata: {
3814
+ contentType: materialized.mimeType,
3815
+ customTime: (/* @__PURE__ */ new Date()).toISOString(),
3816
+ metadata: {
3817
+ filename: materialized.filename,
3818
+ sha256: materialized.sha256Hex,
3819
+ expiresAt: new Date(Date.now() + DEFAULT_FILE_TTL_SECONDS * 1e3).toISOString()
3820
+ }
3821
+ }
3822
+ })
3823
+ );
3824
+ uploaded = true;
3825
+ } catch (uploadError) {
3826
+ const uploadCode = uploadError.code;
3827
+ if (uploadCode !== 412 && uploadCode !== "412") {
3828
+ throw uploadError;
3829
+ }
3830
+ }
3831
+ }
3832
+ const mirror = {
3833
+ openAiFileId: fileId,
3834
+ bucket: bucketName,
3835
+ objectName,
3836
+ fileUri: `gs://${bucketName}/${objectName}`,
3837
+ mimeType: materialized.mimeType
3838
+ };
3839
+ filesState.vertexMirrorById.set(fileId, mirror);
3840
+ if (uploaded) {
3841
+ recordUploadEvent({
3842
+ backend: "vertex",
3843
+ mode: "mirror",
3844
+ filename: materialized.filename,
3845
+ bytes: materialized.bytes,
3846
+ durationMs: Math.max(0, Date.now() - startedAtMs),
3847
+ mimeType: materialized.mimeType,
3848
+ fileId,
3849
+ mirrorId: mirror.objectName,
3850
+ fileUri: mirror.fileUri
3851
+ });
3852
+ }
3853
+ return mirror;
3854
+ }
3855
+ async function filesCreate(params) {
3856
+ const purpose = params.purpose ?? "user_data";
3857
+ const expiresAfterSeconds = params.expiresAfterSeconds ?? DEFAULT_FILE_TTL_SECONDS;
3858
+ if ("path" in params) {
3859
+ const filePath = path4.resolve(params.path);
3860
+ const info = await stat(filePath);
3861
+ const filename2 = normaliseFilename(params.filename, path4.basename(filePath));
3862
+ const mimeType2 = resolveMimeType(filename2, params.mimeType);
3863
+ const sha256Hex2 = await computeFileSha256Hex(filePath);
3864
+ const uploaded2 = await uploadOpenAiFileFromPath({
3865
+ filePath,
3866
+ filename: filename2,
3867
+ mimeType: mimeType2,
3868
+ purpose,
3869
+ expiresAfterSeconds,
3870
+ sha256Hex: sha256Hex2,
3871
+ bytes: info.size
3872
+ });
3873
+ return uploaded2.file;
3874
+ }
3875
+ const filename = normaliseFilename(params.filename);
3876
+ const bytes = toBuffer(params.data);
3877
+ const mimeType = resolveMimeType(filename, params.mimeType, "text/plain");
3878
+ const sha256Hex = computeSha256Hex(bytes);
3879
+ const uploaded = await uploadOpenAiFileFromBytes({
3880
+ bytes,
3881
+ filename,
3882
+ mimeType,
3883
+ purpose,
3884
+ expiresAfterSeconds,
3885
+ sha256Hex
3886
+ });
3887
+ return uploaded.file;
3888
+ }
3889
+ async function filesRetrieve(fileId) {
3890
+ return (await retrieveOpenAiFile(fileId)).file;
3891
+ }
3892
+ async function filesDelete(fileId) {
3893
+ const cachedGemini = filesState.geminiMirrorById.get(fileId);
3894
+ if (cachedGemini) {
3895
+ try {
3896
+ const client = await getGeminiMirrorClient();
3897
+ await client.files.delete({ name: cachedGemini.name });
3898
+ } catch {
3899
+ }
3900
+ filesState.geminiMirrorById.delete(fileId);
3901
+ }
3902
+ const cachedVertex = filesState.vertexMirrorById.get(fileId);
3903
+ if (cachedVertex) {
3904
+ try {
3905
+ await getStorageClient().bucket(cachedVertex.bucket).file(cachedVertex.objectName).delete({ ignoreNotFound: true });
3906
+ } catch {
3907
+ }
3908
+ filesState.vertexMirrorById.delete(fileId);
3909
+ }
3910
+ const cachedMaterialized = filesState.metadataById.get(fileId)?.localPath;
3911
+ if (cachedMaterialized) {
3912
+ try {
3913
+ await unlink(cachedMaterialized);
3914
+ } catch {
3915
+ }
3916
+ }
3917
+ const response = await getOpenAiClient().files.delete(fileId);
3918
+ filesState.metadataById.delete(fileId);
3919
+ filesState.materializedById.delete(fileId);
3920
+ return {
3921
+ id: response.id,
3922
+ deleted: response.deleted,
3923
+ object: "file"
3924
+ };
3925
+ }
3926
+ async function filesContent(fileId) {
3927
+ return await getOpenAiClient().files.content(fileId);
3928
+ }
3929
+ async function getCanonicalFileMetadata(fileId) {
3930
+ const metadata = await retrieveOpenAiFile(fileId);
3931
+ const mimeType = metadata.mimeType ?? resolveMimeType(metadata.filename, void 0);
3932
+ const updated = metadata.mimeType === mimeType ? metadata : recordMetadata({
3933
+ ...metadata,
3934
+ mimeType
3935
+ });
3936
+ return {
3937
+ ...updated,
3938
+ mimeType
3939
+ };
3940
+ }
3941
+ var files = {
3942
+ create: filesCreate,
3943
+ retrieve: filesRetrieve,
3944
+ delete: filesDelete,
3945
+ content: filesContent
3946
+ };
3947
+
3244
3948
  // src/llm.ts
3245
3949
  var toolCallContextStorage = getRuntimeSingleton(
3246
3950
  /* @__PURE__ */ Symbol.for("@ljoukov/llm.toolCallContextStorage"),
3247
- () => new AsyncLocalStorage2()
3951
+ () => new AsyncLocalStorage3()
3248
3952
  );
3249
3953
  function getCurrentToolCallContext() {
3250
3954
  return toolCallContextStorage.getStore() ?? null;
3251
3955
  }
3956
+ var INLINE_ATTACHMENT_FILENAME_SYMBOL = /* @__PURE__ */ Symbol.for("@ljoukov/llm.inlineAttachmentFilename");
3957
+ var INLINE_ATTACHMENT_PROMPT_THRESHOLD_BYTES = 20 * 1024 * 1024;
3958
+ var TOOL_OUTPUT_SPILL_THRESHOLD_BYTES = 1 * 1024 * 1024;
3252
3959
  var LLM_TEXT_MODEL_IDS = [
3253
3960
  ...OPENAI_MODEL_IDS,
3254
3961
  ...CHATGPT_MODEL_IDS,
@@ -3526,6 +4233,52 @@ function isJsonSchemaObject(schema) {
3526
4233
  }
3527
4234
  return false;
3528
4235
  }
4236
+ var CANONICAL_GEMINI_FILE_URI_PREFIX = "openai://file/";
4237
+ function buildCanonicalGeminiFileUri(fileId) {
4238
+ return `${CANONICAL_GEMINI_FILE_URI_PREFIX}${fileId}`;
4239
+ }
4240
+ function parseCanonicalGeminiFileId(fileUri) {
4241
+ if (!fileUri?.startsWith(CANONICAL_GEMINI_FILE_URI_PREFIX)) {
4242
+ return void 0;
4243
+ }
4244
+ const fileId = fileUri.slice(CANONICAL_GEMINI_FILE_URI_PREFIX.length).trim();
4245
+ return fileId.length > 0 ? fileId : void 0;
4246
+ }
4247
+ function cloneContentPart(part) {
4248
+ switch (part.type) {
4249
+ case "text":
4250
+ return {
4251
+ type: "text",
4252
+ text: part.text,
4253
+ thought: part.thought === true ? true : void 0
4254
+ };
4255
+ case "inlineData":
4256
+ return {
4257
+ type: "inlineData",
4258
+ data: part.data,
4259
+ mimeType: part.mimeType,
4260
+ filename: part.filename
4261
+ };
4262
+ case "input_image":
4263
+ return {
4264
+ type: "input_image",
4265
+ image_url: part.image_url ?? void 0,
4266
+ file_id: part.file_id ?? void 0,
4267
+ detail: part.detail,
4268
+ filename: part.filename ?? void 0
4269
+ };
4270
+ case "input_file":
4271
+ return {
4272
+ type: "input_file",
4273
+ file_data: part.file_data ?? void 0,
4274
+ file_id: part.file_id ?? void 0,
4275
+ file_url: part.file_url ?? void 0,
4276
+ filename: part.filename ?? void 0
4277
+ };
4278
+ default:
4279
+ return part;
4280
+ }
4281
+ }
3529
4282
  function sanitisePartForLogging(part) {
3530
4283
  switch (part.type) {
3531
4284
  case "text":
@@ -3537,16 +4290,33 @@ function sanitisePartForLogging(part) {
3537
4290
  case "inlineData": {
3538
4291
  let omittedBytes;
3539
4292
  try {
3540
- omittedBytes = Buffer4.from(part.data, "base64").byteLength;
4293
+ omittedBytes = Buffer5.from(part.data, "base64").byteLength;
3541
4294
  } catch {
3542
- omittedBytes = Buffer4.byteLength(part.data, "utf8");
4295
+ omittedBytes = Buffer5.byteLength(part.data, "utf8");
3543
4296
  }
3544
4297
  return {
3545
4298
  type: "inlineData",
3546
4299
  mimeType: part.mimeType,
4300
+ filename: part.filename,
3547
4301
  data: `[omitted:${omittedBytes}b]`
3548
4302
  };
3549
4303
  }
4304
+ case "input_image":
4305
+ return {
4306
+ type: "input_image",
4307
+ file_id: part.file_id ?? void 0,
4308
+ filename: part.filename ?? void 0,
4309
+ detail: part.detail ?? void 0,
4310
+ image_url: typeof part.image_url === "string" ? part.image_url.startsWith("data:") ? "[omitted:data-url]" : part.image_url : void 0
4311
+ };
4312
+ case "input_file":
4313
+ return {
4314
+ type: "input_file",
4315
+ file_id: part.file_id ?? void 0,
4316
+ filename: part.filename ?? void 0,
4317
+ file_url: typeof part.file_url === "string" ? part.file_url.startsWith("data:") ? "[omitted:data-url]" : part.file_url : void 0,
4318
+ file_data: typeof part.file_data === "string" ? `[omitted:${Buffer5.byteLength(part.file_data, "utf8")}b]` : void 0
4319
+ };
3550
4320
  default:
3551
4321
  return "[unknown part]";
3552
4322
  }
@@ -3567,12 +4337,17 @@ function convertGooglePartsToLlmParts(parts) {
3567
4337
  result.push({
3568
4338
  type: "inlineData",
3569
4339
  data: inline.data,
3570
- mimeType: inline.mimeType
4340
+ mimeType: inline.mimeType,
4341
+ filename: inline.displayName
3571
4342
  });
3572
4343
  continue;
3573
4344
  }
3574
4345
  if (part.fileData?.fileUri) {
3575
- throw new Error("fileData parts are not supported");
4346
+ result.push({
4347
+ type: "input_file",
4348
+ file_url: part.fileData.fileUri,
4349
+ filename: part.fileData.displayName
4350
+ });
3576
4351
  }
3577
4352
  }
3578
4353
  return result;
@@ -3604,13 +4379,86 @@ function toGeminiPart(part) {
3604
4379
  text: part.text,
3605
4380
  thought: part.thought === true ? true : void 0
3606
4381
  };
3607
- case "inlineData":
4382
+ case "inlineData": {
4383
+ const inlineData = {
4384
+ data: part.data,
4385
+ mimeType: part.mimeType
4386
+ };
4387
+ setInlineAttachmentFilename(inlineData, part.filename);
3608
4388
  return {
3609
4389
  inlineData: {
3610
- data: part.data,
3611
- mimeType: part.mimeType
4390
+ ...inlineData
4391
+ }
4392
+ };
4393
+ }
4394
+ case "input_image": {
4395
+ if (part.file_id) {
4396
+ return {
4397
+ fileData: {
4398
+ fileUri: buildCanonicalGeminiFileUri(part.file_id),
4399
+ mimeType: inferToolOutputMimeTypeFromFilename(part.filename) ?? "application/octet-stream",
4400
+ displayName: part.filename ?? void 0
4401
+ }
4402
+ };
4403
+ }
4404
+ if (typeof part.image_url !== "string" || part.image_url.trim().length === 0) {
4405
+ throw new Error("input_image requires image_url or file_id.");
4406
+ }
4407
+ const parsed = parseDataUrlPayload(part.image_url);
4408
+ if (parsed) {
4409
+ const geminiPart = createPartFromBase64(parsed.dataBase64, parsed.mimeType);
4410
+ if (part.filename && geminiPart.inlineData) {
4411
+ geminiPart.inlineData.displayName = part.filename;
4412
+ }
4413
+ return geminiPart;
4414
+ }
4415
+ return {
4416
+ fileData: {
4417
+ fileUri: part.image_url,
4418
+ mimeType: inferToolOutputMimeTypeFromFilename(part.filename) ?? "application/octet-stream",
4419
+ displayName: part.filename ?? void 0
3612
4420
  }
3613
4421
  };
4422
+ }
4423
+ case "input_file": {
4424
+ if (part.file_id) {
4425
+ return {
4426
+ fileData: {
4427
+ fileUri: buildCanonicalGeminiFileUri(part.file_id),
4428
+ mimeType: inferToolOutputMimeTypeFromFilename(part.filename) ?? "application/octet-stream",
4429
+ displayName: part.filename ?? void 0
4430
+ }
4431
+ };
4432
+ }
4433
+ if (typeof part.file_data === "string" && part.file_data.trim().length > 0) {
4434
+ const geminiPart = createPartFromBase64(
4435
+ part.file_data,
4436
+ inferToolOutputMimeTypeFromFilename(part.filename) ?? "application/octet-stream"
4437
+ );
4438
+ if (part.filename && geminiPart.inlineData) {
4439
+ geminiPart.inlineData.displayName = part.filename;
4440
+ }
4441
+ return geminiPart;
4442
+ }
4443
+ if (typeof part.file_url === "string" && part.file_url.trim().length > 0) {
4444
+ const parsed = parseDataUrlPayload(part.file_url);
4445
+ if (parsed) {
4446
+ const geminiPart = createPartFromBase64(parsed.dataBase64, parsed.mimeType);
4447
+ if (part.filename && geminiPart.inlineData) {
4448
+ geminiPart.inlineData.displayName = part.filename;
4449
+ }
4450
+ return geminiPart;
4451
+ }
4452
+ return {
4453
+ fileData: {
4454
+ fileUri: part.file_url,
4455
+ mimeType: inferToolOutputMimeTypeFromFilename(part.filename) ?? "application/octet-stream",
4456
+ displayName: part.filename ?? void 0
4457
+ }
4458
+ };
4459
+ }
4460
+ throw new Error("input_file requires file_id, file_data, or file_url.");
4461
+ }
3614
4462
  default:
3615
4463
  throw new Error("Unsupported LLM content part");
3616
4464
  }
@@ -3707,6 +4555,14 @@ function isInlineImageMime(mimeType) {
3707
4555
  }
3708
4556
  function guessInlineDataFilename(mimeType) {
3709
4557
  switch (mimeType) {
4558
+ case "image/jpeg":
4559
+ return "image.jpg";
4560
+ case "image/png":
4561
+ return "image.png";
4562
+ case "image/webp":
4563
+ return "image.webp";
4564
+ case "image/gif":
4565
+ return "image.gif";
3710
4566
  case "application/pdf":
3711
4567
  return "document.pdf";
3712
4568
  case "application/json":
@@ -3719,14 +4575,269 @@ function guessInlineDataFilename(mimeType) {
3719
4575
  return "attachment.bin";
3720
4576
  }
3721
4577
  }
3722
- function mergeConsecutiveTextParts(parts) {
4578
+ function normaliseAttachmentFilename(value, fallback) {
4579
+ const trimmed = value?.trim();
4580
+ if (!trimmed) {
4581
+ return fallback;
4582
+ }
4583
+ const basename = path5.basename(trimmed).replace(/[^\w.-]+/g, "-");
4584
+ return basename.length > 0 ? basename : fallback;
4585
+ }
4586
+ function setInlineAttachmentFilename(target, filename) {
4587
+ const normalized = filename?.trim();
4588
+ if (!normalized) {
4589
+ return;
4590
+ }
4591
+ target[INLINE_ATTACHMENT_FILENAME_SYMBOL] = normalized;
4592
+ }
4593
+ function getInlineAttachmentFilename(target) {
4594
+ if (!target || typeof target !== "object") {
4595
+ return void 0;
4596
+ }
4597
+ const value = target[INLINE_ATTACHMENT_FILENAME_SYMBOL];
4598
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
4599
+ }
4600
+ function estimateInlinePayloadBytes(value) {
4601
+ return Buffer5.byteLength(value, "utf8");
4602
+ }
4603
+ function isOpenAiNativeContentItem(value) {
4604
+ return !!value && typeof value === "object" && typeof value.type === "string";
4605
+ }
4606
+ function estimateOpenAiInlinePromptBytes(input) {
4607
+ let total = 0;
4608
+ const visitItems = (items) => {
4609
+ for (const item of items) {
4610
+ if (!item || typeof item !== "object") {
4611
+ continue;
4612
+ }
4613
+ if (Array.isArray(item.content)) {
4614
+ visitItems(item.content);
4615
+ }
4616
+ if (Array.isArray(item.output)) {
4617
+ visitItems(item.output);
4618
+ }
4619
+ if (!isOpenAiNativeContentItem(item)) {
4620
+ continue;
4621
+ }
4622
+ if (item.type === "input_image" && typeof item.image_url === "string" && item.image_url.trim().toLowerCase().startsWith("data:")) {
4623
+ total += estimateInlinePayloadBytes(item.image_url);
4624
+ }
4625
+ if (item.type === "input_file" && typeof item.file_data === "string" && item.file_data.trim().length > 0) {
4626
+ total += estimateInlinePayloadBytes(item.file_data);
4627
+ }
4628
+ if (item.type === "input_file" && typeof item.file_url === "string" && item.file_url.trim().toLowerCase().startsWith("data:")) {
4629
+ total += estimateInlinePayloadBytes(item.file_url);
4630
+ }
4631
+ }
4632
+ };
4633
+ visitItems(input);
4634
+ return total;
4635
+ }
4636
+ async function storeCanonicalPromptFile(options) {
4637
+ const file = await runWithFileUploadSource("prompt_inline_offload", async () => {
4638
+ return await filesCreate({
4639
+ data: options.bytes,
4640
+ filename: options.filename,
4641
+ mimeType: options.mimeType,
4642
+ expiresAfterSeconds: DEFAULT_FILE_TTL_SECONDS
4643
+ });
4644
+ });
4645
+ return {
4646
+ fileId: file.id,
4647
+ filename: file.filename,
4648
+ mimeType: options.mimeType
4649
+ };
4650
+ }
4651
+ async function prepareOpenAiPromptContentItem(item) {
4652
+ if (!isOpenAiNativeContentItem(item)) {
4653
+ return item;
4654
+ }
4655
+ if (item.type === "input_image" && typeof item.image_url === "string" && item.image_url.trim().toLowerCase().startsWith("data:")) {
4656
+ const parsed = parseDataUrlPayload(item.image_url);
4657
+ if (!parsed) {
4658
+ return item;
4659
+ }
4660
+ const uploaded = await storeCanonicalPromptFile({
4661
+ bytes: parsed.bytes,
4662
+ mimeType: parsed.mimeType ?? "application/octet-stream",
4663
+ filename: normaliseAttachmentFilename(
4664
+ getInlineAttachmentFilename(item),
4665
+ guessInlineDataFilename(parsed.mimeType)
4666
+ )
4667
+ });
4668
+ return {
4669
+ type: "input_image",
4670
+ detail: item.detail === "high" || item.detail === "low" ? item.detail : "auto",
4671
+ file_id: uploaded.fileId
4672
+ };
4673
+ }
4674
+ if (item.type !== "input_file" || item.file_id) {
4675
+ return item;
4676
+ }
4677
+ if (typeof item.file_data === "string" && item.file_data.trim().length > 0) {
4678
+ const filename = normaliseAttachmentFilename(
4679
+ typeof item.filename === "string" ? item.filename : void 0,
4680
+ guessInlineDataFilename(void 0)
4681
+ );
4682
+ const mimeType = inferToolOutputMimeTypeFromFilename(filename) ?? "application/octet-stream";
4683
+ const uploaded = await storeCanonicalPromptFile({
4684
+ bytes: decodeInlineDataBuffer(item.file_data),
4685
+ mimeType,
4686
+ filename
4687
+ });
4688
+ return { type: "input_file", file_id: uploaded.fileId, filename: uploaded.filename };
4689
+ }
4690
+ if (typeof item.file_url === "string" && item.file_url.trim().toLowerCase().startsWith("data:")) {
4691
+ const parsed = parseDataUrlPayload(item.file_url);
4692
+ if (!parsed) {
4693
+ return item;
4694
+ }
4695
+ const uploaded = await storeCanonicalPromptFile({
4696
+ bytes: parsed.bytes,
4697
+ mimeType: parsed.mimeType ?? "application/octet-stream",
4698
+ filename: normaliseAttachmentFilename(
4699
+ typeof item.filename === "string" ? item.filename : void 0,
4700
+ guessInlineDataFilename(parsed.mimeType)
4701
+ )
4702
+ });
4703
+ return { type: "input_file", file_id: uploaded.fileId, filename: uploaded.filename };
4704
+ }
4705
+ return item;
4706
+ }
4707
+ async function prepareOpenAiPromptInput(input) {
4708
+ const prepareItem = async (item) => {
4709
+ if (!item || typeof item !== "object") {
4710
+ return item;
4711
+ }
4712
+ const record = item;
4713
+ if (Array.isArray(record.content)) {
4714
+ return {
4715
+ ...record,
4716
+ content: await Promise.all(
4717
+ record.content.map((part) => prepareOpenAiPromptContentItem(part))
4718
+ )
4719
+ };
4720
+ }
4721
+ if (Array.isArray(record.output)) {
4722
+ return {
4723
+ ...record,
4724
+ output: await Promise.all(
4725
+ record.output.map((part) => prepareOpenAiPromptContentItem(part))
4726
+ )
4727
+ };
4728
+ }
4729
+ return await prepareOpenAiPromptContentItem(item);
4730
+ };
4731
+ return await Promise.all(input.map((item) => prepareItem(item)));
4732
+ }
4733
+ async function maybePrepareOpenAiPromptInput(input) {
4734
+ if (estimateOpenAiInlinePromptBytes(input) <= INLINE_ATTACHMENT_PROMPT_THRESHOLD_BYTES) {
4735
+ return Array.from(input);
4736
+ }
4737
+ return await prepareOpenAiPromptInput(input);
4738
+ }
4739
+ function estimateGeminiInlinePromptBytes(contents) {
4740
+ let total = 0;
4741
+ for (const content of contents) {
4742
+ for (const part of content.parts ?? []) {
4743
+ if (part.inlineData?.data) {
4744
+ total += estimateInlinePayloadBytes(part.inlineData.data);
4745
+ }
4746
+ }
4747
+ }
4748
+ return total;
4749
+ }
4750
+ function hasCanonicalGeminiFileReferences(contents) {
4751
+ for (const content of contents) {
4752
+ for (const part of content.parts ?? []) {
4753
+ if (parseCanonicalGeminiFileId(part.fileData?.fileUri)) {
4754
+ return true;
4755
+ }
4756
+ }
4757
+ }
4758
+ return false;
4759
+ }
4760
+ async function prepareGeminiPromptContents(contents) {
4761
+ const backend = getGeminiBackend();
4762
+ const preparedContents = [];
4763
+ for (const content of contents) {
4764
+ const parts = [];
4765
+ for (const part of content.parts ?? []) {
4766
+ const canonicalFileId = parseCanonicalGeminiFileId(part.fileData?.fileUri);
4767
+ if (canonicalFileId) {
4768
+ const metadata = await getCanonicalFileMetadata(canonicalFileId);
4769
+ if (backend === "api") {
4770
+ const mirrored = await ensureGeminiFileMirror(canonicalFileId);
4771
+ const mirroredPart = createPartFromUri(mirrored.uri, mirrored.mimeType);
4772
+ if (metadata.filename && mirroredPart.fileData) {
4773
+ mirroredPart.fileData.displayName = metadata.filename;
4774
+ }
4775
+ parts.push(mirroredPart);
4776
+ } else {
4777
+ const mirrored = await ensureVertexFileMirror(canonicalFileId);
4778
+ parts.push({
4779
+ fileData: {
4780
+ fileUri: mirrored.fileUri,
4781
+ mimeType: mirrored.mimeType,
4782
+ displayName: metadata.filename
4783
+ }
4784
+ });
4785
+ }
4786
+ continue;
4787
+ }
4788
+ if (part.inlineData?.data) {
4789
+ const mimeType = part.inlineData.mimeType ?? "application/octet-stream";
4790
+ const filename = normaliseAttachmentFilename(
4791
+ getInlineAttachmentFilename(part.inlineData) ?? part.inlineData.displayName ?? guessInlineDataFilename(mimeType),
4792
+ guessInlineDataFilename(mimeType)
4793
+ );
4794
+ const stored = await storeCanonicalPromptFile({
4795
+ bytes: decodeInlineDataBuffer(part.inlineData.data),
4796
+ mimeType,
4797
+ filename
4798
+ });
4799
+ if (backend === "api") {
4800
+ const mirrored = await ensureGeminiFileMirror(stored.fileId);
4801
+ const mirroredPart = createPartFromUri(mirrored.uri, mirrored.mimeType);
4802
+ if (filename && mirroredPart.fileData) {
4803
+ mirroredPart.fileData.displayName = filename;
4804
+ }
4805
+ parts.push(mirroredPart);
4806
+ } else {
4807
+ const mirrored = await ensureVertexFileMirror(stored.fileId);
4808
+ parts.push({
4809
+ fileData: {
4810
+ fileUri: mirrored.fileUri,
4811
+ mimeType: mirrored.mimeType,
4812
+ displayName: filename
4813
+ }
4814
+ });
4815
+ }
4816
+ continue;
4817
+ }
4818
+ parts.push(part);
4819
+ }
4820
+ preparedContents.push({
4821
+ ...content,
4822
+ parts
4823
+ });
4824
+ }
4825
+ return preparedContents;
4826
+ }
4827
+ async function maybePrepareGeminiPromptContents(contents) {
4828
+ if (!hasCanonicalGeminiFileReferences(contents) && estimateGeminiInlinePromptBytes(contents) <= INLINE_ATTACHMENT_PROMPT_THRESHOLD_BYTES) {
4829
+ return Array.from(contents);
4830
+ }
4831
+ return await prepareGeminiPromptContents(contents);
4832
+ }
4833
+ function mergeConsecutiveTextParts(parts) {
3723
4834
  if (parts.length === 0) {
3724
4835
  return [];
3725
4836
  }
3726
4837
  const merged = [];
3727
4838
  for (const part of parts) {
3728
4839
  if (part.type !== "text") {
3729
- merged.push({ type: "inlineData", data: part.data, mimeType: part.mimeType });
4840
+ merged.push(cloneContentPart(part));
3730
4841
  continue;
3731
4842
  }
3732
4843
  const isThought = part.thought === true;
@@ -4157,13 +5268,7 @@ function resolveTextContents(input) {
4157
5268
  const parts = typeof message.content === "string" ? [{ type: "text", text: message.content }] : message.content;
4158
5269
  contents.push({
4159
5270
  role: message.role,
4160
- parts: parts.map(
4161
- (part) => part.type === "text" ? {
4162
- type: "text",
4163
- text: part.text,
4164
- thought: "thought" in part && part.thought === true ? true : void 0
4165
- } : { type: "inlineData", data: part.data, mimeType: part.mimeType }
4166
- )
5271
+ parts: parts.map((part) => cloneContentPart(part))
4167
5272
  });
4168
5273
  }
4169
5274
  return contents;
@@ -4179,22 +5284,58 @@ function toOpenAiInput(contents) {
4179
5284
  return contents.map((content) => {
4180
5285
  const parts = [];
4181
5286
  for (const part of content.parts) {
4182
- if (part.type === "text") {
4183
- parts.push({ type: "input_text", text: part.text });
4184
- continue;
4185
- }
4186
- const mimeType = part.mimeType;
4187
- if (isInlineImageMime(mimeType)) {
4188
- const dataUrl = `data:${mimeType};base64,${part.data}`;
4189
- parts.push({ type: "input_image", image_url: dataUrl, detail: "auto" });
4190
- continue;
5287
+ switch (part.type) {
5288
+ case "text":
5289
+ parts.push({ type: "input_text", text: part.text });
5290
+ break;
5291
+ case "inlineData": {
5292
+ const mimeType = part.mimeType;
5293
+ if (isInlineImageMime(mimeType)) {
5294
+ const dataUrl = `data:${mimeType};base64,${part.data}`;
5295
+ const imagePart = {
5296
+ type: "input_image",
5297
+ image_url: dataUrl,
5298
+ detail: "auto"
5299
+ };
5300
+ setInlineAttachmentFilename(
5301
+ imagePart,
5302
+ normaliseAttachmentFilename(part.filename, guessInlineDataFilename(mimeType))
5303
+ );
5304
+ parts.push(imagePart);
5305
+ break;
5306
+ }
5307
+ parts.push({
5308
+ type: "input_file",
5309
+ filename: normaliseAttachmentFilename(part.filename, guessInlineDataFilename(mimeType)),
5310
+ file_data: part.data
5311
+ });
5312
+ break;
5313
+ }
5314
+ case "input_image": {
5315
+ const imagePart = {
5316
+ type: "input_image",
5317
+ ...part.file_id ? { file_id: part.file_id } : {},
5318
+ ...part.image_url ? { image_url: part.image_url } : {},
5319
+ detail: part.detail === "high" || part.detail === "low" ? part.detail : "auto"
5320
+ };
5321
+ if (part.filename) {
5322
+ setInlineAttachmentFilename(imagePart, part.filename);
5323
+ }
5324
+ parts.push(imagePart);
5325
+ break;
5326
+ }
5327
+ case "input_file":
5328
+ parts.push({
5329
+ type: "input_file",
5330
+ ...part.file_id ? { file_id: part.file_id } : {},
5331
+ ...part.file_data ? { file_data: part.file_data } : {},
5332
+ ...part.file_url ? { file_url: part.file_url } : {},
5333
+ ...part.filename ? { filename: part.filename } : {}
5334
+ });
5335
+ break;
5336
+ default:
5337
+ throw new Error("Unsupported LLM content part");
4191
5338
  }
4192
- const fileData = decodeInlineDataBuffer(part.data).toString("base64");
4193
- parts.push({
4194
- type: "input_file",
4195
- filename: guessInlineDataFilename(mimeType),
4196
- file_data: fileData
4197
- });
4198
5339
  }
4199
5340
  if (parts.length === 1 && parts[0]?.type === "input_text" && typeof parts[0].text === "string") {
4200
5341
  return {
@@ -4231,28 +5372,54 @@ function toChatGptInput(contents) {
4231
5372
  continue;
4232
5373
  }
4233
5374
  if (isAssistant) {
4234
- const mimeType = part.mimeType ?? "application/octet-stream";
5375
+ const mimeType = part.type === "inlineData" ? part.mimeType ?? "application/octet-stream" : inferToolOutputMimeTypeFromFilename(part.filename) ?? "application/octet-stream";
4235
5376
  parts.push({
4236
5377
  type: "output_text",
4237
- text: isInlineImageMime(part.mimeType) ? `[image:${mimeType}]` : `[file:${mimeType}]`
5378
+ text: part.type === "input_image" || isInlineImageMime(part.mimeType) ? `[image:${mimeType}]` : `[file:${mimeType}]`
4238
5379
  });
4239
- } else {
4240
- if (isInlineImageMime(part.mimeType)) {
4241
- const mimeType = part.mimeType ?? "application/octet-stream";
4242
- const dataUrl = `data:${mimeType};base64,${part.data}`;
5380
+ continue;
5381
+ }
5382
+ switch (part.type) {
5383
+ case "inlineData": {
5384
+ if (isInlineImageMime(part.mimeType)) {
5385
+ const mimeType = part.mimeType ?? "application/octet-stream";
5386
+ const dataUrl = `data:${mimeType};base64,${part.data}`;
5387
+ parts.push({
5388
+ type: "input_image",
5389
+ image_url: dataUrl,
5390
+ detail: "auto"
5391
+ });
5392
+ } else {
5393
+ parts.push({
5394
+ type: "input_file",
5395
+ filename: normaliseAttachmentFilename(
5396
+ part.filename,
5397
+ guessInlineDataFilename(part.mimeType)
5398
+ ),
5399
+ file_data: part.data
5400
+ });
5401
+ }
5402
+ break;
5403
+ }
5404
+ case "input_image":
4243
5405
  parts.push({
4244
5406
  type: "input_image",
4245
- image_url: dataUrl,
4246
- detail: "auto"
5407
+ ...part.file_id ? { file_id: part.file_id } : {},
5408
+ ...part.image_url ? { image_url: part.image_url } : {},
5409
+ detail: part.detail === "high" || part.detail === "low" ? part.detail : "auto"
4247
5410
  });
4248
- } else {
4249
- const fileData = decodeInlineDataBuffer(part.data).toString("base64");
5411
+ break;
5412
+ case "input_file":
4250
5413
  parts.push({
4251
5414
  type: "input_file",
4252
- filename: guessInlineDataFilename(part.mimeType),
4253
- file_data: fileData
5415
+ ...part.file_id ? { file_id: part.file_id } : {},
5416
+ ...part.file_data ? { file_data: part.file_data } : {},
5417
+ ...part.file_url ? { file_url: part.file_url } : {},
5418
+ ...part.filename ? { filename: part.filename } : {}
4254
5419
  });
4255
- }
5420
+ break;
5421
+ default:
5422
+ throw new Error("Unsupported LLM content part");
4256
5423
  }
4257
5424
  }
4258
5425
  if (parts.length === 0) {
@@ -4296,8 +5463,8 @@ ${JSON.stringify(options.responseJsonSchema)}`);
4296
5463
  if (part.type === "text") {
4297
5464
  return part.text;
4298
5465
  }
4299
- const mimeType = part.mimeType ?? "application/octet-stream";
4300
- if (isInlineImageMime(mimeType)) {
5466
+ const mimeType = part.type === "inlineData" ? part.mimeType ?? "application/octet-stream" : inferToolOutputMimeTypeFromFilename(part.filename) ?? "application/octet-stream";
5467
+ if (part.type === "input_image" || isInlineImageMime(mimeType)) {
4301
5468
  return `[image:${mimeType}]`;
4302
5469
  }
4303
5470
  return `[file:${mimeType}]`;
@@ -4568,7 +5735,14 @@ function isLlmToolOutputContentItem(value) {
4568
5735
  return typeof value.text === "string";
4569
5736
  }
4570
5737
  if (itemType === "input_image") {
4571
- return typeof value.image_url === "string";
5738
+ const keys = ["image_url", "file_id", "filename"];
5739
+ for (const key of keys) {
5740
+ const part = value[key];
5741
+ if (part !== void 0 && part !== null && typeof part !== "string") {
5742
+ return false;
5743
+ }
5744
+ }
5745
+ return value.image_url !== void 0 || value.file_id !== void 0;
4572
5746
  }
4573
5747
  if (itemType === "input_file") {
4574
5748
  const keys = ["file_data", "file_id", "file_url", "filename"];
@@ -4637,15 +5811,252 @@ function inferToolOutputMimeTypeFromFilename(filename) {
4637
5811
  }
4638
5812
  return void 0;
4639
5813
  }
4640
- function buildGeminiToolOutputMediaPart(item) {
5814
+ function estimateToolOutputItemBytes(item) {
5815
+ if (item.type === "input_text") {
5816
+ return Buffer5.byteLength(item.text, "utf8");
5817
+ }
5818
+ if (item.type === "input_image") {
5819
+ return typeof item.image_url === "string" ? estimateInlinePayloadBytes(item.image_url) : 0;
5820
+ }
5821
+ if (typeof item.file_data === "string" && item.file_data.trim().length > 0) {
5822
+ return estimateInlinePayloadBytes(item.file_data);
5823
+ }
5824
+ if (typeof item.file_url === "string" && item.file_url.trim().length > 0) {
5825
+ return estimateInlinePayloadBytes(item.file_url);
5826
+ }
5827
+ return 0;
5828
+ }
5829
+ async function spillTextToolOutputToFile(options) {
5830
+ const stored = await runWithFileUploadSource("tool_output_spill", async () => {
5831
+ return await filesCreate({
5832
+ data: options.text,
5833
+ filename: options.filename,
5834
+ mimeType: options.mimeType,
5835
+ expiresAfterSeconds: DEFAULT_FILE_TTL_SECONDS
5836
+ });
5837
+ });
5838
+ return [
5839
+ {
5840
+ type: "input_text",
5841
+ text: `Tool output was attached as ${stored.filename} (${stored.id}) because it exceeded the inline payload threshold.`
5842
+ },
5843
+ {
5844
+ type: "input_file",
5845
+ file_id: stored.id,
5846
+ filename: stored.filename
5847
+ }
5848
+ ];
5849
+ }
5850
+ async function maybeSpillToolOutputItem(item, toolName, options) {
5851
+ if (options?.force !== true && estimateToolOutputItemBytes(item) <= TOOL_OUTPUT_SPILL_THRESHOLD_BYTES) {
5852
+ return item;
5853
+ }
5854
+ if (item.type === "input_text") {
5855
+ return await spillTextToolOutputToFile({
5856
+ text: item.text,
5857
+ filename: normaliseAttachmentFilename(`${toolName}.txt`, "tool-output.txt"),
5858
+ mimeType: "text/plain"
5859
+ });
5860
+ }
4641
5861
  if (item.type === "input_image") {
5862
+ if (item.file_id || !item.image_url) {
5863
+ return item;
5864
+ }
4642
5865
  const parsed = parseDataUrlPayload(item.image_url);
4643
5866
  if (!parsed) {
5867
+ return item;
5868
+ }
5869
+ const stored = await runWithFileUploadSource("tool_output_spill", async () => {
5870
+ return await filesCreate({
5871
+ data: parsed.bytes,
5872
+ filename: normaliseAttachmentFilename(
5873
+ item.filename ?? guessInlineDataFilename(parsed.mimeType),
5874
+ guessInlineDataFilename(parsed.mimeType)
5875
+ ),
5876
+ mimeType: parsed.mimeType,
5877
+ expiresAfterSeconds: DEFAULT_FILE_TTL_SECONDS
5878
+ });
5879
+ });
5880
+ return {
5881
+ type: "input_image",
5882
+ file_id: stored.id,
5883
+ detail: item.detail ?? "auto",
5884
+ filename: stored.filename
5885
+ };
5886
+ }
5887
+ if (item.file_id) {
5888
+ return item;
5889
+ }
5890
+ if (typeof item.file_data === "string" && item.file_data.trim().length > 0) {
5891
+ const fileData = item.file_data;
5892
+ const filename = normaliseAttachmentFilename(
5893
+ item.filename ?? `${toolName}.bin`,
5894
+ `${toolName}.bin`
5895
+ );
5896
+ const stored = await runWithFileUploadSource("tool_output_spill", async () => {
5897
+ return await filesCreate({
5898
+ data: decodeInlineDataBuffer(fileData),
5899
+ filename,
5900
+ mimeType: inferToolOutputMimeTypeFromFilename(filename) ?? "application/octet-stream",
5901
+ expiresAfterSeconds: DEFAULT_FILE_TTL_SECONDS
5902
+ });
5903
+ });
5904
+ return {
5905
+ type: "input_file",
5906
+ file_id: stored.id,
5907
+ filename: stored.filename
5908
+ };
5909
+ }
5910
+ if (typeof item.file_url === "string" && item.file_url.trim().length > 0) {
5911
+ const parsed = parseDataUrlPayload(item.file_url);
5912
+ if (!parsed) {
5913
+ return item;
5914
+ }
5915
+ const stored = await runWithFileUploadSource("tool_output_spill", async () => {
5916
+ return await filesCreate({
5917
+ data: parsed.bytes,
5918
+ filename: normaliseAttachmentFilename(
5919
+ item.filename ?? guessInlineDataFilename(parsed.mimeType),
5920
+ guessInlineDataFilename(parsed.mimeType)
5921
+ ),
5922
+ mimeType: parsed.mimeType,
5923
+ expiresAfterSeconds: DEFAULT_FILE_TTL_SECONDS
5924
+ });
5925
+ });
5926
+ return {
5927
+ type: "input_file",
5928
+ file_id: stored.id,
5929
+ filename: stored.filename
5930
+ };
5931
+ }
5932
+ return item;
5933
+ }
5934
+ async function maybeSpillToolOutput(value, toolName, options) {
5935
+ if (typeof value === "string") {
5936
+ if (options?.force !== true && Buffer5.byteLength(value, "utf8") <= TOOL_OUTPUT_SPILL_THRESHOLD_BYTES) {
5937
+ return value;
5938
+ }
5939
+ return await spillTextToolOutputToFile({
5940
+ text: value,
5941
+ filename: normaliseAttachmentFilename(`${toolName}.txt`, "tool-output.txt"),
5942
+ mimeType: "text/plain"
5943
+ });
5944
+ }
5945
+ if (isLlmToolOutputContentItem(value)) {
5946
+ return await maybeSpillToolOutputItem(value, toolName, options);
5947
+ }
5948
+ if (Array.isArray(value) && value.every((item) => isLlmToolOutputContentItem(item))) {
5949
+ const spilledItems = [];
5950
+ for (const item of value) {
5951
+ const maybeSpilled = await maybeSpillToolOutputItem(item, toolName, options);
5952
+ if (Array.isArray(maybeSpilled)) {
5953
+ spilledItems.push(...maybeSpilled);
5954
+ } else {
5955
+ spilledItems.push(maybeSpilled);
5956
+ }
5957
+ }
5958
+ return spilledItems;
5959
+ }
5960
+ try {
5961
+ const serialized = JSON.stringify(value, null, 2);
5962
+ if (options?.force !== true && Buffer5.byteLength(serialized, "utf8") <= TOOL_OUTPUT_SPILL_THRESHOLD_BYTES) {
5963
+ return value;
5964
+ }
5965
+ return await spillTextToolOutputToFile({
5966
+ text: serialized,
5967
+ filename: normaliseAttachmentFilename(`${toolName}.json`, "tool-output.json"),
5968
+ mimeType: "application/json"
5969
+ });
5970
+ } catch {
5971
+ return value;
5972
+ }
5973
+ }
5974
+ function estimateToolOutputPayloadBytes(value) {
5975
+ if (typeof value === "string") {
5976
+ return Buffer5.byteLength(value, "utf8");
5977
+ }
5978
+ if (isLlmToolOutputContentItem(value)) {
5979
+ return value.type === "input_text" ? 0 : estimateToolOutputItemBytes(value);
5980
+ }
5981
+ if (Array.isArray(value) && value.every((item) => isLlmToolOutputContentItem(item))) {
5982
+ return value.reduce((total, item) => {
5983
+ return total + (item.type === "input_text" ? 0 : estimateToolOutputItemBytes(item));
5984
+ }, 0);
5985
+ }
5986
+ return 0;
5987
+ }
5988
+ async function maybeSpillCombinedToolCallOutputs(callResults) {
5989
+ const totalBytes = callResults.reduce(
5990
+ (sum, callResult) => sum + estimateToolOutputPayloadBytes(callResult.outputPayload),
5991
+ 0
5992
+ );
5993
+ if (totalBytes <= INLINE_ATTACHMENT_PROMPT_THRESHOLD_BYTES) {
5994
+ return Array.from(callResults);
5995
+ }
5996
+ return await Promise.all(
5997
+ callResults.map(async (callResult) => {
5998
+ if (estimateToolOutputPayloadBytes(callResult.outputPayload) === 0) {
5999
+ return callResult;
6000
+ }
6001
+ const outputPayload = await maybeSpillToolOutput(
6002
+ callResult.outputPayload,
6003
+ callResult.entry.toolName,
6004
+ {
6005
+ force: true
6006
+ }
6007
+ );
6008
+ return {
6009
+ ...callResult,
6010
+ outputPayload,
6011
+ result: {
6012
+ ...callResult.result,
6013
+ output: outputPayload
6014
+ }
6015
+ };
6016
+ })
6017
+ );
6018
+ }
6019
+ function buildGeminiToolOutputMediaPart(item) {
6020
+ if (item.type === "input_image") {
6021
+ if (typeof item.file_id === "string" && item.file_id.trim().length > 0) {
6022
+ return {
6023
+ fileData: {
6024
+ fileUri: buildCanonicalGeminiFileUri(item.file_id),
6025
+ mimeType: inferToolOutputMimeTypeFromFilename(item.filename) ?? "application/octet-stream",
6026
+ displayName: item.filename ?? void 0
6027
+ }
6028
+ };
6029
+ }
6030
+ if (typeof item.image_url !== "string" || item.image_url.trim().length === 0) {
4644
6031
  return null;
4645
6032
  }
4646
- return createPartFromBase64(parsed.dataBase64, parsed.mimeType);
6033
+ const parsed = parseDataUrlPayload(item.image_url);
6034
+ if (parsed) {
6035
+ const part = createPartFromBase64(parsed.dataBase64, parsed.mimeType);
6036
+ const displayName = item.filename?.trim();
6037
+ if (displayName && part.inlineData) {
6038
+ part.inlineData.displayName = displayName;
6039
+ }
6040
+ return part;
6041
+ }
6042
+ return {
6043
+ fileData: {
6044
+ fileUri: item.image_url,
6045
+ mimeType: inferToolOutputMimeTypeFromFilename(item.filename) ?? "application/octet-stream",
6046
+ displayName: item.filename ?? void 0
6047
+ }
6048
+ };
4647
6049
  }
4648
6050
  if (item.type === "input_file") {
6051
+ if (typeof item.file_id === "string" && item.file_id.trim().length > 0) {
6052
+ return {
6053
+ fileData: {
6054
+ fileUri: buildCanonicalGeminiFileUri(item.file_id),
6055
+ mimeType: inferToolOutputMimeTypeFromFilename(item.filename) ?? "application/octet-stream",
6056
+ displayName: item.filename ?? void 0
6057
+ }
6058
+ };
6059
+ }
4649
6060
  const dataUrl = typeof item.file_url === "string" ? parseDataUrlPayload(item.file_url) : null;
4650
6061
  if (dataUrl) {
4651
6062
  const part = createPartFromBase64(dataUrl.dataBase64, dataUrl.mimeType);
@@ -4683,11 +6094,12 @@ function toGeminiToolOutputPlaceholder(item) {
4683
6094
  };
4684
6095
  }
4685
6096
  if (item.type === "input_image") {
4686
- const parsed = parseDataUrlPayload(item.image_url);
6097
+ const parsed = typeof item.image_url === "string" ? parseDataUrlPayload(item.image_url) : null;
4687
6098
  return {
4688
6099
  type: item.type,
6100
+ fileId: item.file_id ?? void 0,
4689
6101
  mimeType: parsed?.mimeType ?? void 0,
4690
- media: "attached-inline-data"
6102
+ media: item.file_id ? "attached-file-id" : parsed ? "attached-inline-data" : item.image_url ? "attached-file-data" : void 0
4691
6103
  };
4692
6104
  }
4693
6105
  const dataUrl = typeof item.file_url === "string" ? parseDataUrlPayload(item.file_url) : null;
@@ -4696,7 +6108,7 @@ function toGeminiToolOutputPlaceholder(item) {
4696
6108
  filename: item.filename ?? void 0,
4697
6109
  fileId: item.file_id ?? void 0,
4698
6110
  mimeType: dataUrl?.mimeType ?? inferToolOutputMimeTypeFromFilename(item.filename) ?? void 0,
4699
- media: dataUrl || typeof item.file_data === "string" && item.file_data.trim().length > 0 ? "attached-inline-data" : typeof item.file_url === "string" && item.file_url.trim().length > 0 ? "attached-file-data" : void 0
6111
+ media: item.file_id ? "attached-file-id" : dataUrl || typeof item.file_data === "string" && item.file_data.trim().length > 0 ? "attached-inline-data" : typeof item.file_url === "string" && item.file_url.trim().length > 0 ? "attached-file-data" : void 0
4700
6112
  };
4701
6113
  }
4702
6114
  function buildGeminiFunctionResponseParts(options) {
@@ -4744,8 +6156,8 @@ function parseOpenAiToolArguments(raw) {
4744
6156
  function formatZodIssues(issues) {
4745
6157
  const messages = [];
4746
6158
  for (const issue of issues) {
4747
- const path8 = issue.path.length > 0 ? issue.path.map(String).join(".") : "input";
4748
- messages.push(`${path8}: ${issue.message}`);
6159
+ const path10 = issue.path.length > 0 ? issue.path.map(String).join(".") : "input";
6160
+ messages.push(`${path10}: ${issue.message}`);
4749
6161
  }
4750
6162
  return messages.join("; ");
4751
6163
  }
@@ -4863,8 +6275,9 @@ async function executeToolCall(params) {
4863
6275
  const input = typeof rawInput === "string" ? rawInput : String(rawInput ?? "");
4864
6276
  try {
4865
6277
  const output = await tool2.execute(input);
4866
- const metrics = toolName === "spawn_agent" ? extractSpawnStartupMetrics(output) : void 0;
4867
- return finalize({ toolName, input, output }, output, metrics);
6278
+ const outputPayload = await maybeSpillToolOutput(output, toolName);
6279
+ const metrics = toolName === "spawn_agent" ? extractSpawnStartupMetrics(outputPayload) : void 0;
6280
+ return finalize({ toolName, input, output: outputPayload }, outputPayload, metrics);
4868
6281
  } catch (error) {
4869
6282
  const message = error instanceof Error ? error.message : String(error);
4870
6283
  const outputPayload = buildToolErrorOutput(`Tool ${toolName} failed: ${message}`);
@@ -4898,8 +6311,13 @@ async function executeToolCall(params) {
4898
6311
  }
4899
6312
  try {
4900
6313
  const output = await tool2.execute(parsed.data);
4901
- const metrics = toolName === "spawn_agent" ? extractSpawnStartupMetrics(output) : void 0;
4902
- return finalize({ toolName, input: parsed.data, output }, output, metrics);
6314
+ const outputPayload = await maybeSpillToolOutput(output, toolName);
6315
+ const metrics = toolName === "spawn_agent" ? extractSpawnStartupMetrics(outputPayload) : void 0;
6316
+ return finalize(
6317
+ { toolName, input: parsed.data, output: outputPayload },
6318
+ outputPayload,
6319
+ metrics
6320
+ );
4903
6321
  } catch (error) {
4904
6322
  const message = error instanceof Error ? error.message : String(error);
4905
6323
  const outputPayload = buildToolErrorOutput(`Tool ${toolName} failed: ${message}`);
@@ -5100,7 +6518,7 @@ function toGemini25ProThinkingBudget(thinkingLevel) {
5100
6518
  }
5101
6519
  }
5102
6520
  function resolveGeminiThinkingConfig(modelId, thinkingLevel) {
5103
- if (isGeminiImageModelId(modelId)) {
6521
+ if (isGeminiImageModelId(modelId) || modelId === "gemini-flash-lite-latest") {
5104
6522
  return void 0;
5105
6523
  }
5106
6524
  if (thinkingLevel) {
@@ -5131,9 +6549,9 @@ function resolveGeminiThinkingConfig(modelId, thinkingLevel) {
5131
6549
  }
5132
6550
  function decodeInlineDataBuffer(base64) {
5133
6551
  try {
5134
- return Buffer4.from(base64, "base64");
6552
+ return Buffer5.from(base64, "base64");
5135
6553
  } catch {
5136
- return Buffer4.from(base64, "base64url");
6554
+ return Buffer5.from(base64, "base64url");
5137
6555
  }
5138
6556
  }
5139
6557
  function extractImages(content) {
@@ -5203,7 +6621,7 @@ function parseDataUrlPayload(value) {
5203
6621
  const isBase64 = /;base64(?:;|$)/iu.test(header);
5204
6622
  const mimeType = (header.split(";")[0] ?? "application/octet-stream").trim().toLowerCase();
5205
6623
  try {
5206
- const bytes = isBase64 ? Buffer4.from(payload, "base64") : Buffer4.from(decodeURIComponent(payload), "utf8");
6624
+ const bytes = isBase64 ? Buffer5.from(payload, "base64") : Buffer5.from(decodeURIComponent(payload), "utf8");
5207
6625
  return {
5208
6626
  mimeType,
5209
6627
  dataBase64: bytes.toString("base64"),
@@ -5306,7 +6724,10 @@ function collectLoggedAttachmentsFromLlmParts(parts, prefix) {
5306
6724
  continue;
5307
6725
  }
5308
6726
  attachments.push({
5309
- filename: buildLoggedAttachmentFilename(prefix, index, part.mimeType),
6727
+ filename: normaliseAttachmentFilename(
6728
+ part.filename,
6729
+ buildLoggedAttachmentFilename(prefix, index, part.mimeType)
6730
+ ),
5310
6731
  bytes: decodeInlineDataBuffer(part.data)
5311
6732
  });
5312
6733
  index += 1;
@@ -5435,15 +6856,19 @@ function startLlmCallLoggerFromContents(options) {
5435
6856
  sections.push(part.text);
5436
6857
  continue;
5437
6858
  }
5438
- const filename = buildLoggedAttachmentFilename("input", attachmentIndex, part.mimeType);
5439
- attachments.push({
5440
- filename,
5441
- bytes: decodeInlineDataBuffer(part.data)
5442
- });
5443
- attachmentIndex += 1;
5444
- sections.push(
5445
- `[inlineData] file=${filename} mime=${part.mimeType ?? "application/octet-stream"} bytes=${attachments[attachments.length - 1]?.bytes.byteLength ?? 0}`
5446
- );
6859
+ if (part.type === "inlineData") {
6860
+ const filename = buildLoggedAttachmentFilename("input", attachmentIndex, part.mimeType);
6861
+ attachments.push({
6862
+ filename,
6863
+ bytes: decodeInlineDataBuffer(part.data)
6864
+ });
6865
+ attachmentIndex += 1;
6866
+ sections.push(
6867
+ `[inlineData] file=${filename} mime=${part.mimeType ?? "application/octet-stream"} bytes=${attachments[attachments.length - 1]?.bytes.byteLength ?? 0}`
6868
+ );
6869
+ continue;
6870
+ }
6871
+ sections.push(`[${part.type}] file_id=${"file_id" in part ? part.file_id ?? "" : ""}`);
5447
6872
  }
5448
6873
  sections.push("");
5449
6874
  }
@@ -5568,313 +6993,323 @@ async function runTextCall(params) {
5568
6993
  request.signal.addEventListener(
5569
6994
  "abort",
5570
6995
  () => abortController.abort(request.signal?.reason),
5571
- { once: true }
5572
- );
5573
- }
5574
- return abortController.signal;
5575
- };
5576
- const signal = resolveAbortSignal();
5577
- try {
5578
- if (provider === "openai") {
5579
- const openAiInput = toOpenAiInput(contents);
5580
- const openAiTools = toOpenAiTools(request.tools);
5581
- const reasoningEffort = resolveOpenAiReasoningEffort(modelForProvider, request.thinkingLevel);
5582
- const openAiTextConfig = {
5583
- format: request.openAiTextFormat ?? { type: "text" },
5584
- verbosity: resolveOpenAiVerbosity(modelForProvider)
5585
- };
5586
- const reasoning = {
5587
- effort: toOpenAiReasoningEffort(reasoningEffort),
5588
- summary: "detailed"
5589
- };
5590
- await runOpenAiCall(async (client) => {
5591
- const stream = client.responses.stream(
5592
- {
5593
- model: modelForProvider,
5594
- input: openAiInput,
5595
- reasoning,
5596
- text: openAiTextConfig,
5597
- ...openAiTools ? { tools: openAiTools } : {},
5598
- include: ["code_interpreter_call.outputs", "reasoning.encrypted_content"]
5599
- },
5600
- { signal }
6996
+ { once: true }
6997
+ );
6998
+ }
6999
+ return abortController.signal;
7000
+ };
7001
+ const signal = resolveAbortSignal();
7002
+ const { result } = await collectFileUploadMetrics(async () => {
7003
+ try {
7004
+ if (provider === "openai") {
7005
+ const openAiInput = await maybePrepareOpenAiPromptInput(toOpenAiInput(contents));
7006
+ const openAiTools = toOpenAiTools(request.tools);
7007
+ const reasoningEffort = resolveOpenAiReasoningEffort(
7008
+ modelForProvider,
7009
+ request.thinkingLevel
5601
7010
  );
5602
- for await (const event of stream) {
5603
- switch (event.type) {
5604
- case "response.output_text.delta": {
5605
- const delta = event.delta ?? "";
5606
- pushDelta("response", typeof delta === "string" ? delta : "");
5607
- break;
5608
- }
5609
- case "response.reasoning_summary_text.delta": {
5610
- const delta = event.delta ?? "";
5611
- pushDelta("thought", typeof delta === "string" ? delta : "");
5612
- break;
5613
- }
5614
- case "response.refusal.delta": {
5615
- blocked = true;
5616
- queue.push({ type: "blocked" });
5617
- break;
5618
- }
5619
- default:
5620
- break;
5621
- }
5622
- }
5623
- const finalResponse = await stream.finalResponse();
5624
- modelVersion = typeof finalResponse.model === "string" ? finalResponse.model : request.model;
5625
- queue.push({ type: "model", modelVersion });
5626
- if (finalResponse.error) {
5627
- const message = typeof finalResponse.error.message === "string" ? finalResponse.error.message : "OpenAI response failed";
5628
- throw new Error(message);
5629
- }
5630
- if (finalResponse.status && finalResponse.status !== "completed" && finalResponse.status !== "in_progress") {
5631
- const detail = finalResponse.incomplete_details?.reason;
5632
- throw new Error(
5633
- `OpenAI response status ${finalResponse.status}${detail ? ` (${detail})` : ""}`
7011
+ const openAiTextConfig = {
7012
+ format: request.openAiTextFormat ?? { type: "text" },
7013
+ verbosity: resolveOpenAiVerbosity(modelForProvider)
7014
+ };
7015
+ const reasoning = {
7016
+ effort: toOpenAiReasoningEffort(reasoningEffort),
7017
+ summary: "detailed"
7018
+ };
7019
+ await runOpenAiCall(async (client) => {
7020
+ const stream = client.responses.stream(
7021
+ {
7022
+ model: modelForProvider,
7023
+ input: openAiInput,
7024
+ reasoning,
7025
+ text: openAiTextConfig,
7026
+ ...openAiTools ? { tools: openAiTools } : {},
7027
+ include: ["code_interpreter_call.outputs", "reasoning.encrypted_content"]
7028
+ },
7029
+ { signal }
5634
7030
  );
5635
- }
5636
- latestUsage = extractOpenAiUsageTokens(finalResponse.usage);
5637
- if (responseParts.length === 0) {
5638
- const fallback = extractOpenAiResponseParts(finalResponse);
5639
- blocked = blocked || fallback.blocked;
5640
- for (const part of fallback.parts) {
5641
- if (part.type === "text") {
5642
- pushDelta(part.thought === true ? "thought" : "response", part.text);
5643
- } else {
5644
- pushInline(part.data, part.mimeType);
7031
+ for await (const event of stream) {
7032
+ switch (event.type) {
7033
+ case "response.output_text.delta": {
7034
+ const delta = event.delta ?? "";
7035
+ pushDelta("response", typeof delta === "string" ? delta : "");
7036
+ break;
7037
+ }
7038
+ case "response.reasoning_summary_text.delta": {
7039
+ const delta = event.delta ?? "";
7040
+ pushDelta("thought", typeof delta === "string" ? delta : "");
7041
+ break;
7042
+ }
7043
+ case "response.refusal.delta": {
7044
+ blocked = true;
7045
+ queue.push({ type: "blocked" });
7046
+ break;
7047
+ }
7048
+ default:
7049
+ break;
5645
7050
  }
5646
7051
  }
5647
- }
5648
- }, modelForProvider);
5649
- } else if (provider === "chatgpt") {
5650
- const chatGptInput = toChatGptInput(contents);
5651
- const reasoningEffort = resolveOpenAiReasoningEffort(request.model, request.thinkingLevel);
5652
- const openAiTools = toOpenAiTools(request.tools);
5653
- const requestPayload = {
5654
- model: modelForProvider,
5655
- store: false,
5656
- stream: true,
5657
- ...providerInfo.serviceTier ? { service_tier: providerInfo.serviceTier } : {},
5658
- instructions: chatGptInput.instructions ?? "You are a helpful assistant.",
5659
- input: chatGptInput.input,
5660
- include: ["reasoning.encrypted_content"],
5661
- reasoning: {
5662
- effort: toOpenAiReasoningEffort(reasoningEffort),
5663
- summary: "detailed"
5664
- },
5665
- text: {
5666
- format: request.openAiTextFormat ?? { type: "text" },
5667
- verbosity: resolveOpenAiVerbosity(request.model)
5668
- },
5669
- ...openAiTools ? { tools: openAiTools } : {}
5670
- };
5671
- let sawResponseDelta = false;
5672
- let sawThoughtDelta = false;
5673
- const result = await collectChatGptCodexResponseWithRetry({
5674
- request: requestPayload,
5675
- signal,
5676
- onDelta: (delta) => {
5677
- if (delta.thoughtDelta) {
5678
- sawThoughtDelta = true;
5679
- pushDelta("thought", delta.thoughtDelta);
7052
+ const finalResponse = await stream.finalResponse();
7053
+ modelVersion = typeof finalResponse.model === "string" ? finalResponse.model : request.model;
7054
+ queue.push({ type: "model", modelVersion });
7055
+ if (finalResponse.error) {
7056
+ const message = typeof finalResponse.error.message === "string" ? finalResponse.error.message : "OpenAI response failed";
7057
+ throw new Error(message);
5680
7058
  }
5681
- if (delta.textDelta) {
5682
- sawResponseDelta = true;
5683
- pushDelta("response", delta.textDelta);
7059
+ if (finalResponse.status && finalResponse.status !== "completed" && finalResponse.status !== "in_progress") {
7060
+ const detail = finalResponse.incomplete_details?.reason;
7061
+ throw new Error(
7062
+ `OpenAI response status ${finalResponse.status}${detail ? ` (${detail})` : ""}`
7063
+ );
5684
7064
  }
5685
- }
5686
- });
5687
- blocked = blocked || result.blocked;
5688
- if (blocked) {
5689
- queue.push({ type: "blocked" });
5690
- }
5691
- if (result.model) {
5692
- modelVersion = providerInfo.serviceTier ? request.model : `chatgpt-${result.model}`;
5693
- queue.push({ type: "model", modelVersion });
5694
- }
5695
- latestUsage = extractChatGptUsageTokens(result.usage);
5696
- const fallbackText = typeof result.text === "string" ? result.text : "";
5697
- const fallbackThoughts = typeof result.reasoningSummaryText === "string" && result.reasoningSummaryText.length > 0 ? result.reasoningSummaryText : typeof result.reasoningText === "string" ? result.reasoningText : "";
5698
- if (!sawThoughtDelta && fallbackThoughts.length > 0) {
5699
- pushDelta("thought", fallbackThoughts);
5700
- }
5701
- if (!sawResponseDelta && fallbackText.length > 0) {
5702
- pushDelta("response", fallbackText);
5703
- }
5704
- } else if (provider === "fireworks") {
5705
- if (request.tools && request.tools.length > 0) {
5706
- throw new Error(
5707
- "Fireworks provider does not support provider-native tools in generateText; use runToolLoop for function tools."
5708
- );
5709
- }
5710
- const fireworksMessages = toFireworksMessages(contents, {
5711
- responseMimeType: request.responseMimeType,
5712
- responseJsonSchema: request.responseJsonSchema
5713
- });
5714
- await runFireworksCall(async (client) => {
5715
- const responseFormat = request.responseJsonSchema ? {
5716
- type: "json_schema",
5717
- json_schema: {
5718
- name: "llm-response",
5719
- schema: request.responseJsonSchema
7065
+ latestUsage = extractOpenAiUsageTokens(finalResponse.usage);
7066
+ if (responseParts.length === 0) {
7067
+ const fallback = extractOpenAiResponseParts(finalResponse);
7068
+ blocked = blocked || fallback.blocked;
7069
+ for (const part of fallback.parts) {
7070
+ if (part.type === "text") {
7071
+ pushDelta(part.thought === true ? "thought" : "response", part.text);
7072
+ } else if (part.type === "inlineData") {
7073
+ pushInline(part.data, part.mimeType);
7074
+ }
7075
+ }
5720
7076
  }
5721
- } : request.responseMimeType === "application/json" ? { type: "json_object" } : void 0;
5722
- const response = await client.chat.completions.create(
5723
- {
5724
- model: modelForProvider,
5725
- messages: fireworksMessages,
5726
- ...responseFormat ? { response_format: responseFormat } : {}
7077
+ }, modelForProvider);
7078
+ } else if (provider === "chatgpt") {
7079
+ const chatGptInput = toChatGptInput(contents);
7080
+ const reasoningEffort = resolveOpenAiReasoningEffort(request.model, request.thinkingLevel);
7081
+ const openAiTools = toOpenAiTools(request.tools);
7082
+ const requestPayload = {
7083
+ model: modelForProvider,
7084
+ store: false,
7085
+ stream: true,
7086
+ ...providerInfo.serviceTier ? { service_tier: providerInfo.serviceTier } : {},
7087
+ instructions: chatGptInput.instructions ?? "You are a helpful assistant.",
7088
+ input: chatGptInput.input,
7089
+ include: ["reasoning.encrypted_content"],
7090
+ reasoning: {
7091
+ effort: toOpenAiReasoningEffort(reasoningEffort),
7092
+ summary: "detailed"
5727
7093
  },
5728
- { signal }
5729
- );
5730
- modelVersion = typeof response.model === "string" ? response.model : request.model;
5731
- queue.push({ type: "model", modelVersion });
5732
- const choice = Array.isArray(response.choices) ? response.choices[0] : void 0;
5733
- if (choice?.finish_reason === "content_filter") {
5734
- blocked = true;
7094
+ text: {
7095
+ format: request.openAiTextFormat ?? { type: "text" },
7096
+ verbosity: resolveOpenAiVerbosity(request.model)
7097
+ },
7098
+ ...openAiTools ? { tools: openAiTools } : {}
7099
+ };
7100
+ let sawResponseDelta = false;
7101
+ let sawThoughtDelta = false;
7102
+ const result2 = await collectChatGptCodexResponseWithRetry({
7103
+ request: requestPayload,
7104
+ signal,
7105
+ onDelta: (delta) => {
7106
+ if (delta.thoughtDelta) {
7107
+ sawThoughtDelta = true;
7108
+ pushDelta("thought", delta.thoughtDelta);
7109
+ }
7110
+ if (delta.textDelta) {
7111
+ sawResponseDelta = true;
7112
+ pushDelta("response", delta.textDelta);
7113
+ }
7114
+ }
7115
+ });
7116
+ blocked = blocked || result2.blocked;
7117
+ if (blocked) {
5735
7118
  queue.push({ type: "blocked" });
5736
7119
  }
5737
- const textOutput = extractFireworksMessageText(
5738
- choice?.message
5739
- );
5740
- if (textOutput.length > 0) {
5741
- pushDelta("response", textOutput);
7120
+ if (result2.model) {
7121
+ modelVersion = providerInfo.serviceTier ? request.model : `chatgpt-${result2.model}`;
7122
+ queue.push({ type: "model", modelVersion });
5742
7123
  }
5743
- latestUsage = extractFireworksUsageTokens(response.usage);
5744
- }, modelForProvider);
5745
- } else {
5746
- const geminiContents = contents.map(convertLlmContentToGeminiContent);
5747
- const thinkingConfig = resolveGeminiThinkingConfig(modelForProvider, request.thinkingLevel);
5748
- const config = {
5749
- maxOutputTokens: 32e3,
5750
- ...thinkingConfig ? { thinkingConfig } : {},
5751
- ...request.responseMimeType ? { responseMimeType: request.responseMimeType } : {},
5752
- ...request.responseJsonSchema ? { responseJsonSchema: request.responseJsonSchema } : {},
5753
- ...request.responseModalities ? { responseModalities: Array.from(request.responseModalities) } : {},
5754
- ...request.imageAspectRatio || request.imageSize ? {
5755
- imageConfig: {
5756
- ...request.imageAspectRatio ? { aspectRatio: request.imageAspectRatio } : {},
5757
- ...request.imageSize ? { imageSize: request.imageSize } : {}
5758
- }
5759
- } : {}
5760
- };
5761
- const geminiTools = toGeminiTools(request.tools);
5762
- if (geminiTools) {
5763
- config.tools = geminiTools;
5764
- }
5765
- await runGeminiCall(async (client) => {
5766
- const stream = await client.models.generateContentStream({
5767
- model: modelForProvider,
5768
- contents: geminiContents,
5769
- config
7124
+ latestUsage = extractChatGptUsageTokens(result2.usage);
7125
+ const fallbackText = typeof result2.text === "string" ? result2.text : "";
7126
+ const fallbackThoughts = typeof result2.reasoningSummaryText === "string" && result2.reasoningSummaryText.length > 0 ? result2.reasoningSummaryText : typeof result2.reasoningText === "string" ? result2.reasoningText : "";
7127
+ if (!sawThoughtDelta && fallbackThoughts.length > 0) {
7128
+ pushDelta("thought", fallbackThoughts);
7129
+ }
7130
+ if (!sawResponseDelta && fallbackText.length > 0) {
7131
+ pushDelta("response", fallbackText);
7132
+ }
7133
+ } else if (provider === "fireworks") {
7134
+ if (request.tools && request.tools.length > 0) {
7135
+ throw new Error(
7136
+ "Fireworks provider does not support provider-native tools in generateText; use runToolLoop for function tools."
7137
+ );
7138
+ }
7139
+ const fireworksMessages = toFireworksMessages(contents, {
7140
+ responseMimeType: request.responseMimeType,
7141
+ responseJsonSchema: request.responseJsonSchema
5770
7142
  });
5771
- let latestGrounding;
5772
- for await (const chunk of stream) {
5773
- if (chunk.modelVersion) {
5774
- modelVersion = chunk.modelVersion;
5775
- queue.push({ type: "model", modelVersion });
5776
- }
5777
- if (chunk.promptFeedback?.blockReason) {
7143
+ await runFireworksCall(async (client) => {
7144
+ const responseFormat = request.responseJsonSchema ? {
7145
+ type: "json_schema",
7146
+ json_schema: {
7147
+ name: "llm-response",
7148
+ schema: request.responseJsonSchema
7149
+ }
7150
+ } : request.responseMimeType === "application/json" ? { type: "json_object" } : void 0;
7151
+ const response = await client.chat.completions.create(
7152
+ {
7153
+ model: modelForProvider,
7154
+ messages: fireworksMessages,
7155
+ ...responseFormat ? { response_format: responseFormat } : {}
7156
+ },
7157
+ { signal }
7158
+ );
7159
+ modelVersion = typeof response.model === "string" ? response.model : request.model;
7160
+ queue.push({ type: "model", modelVersion });
7161
+ const choice = Array.isArray(response.choices) ? response.choices[0] : void 0;
7162
+ if (choice?.finish_reason === "content_filter") {
5778
7163
  blocked = true;
5779
7164
  queue.push({ type: "blocked" });
5780
7165
  }
5781
- latestUsage = mergeTokenUpdates(
5782
- latestUsage,
5783
- extractGeminiUsageTokens(chunk.usageMetadata)
7166
+ const textOutput = extractFireworksMessageText(
7167
+ choice?.message
5784
7168
  );
5785
- const candidates = chunk.candidates;
5786
- if (!candidates || candidates.length === 0) {
5787
- continue;
7169
+ if (textOutput.length > 0) {
7170
+ pushDelta("response", textOutput);
5788
7171
  }
5789
- const primary = candidates[0];
5790
- if (primary && isModerationFinish(primary.finishReason)) {
5791
- blocked = true;
5792
- queue.push({ type: "blocked" });
5793
- }
5794
- for (const candidate of candidates) {
5795
- const candidateContent = candidate.content;
5796
- if (!candidateContent) {
5797
- continue;
7172
+ latestUsage = extractFireworksUsageTokens(response.usage);
7173
+ }, modelForProvider);
7174
+ } else {
7175
+ const geminiContents = await maybePrepareGeminiPromptContents(
7176
+ contents.map(convertLlmContentToGeminiContent)
7177
+ );
7178
+ const thinkingConfig = resolveGeminiThinkingConfig(modelForProvider, request.thinkingLevel);
7179
+ const config = {
7180
+ maxOutputTokens: 32e3,
7181
+ ...thinkingConfig ? { thinkingConfig } : {},
7182
+ ...request.responseMimeType ? { responseMimeType: request.responseMimeType } : {},
7183
+ ...request.responseJsonSchema ? { responseJsonSchema: request.responseJsonSchema } : {},
7184
+ ...request.responseModalities ? { responseModalities: Array.from(request.responseModalities) } : {},
7185
+ ...request.imageAspectRatio || request.imageSize ? {
7186
+ imageConfig: {
7187
+ ...request.imageAspectRatio ? { aspectRatio: request.imageAspectRatio } : {},
7188
+ ...request.imageSize ? { imageSize: request.imageSize } : {}
7189
+ }
7190
+ } : {}
7191
+ };
7192
+ const geminiTools = toGeminiTools(request.tools);
7193
+ if (geminiTools) {
7194
+ config.tools = geminiTools;
7195
+ }
7196
+ await runGeminiCall(async (client) => {
7197
+ const stream = await client.models.generateContentStream({
7198
+ model: modelForProvider,
7199
+ contents: geminiContents,
7200
+ config
7201
+ });
7202
+ let latestGrounding;
7203
+ for await (const chunk of stream) {
7204
+ if (chunk.modelVersion) {
7205
+ modelVersion = chunk.modelVersion;
7206
+ queue.push({ type: "model", modelVersion });
7207
+ }
7208
+ if (chunk.promptFeedback?.blockReason) {
7209
+ blocked = true;
7210
+ queue.push({ type: "blocked" });
5798
7211
  }
5799
- if (candidate.groundingMetadata) {
5800
- latestGrounding = candidate.groundingMetadata;
7212
+ latestUsage = mergeTokenUpdates(
7213
+ latestUsage,
7214
+ extractGeminiUsageTokens(chunk.usageMetadata)
7215
+ );
7216
+ const candidates = chunk.candidates;
7217
+ if (!candidates || candidates.length === 0) {
7218
+ continue;
5801
7219
  }
5802
- const content2 = convertGeminiContentToLlmContent(candidateContent);
5803
- if (!responseRole) {
5804
- responseRole = content2.role;
7220
+ const primary = candidates[0];
7221
+ if (primary && isModerationFinish(primary.finishReason)) {
7222
+ blocked = true;
7223
+ queue.push({ type: "blocked" });
5805
7224
  }
5806
- for (const part of content2.parts) {
5807
- if (part.type === "text") {
5808
- pushDelta(part.thought === true ? "thought" : "response", part.text);
5809
- } else {
5810
- pushInline(part.data, part.mimeType);
7225
+ for (const candidate of candidates) {
7226
+ const candidateContent = candidate.content;
7227
+ if (!candidateContent) {
7228
+ continue;
7229
+ }
7230
+ if (candidate.groundingMetadata) {
7231
+ latestGrounding = candidate.groundingMetadata;
7232
+ }
7233
+ const content2 = convertGeminiContentToLlmContent(candidateContent);
7234
+ if (!responseRole) {
7235
+ responseRole = content2.role;
7236
+ }
7237
+ for (const part of content2.parts) {
7238
+ if (part.type === "text") {
7239
+ pushDelta(part.thought === true ? "thought" : "response", part.text);
7240
+ } else if (part.type === "inlineData") {
7241
+ pushInline(part.data, part.mimeType);
7242
+ }
5811
7243
  }
5812
7244
  }
5813
7245
  }
7246
+ grounding = latestGrounding;
7247
+ }, modelForProvider);
7248
+ }
7249
+ const mergedParts = mergeConsecutiveTextParts(responseParts);
7250
+ const content = mergedParts.length > 0 ? { role: responseRole ?? "assistant", parts: mergedParts } : void 0;
7251
+ const { text, thoughts } = extractTextByChannel(content);
7252
+ const outputAttachments = collectLoggedAttachmentsFromLlmParts(mergedParts, "output");
7253
+ const costUsd = estimateCallCostUsd({
7254
+ modelId: modelVersion,
7255
+ tokens: latestUsage,
7256
+ responseImages,
7257
+ imageSize: request.imageSize
7258
+ });
7259
+ if (latestUsage) {
7260
+ queue.push({ type: "usage", usage: latestUsage, costUsd, modelVersion });
7261
+ }
7262
+ callLogger?.complete({
7263
+ responseText: text,
7264
+ attachments: outputAttachments,
7265
+ metadata: {
7266
+ provider,
7267
+ model: request.model,
7268
+ modelVersion,
7269
+ blocked,
7270
+ costUsd,
7271
+ usage: latestUsage,
7272
+ grounding: grounding ? sanitiseLogValue(grounding) : void 0,
7273
+ responseChars: text.length,
7274
+ thoughtChars: thoughts.length,
7275
+ responseImages,
7276
+ uploads: getCurrentFileUploadMetrics()
5814
7277
  }
5815
- grounding = latestGrounding;
5816
- }, modelForProvider);
5817
- }
5818
- const mergedParts = mergeConsecutiveTextParts(responseParts);
5819
- const content = mergedParts.length > 0 ? { role: responseRole ?? "assistant", parts: mergedParts } : void 0;
5820
- const { text, thoughts } = extractTextByChannel(content);
5821
- const outputAttachments = collectLoggedAttachmentsFromLlmParts(mergedParts, "output");
5822
- const costUsd = estimateCallCostUsd({
5823
- modelId: modelVersion,
5824
- tokens: latestUsage,
5825
- responseImages,
5826
- imageSize: request.imageSize
5827
- });
5828
- if (latestUsage) {
5829
- queue.push({ type: "usage", usage: latestUsage, costUsd, modelVersion });
5830
- }
5831
- callLogger?.complete({
5832
- responseText: text,
5833
- attachments: outputAttachments,
5834
- metadata: {
5835
- provider,
5836
- model: request.model,
5837
- modelVersion,
5838
- blocked,
5839
- costUsd,
5840
- usage: latestUsage,
5841
- grounding: grounding ? sanitiseLogValue(grounding) : void 0,
5842
- responseChars: text.length,
5843
- thoughtChars: thoughts.length,
5844
- responseImages
5845
- }
5846
- });
5847
- return {
5848
- provider,
5849
- model: request.model,
5850
- modelVersion,
5851
- content,
5852
- text,
5853
- thoughts,
5854
- blocked,
5855
- usage: latestUsage,
5856
- costUsd,
5857
- grounding
5858
- };
5859
- } catch (error) {
5860
- const partialParts = mergeConsecutiveTextParts(responseParts);
5861
- const partialContent = partialParts.length > 0 ? { role: responseRole ?? "assistant", parts: partialParts } : void 0;
5862
- const { text: partialText } = extractTextByChannel(partialContent);
5863
- callLogger?.fail(error, {
5864
- responseText: partialText,
5865
- attachments: collectLoggedAttachmentsFromLlmParts(partialParts, "output"),
5866
- metadata: {
7278
+ });
7279
+ return {
5867
7280
  provider,
5868
7281
  model: request.model,
5869
7282
  modelVersion,
7283
+ content,
7284
+ text,
7285
+ thoughts,
5870
7286
  blocked,
5871
7287
  usage: latestUsage,
5872
- partialResponseParts: responseParts.length,
5873
- responseImages
5874
- }
5875
- });
5876
- throw error;
5877
- }
7288
+ costUsd,
7289
+ grounding
7290
+ };
7291
+ } catch (error) {
7292
+ const partialParts = mergeConsecutiveTextParts(responseParts);
7293
+ const partialContent = partialParts.length > 0 ? { role: responseRole ?? "assistant", parts: partialParts } : void 0;
7294
+ const { text: partialText } = extractTextByChannel(partialContent);
7295
+ callLogger?.fail(error, {
7296
+ responseText: partialText,
7297
+ attachments: collectLoggedAttachmentsFromLlmParts(partialParts, "output"),
7298
+ metadata: {
7299
+ provider,
7300
+ model: request.model,
7301
+ modelVersion,
7302
+ blocked,
7303
+ usage: latestUsage,
7304
+ partialResponseParts: responseParts.length,
7305
+ responseImages,
7306
+ uploads: getCurrentFileUploadMetrics()
7307
+ }
7308
+ });
7309
+ throw error;
7310
+ }
7311
+ });
7312
+ return result;
5878
7313
  }
5879
7314
  function streamText(request) {
5880
7315
  const queue = createAsyncQueue();
@@ -6157,7 +7592,12 @@ function normalizeToolLoopSteeringInput(input) {
6157
7592
  if (part.type === "text") {
6158
7593
  parts.push({ type: "text", text: part.text });
6159
7594
  } else {
6160
- parts.push({ type: "inlineData", data: part.data, mimeType: part.mimeType });
7595
+ parts.push({
7596
+ type: "inlineData",
7597
+ data: part.data,
7598
+ mimeType: part.mimeType,
7599
+ filename: part.filename
7600
+ });
6161
7601
  }
6162
7602
  }
6163
7603
  if (parts.length > 0) {
@@ -6345,9 +7785,10 @@ async function runToolLoop(request) {
6345
7785
  let reasoningSummary = "";
6346
7786
  let stepToolCallText;
6347
7787
  let stepToolCallPayload;
7788
+ const preparedInput = await maybePrepareOpenAiPromptInput(input);
6348
7789
  const stepRequestPayload = {
6349
7790
  model: providerInfo.model,
6350
- input,
7791
+ input: preparedInput,
6351
7792
  ...previousResponseId ? { previous_response_id: previousResponseId } : {},
6352
7793
  ...openAiTools.length > 0 ? { tools: openAiTools } : {},
6353
7794
  ...openAiTools.length > 0 ? { parallel_tool_calls: true } : {},
@@ -6375,7 +7816,7 @@ async function runToolLoop(request) {
6375
7816
  const stream = client.responses.stream(
6376
7817
  {
6377
7818
  model: providerInfo.model,
6378
- input,
7819
+ input: preparedInput,
6379
7820
  ...previousResponseId ? { previous_response_id: previousResponseId } : {},
6380
7821
  ...openAiTools.length > 0 ? { tools: openAiTools } : {},
6381
7822
  ...openAiTools.length > 0 ? { parallel_tool_calls: true } : {},
@@ -6552,27 +7993,29 @@ async function runToolLoop(request) {
6552
7993
  input: entry.value
6553
7994
  });
6554
7995
  }
6555
- const callResults = await Promise.all(
6556
- callInputs.map(async (entry) => {
6557
- return await toolCallContextStorage.run(
6558
- {
6559
- toolName: entry.toolName,
6560
- toolId: entry.toolId,
6561
- turn: entry.turn,
6562
- toolIndex: entry.toolIndex
6563
- },
6564
- async () => {
6565
- const { result, outputPayload } = await executeToolCall({
6566
- callKind: entry.call.kind,
7996
+ const callResults = await maybeSpillCombinedToolCallOutputs(
7997
+ await Promise.all(
7998
+ callInputs.map(async (entry) => {
7999
+ return await toolCallContextStorage.run(
8000
+ {
6567
8001
  toolName: entry.toolName,
6568
- tool: request.tools[entry.toolName],
6569
- rawInput: entry.value,
6570
- parseError: entry.parseError
6571
- });
6572
- return { entry, result, outputPayload };
6573
- }
6574
- );
6575
- })
8002
+ toolId: entry.toolId,
8003
+ turn: entry.turn,
8004
+ toolIndex: entry.toolIndex
8005
+ },
8006
+ async () => {
8007
+ const { result, outputPayload } = await executeToolCall({
8008
+ callKind: entry.call.kind,
8009
+ toolName: entry.toolName,
8010
+ tool: request.tools[entry.toolName],
8011
+ rawInput: entry.value,
8012
+ parseError: entry.parseError
8013
+ });
8014
+ return { entry, result, outputPayload };
8015
+ }
8016
+ );
8017
+ })
8018
+ )
6576
8019
  );
6577
8020
  const toolOutputs = [];
6578
8021
  let toolExecutionMs = 0;
@@ -6853,27 +8296,29 @@ async function runToolLoop(request) {
6853
8296
  input: entry.value
6854
8297
  });
6855
8298
  }
6856
- const callResults = await Promise.all(
6857
- callInputs.map(async (entry) => {
6858
- return await toolCallContextStorage.run(
6859
- {
6860
- toolName: entry.toolName,
6861
- toolId: entry.toolId,
6862
- turn: entry.turn,
6863
- toolIndex: entry.toolIndex
6864
- },
6865
- async () => {
6866
- const { result, outputPayload } = await executeToolCall({
6867
- callKind: entry.call.kind,
8299
+ const callResults = await maybeSpillCombinedToolCallOutputs(
8300
+ await Promise.all(
8301
+ callInputs.map(async (entry) => {
8302
+ return await toolCallContextStorage.run(
8303
+ {
6868
8304
  toolName: entry.toolName,
6869
- tool: request.tools[entry.toolName],
6870
- rawInput: entry.value,
6871
- parseError: entry.parseError
6872
- });
6873
- return { entry, result, outputPayload };
6874
- }
6875
- );
6876
- })
8305
+ toolId: entry.toolId,
8306
+ turn: entry.turn,
8307
+ toolIndex: entry.toolIndex
8308
+ },
8309
+ async () => {
8310
+ const { result, outputPayload } = await executeToolCall({
8311
+ callKind: entry.call.kind,
8312
+ toolName: entry.toolName,
8313
+ tool: request.tools[entry.toolName],
8314
+ rawInput: entry.value,
8315
+ parseError: entry.parseError
8316
+ });
8317
+ return { entry, result, outputPayload };
8318
+ }
8319
+ );
8320
+ })
8321
+ )
6877
8322
  );
6878
8323
  let toolExecutionMs = 0;
6879
8324
  let waitToolMs = 0;
@@ -7145,27 +8590,29 @@ async function runToolLoop(request) {
7145
8590
  input: entry.value
7146
8591
  });
7147
8592
  }
7148
- const callResults = await Promise.all(
7149
- callInputs.map(async (entry) => {
7150
- return await toolCallContextStorage.run(
7151
- {
7152
- toolName: entry.toolName,
7153
- toolId: entry.toolId,
7154
- turn: entry.turn,
7155
- toolIndex: entry.toolIndex
7156
- },
7157
- async () => {
7158
- const { result, outputPayload } = await executeToolCall({
7159
- callKind: "function",
8593
+ const callResults = await maybeSpillCombinedToolCallOutputs(
8594
+ await Promise.all(
8595
+ callInputs.map(async (entry) => {
8596
+ return await toolCallContextStorage.run(
8597
+ {
7160
8598
  toolName: entry.toolName,
7161
- tool: request.tools[entry.toolName],
7162
- rawInput: entry.value,
7163
- parseError: entry.parseError
7164
- });
7165
- return { entry, result, outputPayload };
7166
- }
7167
- );
7168
- })
8599
+ toolId: entry.toolId,
8600
+ turn: entry.turn,
8601
+ toolIndex: entry.toolIndex
8602
+ },
8603
+ async () => {
8604
+ const { result, outputPayload } = await executeToolCall({
8605
+ callKind: "function",
8606
+ toolName: entry.toolName,
8607
+ tool: request.tools[entry.toolName],
8608
+ rawInput: entry.value,
8609
+ parseError: entry.parseError
8610
+ });
8611
+ return { entry, result, outputPayload };
8612
+ }
8613
+ );
8614
+ })
8615
+ )
7169
8616
  );
7170
8617
  const assistantToolCalls = [];
7171
8618
  const toolMessages = [];
@@ -7304,9 +8751,10 @@ async function runToolLoop(request) {
7304
8751
  ...thinkingConfig ? { thinkingConfig } : {}
7305
8752
  };
7306
8753
  const onEvent = request.onEvent;
8754
+ const preparedGeminiContents = await maybePrepareGeminiPromptContents(geminiContents);
7307
8755
  const stepRequestPayload = {
7308
8756
  model: request.model,
7309
- contents: geminiContents,
8757
+ contents: preparedGeminiContents,
7310
8758
  config
7311
8759
  };
7312
8760
  const stepCallLogger = startLlmCallLoggerFromPayload({
@@ -7320,7 +8768,7 @@ async function runToolLoop(request) {
7320
8768
  async (client) => {
7321
8769
  const stream = await client.models.generateContentStream({
7322
8770
  model: request.model,
7323
- contents: geminiContents,
8771
+ contents: preparedGeminiContents,
7324
8772
  config
7325
8773
  });
7326
8774
  let responseText2 = "";
@@ -7496,26 +8944,28 @@ async function runToolLoop(request) {
7496
8944
  input: entry.rawInput
7497
8945
  });
7498
8946
  }
7499
- const callResults = await Promise.all(
7500
- callInputs.map(async (entry) => {
7501
- return await toolCallContextStorage.run(
7502
- {
7503
- toolName: entry.toolName,
7504
- toolId: entry.toolId,
7505
- turn: entry.turn,
7506
- toolIndex: entry.toolIndex
7507
- },
7508
- async () => {
7509
- const { result, outputPayload } = await executeToolCall({
7510
- callKind: "function",
8947
+ const callResults = await maybeSpillCombinedToolCallOutputs(
8948
+ await Promise.all(
8949
+ callInputs.map(async (entry) => {
8950
+ return await toolCallContextStorage.run(
8951
+ {
7511
8952
  toolName: entry.toolName,
7512
- tool: request.tools[entry.toolName],
7513
- rawInput: entry.rawInput
7514
- });
7515
- return { entry, result, outputPayload };
7516
- }
7517
- );
7518
- })
8953
+ toolId: entry.toolId,
8954
+ turn: entry.turn,
8955
+ toolIndex: entry.toolIndex
8956
+ },
8957
+ async () => {
8958
+ const { result, outputPayload } = await executeToolCall({
8959
+ callKind: "function",
8960
+ toolName: entry.toolName,
8961
+ tool: request.tools[entry.toolName],
8962
+ rawInput: entry.rawInput
8963
+ });
8964
+ return { entry, result, outputPayload };
8965
+ }
8966
+ );
8967
+ })
8968
+ )
7519
8969
  );
7520
8970
  let toolExecutionMs = 0;
7521
8971
  let waitToolMs = 0;
@@ -7920,7 +9370,7 @@ ${lines}`;
7920
9370
 
7921
9371
  // src/agent.ts
7922
9372
  import { randomBytes as randomBytes3 } from "crypto";
7923
- import path7 from "path";
9373
+ import path9 from "path";
7924
9374
 
7925
9375
  // src/agent/subagents.ts
7926
9376
  import { randomBytes as randomBytes2 } from "crypto";
@@ -8395,26 +9845,26 @@ function resolveInputItemsText(items) {
8395
9845
  }
8396
9846
  const itemType = typeof item.type === "string" ? item.type.trim() : "";
8397
9847
  const name = typeof item.name === "string" ? item.name.trim() : "";
8398
- const path8 = typeof item.path === "string" ? item.path.trim() : "";
9848
+ const path10 = typeof item.path === "string" ? item.path.trim() : "";
8399
9849
  const imageUrl = typeof item.image_url === "string" ? item.image_url.trim() : "";
8400
9850
  if (itemType === "image") {
8401
9851
  lines.push("[image]");
8402
9852
  continue;
8403
9853
  }
8404
- if (itemType === "local_image" && path8) {
8405
- lines.push(`[local_image:${path8}]`);
9854
+ if (itemType === "local_image" && path10) {
9855
+ lines.push(`[local_image:${path10}]`);
8406
9856
  continue;
8407
9857
  }
8408
- if (itemType === "skill" && name && path8) {
8409
- lines.push(`[skill:$${name}](${path8})`);
9858
+ if (itemType === "skill" && name && path10) {
9859
+ lines.push(`[skill:$${name}](${path10})`);
8410
9860
  continue;
8411
9861
  }
8412
- if (itemType === "mention" && name && path8) {
8413
- lines.push(`[mention:$${name}](${path8})`);
9862
+ if (itemType === "mention" && name && path10) {
9863
+ lines.push(`[mention:$${name}](${path10})`);
8414
9864
  continue;
8415
9865
  }
8416
- if (path8 || imageUrl) {
8417
- lines.push(`[${itemType || "input"}:${path8 || imageUrl}]`);
9866
+ if (path10 || imageUrl) {
9867
+ lines.push(`[${itemType || "input"}:${path10 || imageUrl}]`);
8418
9868
  continue;
8419
9869
  }
8420
9870
  if (name) {
@@ -8732,27 +10182,27 @@ function sleep2(ms) {
8732
10182
  }
8733
10183
 
8734
10184
  // src/tools/filesystemTools.ts
8735
- import path6 from "path";
8736
- import { Buffer as Buffer5 } from "buffer";
10185
+ import path8 from "path";
10186
+ import { Buffer as Buffer6 } from "buffer";
8737
10187
  import { z as z6 } from "zod";
8738
10188
 
8739
10189
  // src/tools/applyPatch.ts
8740
- import path5 from "path";
10190
+ import path7 from "path";
8741
10191
  import { z as z5 } from "zod";
8742
10192
 
8743
10193
  // src/tools/filesystem.ts
8744
10194
  import { promises as fs3 } from "fs";
8745
- import path4 from "path";
10195
+ import path6 from "path";
8746
10196
  var InMemoryAgentFilesystem = class {
8747
10197
  #files = /* @__PURE__ */ new Map();
8748
10198
  #dirs = /* @__PURE__ */ new Map();
8749
10199
  #clock = 0;
8750
10200
  constructor(initialFiles = {}) {
8751
- const root = path4.resolve("/");
10201
+ const root = path6.resolve("/");
8752
10202
  this.#dirs.set(root, { mtimeMs: this.#nextMtime() });
8753
10203
  for (const [filePath, content] of Object.entries(initialFiles)) {
8754
- const absolutePath = path4.resolve(filePath);
8755
- this.#ensureDirSync(path4.dirname(absolutePath));
10204
+ const absolutePath = path6.resolve(filePath);
10205
+ this.#ensureDirSync(path6.dirname(absolutePath));
8756
10206
  this.#files.set(absolutePath, {
8757
10207
  content,
8758
10208
  mtimeMs: this.#nextMtime()
@@ -8760,7 +10210,7 @@ var InMemoryAgentFilesystem = class {
8760
10210
  }
8761
10211
  }
8762
10212
  async readTextFile(filePath) {
8763
- const absolutePath = path4.resolve(filePath);
10213
+ const absolutePath = path6.resolve(filePath);
8764
10214
  const file = this.#files.get(absolutePath);
8765
10215
  if (!file) {
8766
10216
  throw createNoSuchFileError("open", absolutePath);
@@ -8772,24 +10222,24 @@ var InMemoryAgentFilesystem = class {
8772
10222
  return Buffer.from(content, "utf8");
8773
10223
  }
8774
10224
  async writeTextFile(filePath, content) {
8775
- const absolutePath = path4.resolve(filePath);
8776
- const parentPath = path4.dirname(absolutePath);
10225
+ const absolutePath = path6.resolve(filePath);
10226
+ const parentPath = path6.dirname(absolutePath);
8777
10227
  if (!this.#dirs.has(parentPath)) {
8778
10228
  throw createNoSuchFileError("open", parentPath);
8779
10229
  }
8780
10230
  this.#files.set(absolutePath, { content, mtimeMs: this.#nextMtime() });
8781
10231
  }
8782
10232
  async deleteFile(filePath) {
8783
- const absolutePath = path4.resolve(filePath);
10233
+ const absolutePath = path6.resolve(filePath);
8784
10234
  if (!this.#files.delete(absolutePath)) {
8785
10235
  throw createNoSuchFileError("unlink", absolutePath);
8786
10236
  }
8787
10237
  }
8788
10238
  async ensureDir(directoryPath) {
8789
- this.#ensureDirSync(path4.resolve(directoryPath));
10239
+ this.#ensureDirSync(path6.resolve(directoryPath));
8790
10240
  }
8791
10241
  async readDir(directoryPath) {
8792
- const absolutePath = path4.resolve(directoryPath);
10242
+ const absolutePath = path6.resolve(directoryPath);
8793
10243
  const directory = this.#dirs.get(absolutePath);
8794
10244
  if (!directory) {
8795
10245
  throw createNoSuchFileError("scandir", absolutePath);
@@ -8800,10 +10250,10 @@ var InMemoryAgentFilesystem = class {
8800
10250
  if (dirPath === absolutePath) {
8801
10251
  continue;
8802
10252
  }
8803
- if (path4.dirname(dirPath) !== absolutePath) {
10253
+ if (path6.dirname(dirPath) !== absolutePath) {
8804
10254
  continue;
8805
10255
  }
8806
- const name = path4.basename(dirPath);
10256
+ const name = path6.basename(dirPath);
8807
10257
  if (seenNames.has(name)) {
8808
10258
  continue;
8809
10259
  }
@@ -8816,10 +10266,10 @@ var InMemoryAgentFilesystem = class {
8816
10266
  });
8817
10267
  }
8818
10268
  for (const [filePath, fileRecord] of this.#files.entries()) {
8819
- if (path4.dirname(filePath) !== absolutePath) {
10269
+ if (path6.dirname(filePath) !== absolutePath) {
8820
10270
  continue;
8821
10271
  }
8822
- const name = path4.basename(filePath);
10272
+ const name = path6.basename(filePath);
8823
10273
  if (seenNames.has(name)) {
8824
10274
  continue;
8825
10275
  }
@@ -8835,7 +10285,7 @@ var InMemoryAgentFilesystem = class {
8835
10285
  return entries;
8836
10286
  }
8837
10287
  async stat(entryPath) {
8838
- const absolutePath = path4.resolve(entryPath);
10288
+ const absolutePath = path6.resolve(entryPath);
8839
10289
  const file = this.#files.get(absolutePath);
8840
10290
  if (file) {
8841
10291
  return { kind: "file", mtimeMs: file.mtimeMs };
@@ -8851,7 +10301,7 @@ var InMemoryAgentFilesystem = class {
8851
10301
  return Object.fromEntries(entries.map(([filePath, record]) => [filePath, record.content]));
8852
10302
  }
8853
10303
  #ensureDirSync(directoryPath) {
8854
- const absolutePath = path4.resolve(directoryPath);
10304
+ const absolutePath = path6.resolve(directoryPath);
8855
10305
  const parts = [];
8856
10306
  let cursor = absolutePath;
8857
10307
  for (; ; ) {
@@ -8859,7 +10309,7 @@ var InMemoryAgentFilesystem = class {
8859
10309
  break;
8860
10310
  }
8861
10311
  parts.push(cursor);
8862
- const parent = path4.dirname(cursor);
10312
+ const parent = path6.dirname(cursor);
8863
10313
  if (parent === cursor) {
8864
10314
  break;
8865
10315
  }
@@ -8893,7 +10343,7 @@ function createNodeAgentFilesystem() {
8893
10343
  const entries = await fs3.readdir(directoryPath, { withFileTypes: true });
8894
10344
  const result = [];
8895
10345
  for (const entry of entries) {
8896
- const entryPath = path4.resolve(directoryPath, entry.name);
10346
+ const entryPath = path6.resolve(directoryPath, entry.name);
8897
10347
  const stats = await fs3.lstat(entryPath);
8898
10348
  result.push({
8899
10349
  name: entry.name,
@@ -9057,7 +10507,7 @@ function createApplyPatchTool(options = {}) {
9057
10507
  });
9058
10508
  }
9059
10509
  async function applyPatch(request) {
9060
- const cwd = path5.resolve(request.cwd ?? process.cwd());
10510
+ const cwd = path7.resolve(request.cwd ?? process.cwd());
9061
10511
  const adapter = request.fs ?? createNodeAgentFilesystem();
9062
10512
  const allowOutsideCwd = request.allowOutsideCwd === true;
9063
10513
  const patchBytes = Buffer.byteLength(request.patch, "utf8");
@@ -9079,7 +10529,7 @@ async function applyPatch(request) {
9079
10529
  kind: "add",
9080
10530
  path: absolutePath2
9081
10531
  });
9082
- await adapter.ensureDir(path5.dirname(absolutePath2));
10532
+ await adapter.ensureDir(path7.dirname(absolutePath2));
9083
10533
  await adapter.writeTextFile(absolutePath2, operation.content);
9084
10534
  added.push(toDisplayPath(absolutePath2, cwd));
9085
10535
  continue;
@@ -9113,7 +10563,7 @@ async function applyPatch(request) {
9113
10563
  fromPath: absolutePath,
9114
10564
  toPath: destinationPath
9115
10565
  });
9116
- await adapter.ensureDir(path5.dirname(destinationPath));
10566
+ await adapter.ensureDir(path7.dirname(destinationPath));
9117
10567
  await adapter.writeTextFile(destinationPath, next);
9118
10568
  await adapter.deleteFile(absolutePath);
9119
10569
  modified.push(toDisplayPath(destinationPath, cwd));
@@ -9144,22 +10594,22 @@ function resolvePatchPath(rawPath, cwd, allowOutsideCwd) {
9144
10594
  if (trimmed.length === 0) {
9145
10595
  throw new Error("apply_patch failed: empty file path");
9146
10596
  }
9147
- const absolutePath = path5.isAbsolute(trimmed) ? path5.resolve(trimmed) : path5.resolve(cwd, trimmed);
10597
+ const absolutePath = path7.isAbsolute(trimmed) ? path7.resolve(trimmed) : path7.resolve(cwd, trimmed);
9148
10598
  if (!allowOutsideCwd && !isPathInsideCwd(absolutePath, cwd)) {
9149
10599
  throw new Error(`apply_patch failed: path "${trimmed}" resolves outside cwd "${cwd}"`);
9150
10600
  }
9151
10601
  return absolutePath;
9152
10602
  }
9153
10603
  function isPathInsideCwd(candidatePath, cwd) {
9154
- const relative = path5.relative(cwd, candidatePath);
9155
- return relative === "" || !relative.startsWith("..") && !path5.isAbsolute(relative);
10604
+ const relative = path7.relative(cwd, candidatePath);
10605
+ return relative === "" || !relative.startsWith("..") && !path7.isAbsolute(relative);
9156
10606
  }
9157
10607
  function toDisplayPath(absolutePath, cwd) {
9158
- const relative = path5.relative(cwd, absolutePath);
10608
+ const relative = path7.relative(cwd, absolutePath);
9159
10609
  if (relative === "") {
9160
10610
  return ".";
9161
10611
  }
9162
- if (!relative.startsWith("..") && !path5.isAbsolute(relative)) {
10612
+ if (!relative.startsWith("..") && !path7.isAbsolute(relative)) {
9163
10613
  return relative;
9164
10614
  }
9165
10615
  return absolutePath;
@@ -9926,7 +11376,7 @@ async function readBinaryFile(filesystem, filePath) {
9926
11376
  return await filesystem.readBinaryFile(filePath);
9927
11377
  }
9928
11378
  const text = await filesystem.readTextFile(filePath);
9929
- return Buffer5.from(text, "utf8");
11379
+ return Buffer6.from(text, "utf8");
9930
11380
  }
9931
11381
  function detectImageMimeType(buffer, filePath) {
9932
11382
  if (buffer.length >= 8 && buffer[0] === 137 && buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71 && buffer[4] === 13 && buffer[5] === 10 && buffer[6] === 26 && buffer[7] === 10) {
@@ -9944,7 +11394,7 @@ function detectImageMimeType(buffer, filePath) {
9944
11394
  if (buffer.length >= 12 && buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP") {
9945
11395
  return "image/webp";
9946
11396
  }
9947
- const fromExtension = IMAGE_MIME_BY_EXTENSION[path6.extname(filePath).toLowerCase()];
11397
+ const fromExtension = IMAGE_MIME_BY_EXTENSION[path8.extname(filePath).toLowerCase()];
9948
11398
  if (fromExtension && SUPPORTED_IMAGE_MIME_TYPES.has(fromExtension)) {
9949
11399
  return fromExtension;
9950
11400
  }
@@ -9954,13 +11404,13 @@ function isPdfFile(buffer, filePath) {
9954
11404
  if (buffer.length >= 5 && buffer.subarray(0, 5).toString("ascii") === "%PDF-") {
9955
11405
  return true;
9956
11406
  }
9957
- return path6.extname(filePath).toLowerCase() === ".pdf";
11407
+ return path8.extname(filePath).toLowerCase() === ".pdf";
9958
11408
  }
9959
11409
  function isValidUtf8(buffer) {
9960
11410
  if (buffer.length === 0) {
9961
11411
  return true;
9962
11412
  }
9963
- return Buffer5.from(buffer.toString("utf8"), "utf8").equals(buffer);
11413
+ return Buffer6.from(buffer.toString("utf8"), "utf8").equals(buffer);
9964
11414
  }
9965
11415
  async function readFileGemini(input, options) {
9966
11416
  const runtime = resolveRuntime(options);
@@ -9992,7 +11442,7 @@ async function writeFileGemini(input, options) {
9992
11442
  action: "write",
9993
11443
  path: filePath
9994
11444
  });
9995
- await runtime.filesystem.ensureDir(path6.dirname(filePath));
11445
+ await runtime.filesystem.ensureDir(path8.dirname(filePath));
9996
11446
  await runtime.filesystem.writeTextFile(filePath, input.content);
9997
11447
  return `Successfully wrote file: ${toDisplayPath2(filePath, runtime.cwd)}`;
9998
11448
  }
@@ -10013,7 +11463,7 @@ async function replaceFileContentGemini(input, options) {
10013
11463
  originalContent = await runtime.filesystem.readTextFile(filePath);
10014
11464
  } catch (error) {
10015
11465
  if (isNoEntError(error) && oldValue.length === 0) {
10016
- await runtime.filesystem.ensureDir(path6.dirname(filePath));
11466
+ await runtime.filesystem.ensureDir(path8.dirname(filePath));
10017
11467
  await runtime.filesystem.writeTextFile(filePath, newValue);
10018
11468
  return `Successfully wrote new file: ${toDisplayPath2(filePath, runtime.cwd)}`;
10019
11469
  }
@@ -10183,15 +11633,15 @@ async function globFilesGemini(input, options) {
10183
11633
  throw new Error(`Path is not a directory: ${dirPath}`);
10184
11634
  }
10185
11635
  const matcher = createGlobMatcher(input.pattern, input.case_sensitive === true);
10186
- const files = await collectSearchFiles({
11636
+ const files2 = await collectSearchFiles({
10187
11637
  filesystem: runtime.filesystem,
10188
11638
  searchPath: dirPath,
10189
11639
  rootKind: "directory",
10190
11640
  maxScannedFiles: runtime.grepMaxScannedFiles
10191
11641
  });
10192
11642
  const matched = [];
10193
- for (const filePath of files) {
10194
- const relativePath = normalizeSlashes(path6.relative(dirPath, filePath));
11643
+ for (const filePath of files2) {
11644
+ const relativePath = normalizeSlashes(path8.relative(dirPath, filePath));
10195
11645
  if (!matcher(relativePath)) {
10196
11646
  continue;
10197
11647
  }
@@ -10209,7 +11659,7 @@ async function globFilesGemini(input, options) {
10209
11659
  }
10210
11660
  function resolveRuntime(options) {
10211
11661
  return {
10212
- cwd: path6.resolve(options.cwd ?? process.cwd()),
11662
+ cwd: path8.resolve(options.cwd ?? process.cwd()),
10213
11663
  filesystem: options.fs ?? createNodeAgentFilesystem(),
10214
11664
  allowOutsideCwd: options.allowOutsideCwd === true,
10215
11665
  checkAccess: options.checkAccess,
@@ -10240,13 +11690,13 @@ function mapApplyPatchAction(action) {
10240
11690
  return "move";
10241
11691
  }
10242
11692
  function resolvePathWithPolicy(inputPath, cwd, allowOutsideCwd) {
10243
- const absolutePath = path6.isAbsolute(inputPath) ? path6.resolve(inputPath) : path6.resolve(cwd, inputPath);
11693
+ const absolutePath = path8.isAbsolute(inputPath) ? path8.resolve(inputPath) : path8.resolve(cwd, inputPath);
10244
11694
  if (allowOutsideCwd || isPathInsideCwd2(absolutePath, cwd)) {
10245
11695
  return absolutePath;
10246
11696
  }
10247
- if (path6.isAbsolute(inputPath)) {
11697
+ if (path8.isAbsolute(inputPath)) {
10248
11698
  const sandboxRelativePath = inputPath.replace(/^[/\\]+/, "");
10249
- const sandboxRootedPath = path6.resolve(cwd, sandboxRelativePath);
11699
+ const sandboxRootedPath = path8.resolve(cwd, sandboxRelativePath);
10250
11700
  if (isPathInsideCwd2(sandboxRootedPath, cwd)) {
10251
11701
  return sandboxRootedPath;
10252
11702
  }
@@ -10254,25 +11704,25 @@ function resolvePathWithPolicy(inputPath, cwd, allowOutsideCwd) {
10254
11704
  throw new Error(`path "${inputPath}" resolves outside cwd "${cwd}"`);
10255
11705
  }
10256
11706
  function isPathInsideCwd2(candidatePath, cwd) {
10257
- const relative = path6.relative(cwd, candidatePath);
10258
- return relative === "" || !relative.startsWith("..") && !path6.isAbsolute(relative);
11707
+ const relative = path8.relative(cwd, candidatePath);
11708
+ return relative === "" || !relative.startsWith("..") && !path8.isAbsolute(relative);
10259
11709
  }
10260
11710
  function toDisplayPath2(absolutePath, cwd) {
10261
- const relative = path6.relative(cwd, absolutePath);
11711
+ const relative = path8.relative(cwd, absolutePath);
10262
11712
  if (relative === "") {
10263
11713
  return ".";
10264
11714
  }
10265
- if (!relative.startsWith("..") && !path6.isAbsolute(relative)) {
11715
+ if (!relative.startsWith("..") && !path8.isAbsolute(relative)) {
10266
11716
  return relative;
10267
11717
  }
10268
11718
  return absolutePath;
10269
11719
  }
10270
11720
  function toSandboxDisplayPath(absolutePath, cwd) {
10271
- const relative = path6.relative(cwd, absolutePath);
11721
+ const relative = path8.relative(cwd, absolutePath);
10272
11722
  if (relative === "") {
10273
11723
  return "/";
10274
11724
  }
10275
- if (!relative.startsWith("..") && !path6.isAbsolute(relative)) {
11725
+ if (!relative.startsWith("..") && !path8.isAbsolute(relative)) {
10276
11726
  return `/${normalizeSlashes(relative)}`;
10277
11727
  }
10278
11728
  return normalizeSlashes(absolutePath);
@@ -10348,7 +11798,7 @@ async function collectSearchFiles(params) {
10348
11798
  return [searchPath];
10349
11799
  }
10350
11800
  const queue = [searchPath];
10351
- const files = [];
11801
+ const files2 = [];
10352
11802
  while (queue.length > 0) {
10353
11803
  const current = queue.shift();
10354
11804
  if (!current) {
@@ -10363,13 +11813,13 @@ async function collectSearchFiles(params) {
10363
11813
  if (entry.kind !== "file") {
10364
11814
  continue;
10365
11815
  }
10366
- files.push(entry.path);
10367
- if (files.length >= maxScannedFiles) {
10368
- return files;
11816
+ files2.push(entry.path);
11817
+ if (files2.length >= maxScannedFiles) {
11818
+ return files2;
10369
11819
  }
10370
11820
  }
10371
11821
  }
10372
- return files;
11822
+ return files2;
10373
11823
  }
10374
11824
  function compileRegex(pattern, flags = "m") {
10375
11825
  try {
@@ -10388,7 +11838,7 @@ function createGlobMatcher(pattern, caseSensitive = false) {
10388
11838
  }));
10389
11839
  return (candidatePath) => {
10390
11840
  const normalizedPath = normalizeSlashes(candidatePath);
10391
- const basename = path6.posix.basename(normalizedPath);
11841
+ const basename = path8.posix.basename(normalizedPath);
10392
11842
  return compiled.some(
10393
11843
  (entry) => entry.regex.test(entry.applyToBasename ? basename : normalizedPath)
10394
11844
  );
@@ -10665,13 +12115,24 @@ async function runAgentLoopInternal(request, context) {
10665
12115
  }
10666
12116
  streamEventLogger?.appendEvent(event);
10667
12117
  } : void 0;
12118
+ let uploadMetrics = emptyFileUploadMetrics();
10668
12119
  try {
10669
- const result = await runToolLoop({
10670
- ...toolLoopRequestWithSteering,
10671
- ...instructions ? { instructions } : {},
10672
- ...wrappedOnEvent ? { onEvent: wrappedOnEvent } : {},
10673
- tools: mergedTools
12120
+ let result;
12121
+ await collectFileUploadMetrics(async () => {
12122
+ try {
12123
+ result = await runToolLoop({
12124
+ ...toolLoopRequestWithSteering,
12125
+ ...instructions ? { instructions } : {},
12126
+ ...wrappedOnEvent ? { onEvent: wrappedOnEvent } : {},
12127
+ tools: mergedTools
12128
+ });
12129
+ } finally {
12130
+ uploadMetrics = getCurrentFileUploadMetrics();
12131
+ }
10674
12132
  });
12133
+ if (!result) {
12134
+ throw new Error("runToolLoop returned no result.");
12135
+ }
10675
12136
  streamEventLogger?.flush();
10676
12137
  emitTelemetry({
10677
12138
  type: "agent.run.completed",
@@ -10680,7 +12141,10 @@ async function runAgentLoopInternal(request, context) {
10680
12141
  stepCount: result.steps.length,
10681
12142
  toolCallCount: countToolCalls(result),
10682
12143
  totalCostUsd: result.totalCostUsd,
10683
- usage: summarizeResultUsage(result)
12144
+ usage: summarizeResultUsage(result),
12145
+ uploadCount: uploadMetrics.count,
12146
+ uploadBytes: uploadMetrics.totalBytes,
12147
+ uploadLatencyMs: uploadMetrics.totalLatencyMs
10684
12148
  });
10685
12149
  loggingSession?.logLine(
10686
12150
  [
@@ -10689,7 +12153,10 @@ async function runAgentLoopInternal(request, context) {
10689
12153
  `durationMs=${Math.max(0, Date.now() - startedAtMs).toString()}`,
10690
12154
  `steps=${result.steps.length.toString()}`,
10691
12155
  `toolCalls=${countToolCalls(result).toString()}`,
10692
- `totalCostUsd=${(result.totalCostUsd ?? 0).toFixed(6)}`
12156
+ `totalCostUsd=${(result.totalCostUsd ?? 0).toFixed(6)}`,
12157
+ `uploadCount=${uploadMetrics.count.toString()}`,
12158
+ `uploadBytes=${uploadMetrics.totalBytes.toString()}`,
12159
+ `uploadLatencyMs=${uploadMetrics.totalLatencyMs.toString()}`
10693
12160
  ].join(" ")
10694
12161
  );
10695
12162
  for (const step of result.steps) {
@@ -10710,6 +12177,9 @@ async function runAgentLoopInternal(request, context) {
10710
12177
  type: "agent.run.completed",
10711
12178
  success: false,
10712
12179
  durationMs: Math.max(0, Date.now() - startedAtMs),
12180
+ uploadCount: uploadMetrics.count,
12181
+ uploadBytes: uploadMetrics.totalBytes,
12182
+ uploadLatencyMs: uploadMetrics.totalLatencyMs,
10713
12183
  error: toErrorMessage3(error)
10714
12184
  });
10715
12185
  loggingSession?.logLine(
@@ -10717,6 +12187,9 @@ async function runAgentLoopInternal(request, context) {
10717
12187
  `[agent:${runId}] run_completed`,
10718
12188
  `status=error`,
10719
12189
  `durationMs=${Math.max(0, Date.now() - startedAtMs).toString()}`,
12190
+ `uploadCount=${uploadMetrics.count.toString()}`,
12191
+ `uploadBytes=${uploadMetrics.totalBytes.toString()}`,
12192
+ `uploadLatencyMs=${uploadMetrics.totalLatencyMs.toString()}`,
10720
12193
  `error=${toErrorMessage3(error)}`
10721
12194
  ].join(" ")
10722
12195
  );
@@ -10923,7 +12396,7 @@ function resolveWorkspaceDirForLogging(request) {
10923
12396
  if (explicitSelection && typeof explicitSelection === "object" && !Array.isArray(explicitSelection)) {
10924
12397
  const cwd = explicitSelection.options?.cwd;
10925
12398
  if (typeof cwd === "string" && cwd.trim().length > 0) {
10926
- return path7.resolve(cwd);
12399
+ return path9.resolve(cwd);
10927
12400
  }
10928
12401
  }
10929
12402
  return process.cwd();
@@ -10933,7 +12406,7 @@ function createRootAgentLoggingSession(request) {
10933
12406
  if (!selected) {
10934
12407
  return void 0;
10935
12408
  }
10936
- const workspaceDir = typeof selected.workspaceDir === "string" && selected.workspaceDir.trim().length > 0 ? path7.resolve(selected.workspaceDir) : resolveWorkspaceDirForLogging(request);
12409
+ const workspaceDir = typeof selected.workspaceDir === "string" && selected.workspaceDir.trim().length > 0 ? path9.resolve(selected.workspaceDir) : resolveWorkspaceDirForLogging(request);
10937
12410
  return createAgentLoggingSession({
10938
12411
  ...selected,
10939
12412
  workspaceDir,
@@ -11667,6 +13140,7 @@ export {
11667
13140
  CODEX_APPLY_PATCH_FREEFORM_TOOL_DESCRIPTION,
11668
13141
  CODEX_APPLY_PATCH_JSON_TOOL_DESCRIPTION,
11669
13142
  CODEX_APPLY_PATCH_LARK_GRAMMAR,
13143
+ DEFAULT_FILE_TTL_SECONDS,
11670
13144
  FIREWORKS_DEFAULT_GLM_MODEL,
11671
13145
  FIREWORKS_DEFAULT_GPT_OSS_120B_MODEL,
11672
13146
  FIREWORKS_DEFAULT_KIMI_MODEL,
@@ -11707,10 +13181,12 @@ export {
11707
13181
  createViewImageTool,
11708
13182
  createWriteFileTool,
11709
13183
  customTool,
13184
+ emptyFileUploadMetrics,
11710
13185
  encodeChatGptAuthJson,
11711
13186
  encodeChatGptAuthJsonB64,
11712
13187
  estimateCallCostUsd,
11713
13188
  exchangeChatGptOauthCode,
13189
+ files,
11714
13190
  generateImageInBatches,
11715
13191
  generateImages,
11716
13192
  generateJson,