> ## Documentation Index
> Fetch the complete documentation index at: https://docs.eigenpal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Upload files

> Upload reusable files directly or through the API, then reference them from runs and datasets.

Eigenpal supports two transfer paths that create the same reusable file resource:

* **Multipart upload** sends the file to the Eigenpal app. Maintained clients use it while the complete request fits the deployment's configured body limit.
* **Storage-direct upload** is selected for larger files when enabled and supports files up to 100 MiB without sending the bytes through a web function.

Both flows return a `file_...` id. Use it in run or dataset JSON as `{ "$fileId": "file_..." }`.

## Storage-direct upload

First declare the file before sending its bytes:

```bash theme={null}
curl https://api.eigenpal.com/v1/files/uploads \
  -H "Authorization: Bearer $EIGENPAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "filename": "invoice.pdf",
    "contentType": "application/pdf",
    "size": 10485760
  }'
```

When the declared file exceeds the deployment's multipart limit, cloud deployments return `transport: "presigned-put"` with:

* `uploadId` and the reserved `fileId`
* an expiring `url`
* exact `headers` to include in the `PUT`
* `expiresAt` and `maxFileSizeBytes`

Upload the original bytes to that URL. Do not change the signed headers or object key. Treat the URL as a temporary credential and never log it.

After the `PUT` succeeds, complete the upload:

```bash theme={null}
curl -X POST \
  "https://api.eigenpal.com/v1/files/uploads/$UPLOAD_ID/complete" \
  -H "Authorization: Bearer $EIGENPAL_API_KEY"
```

Completion verifies the object size, type, ownership, and signed metadata before it becomes reusable. It returns the normal file resource and is safe to retry if the response is lost.

To abandon a pending upload:

```bash theme={null}
curl -X DELETE \
  "https://api.eigenpal.com/v1/files/uploads/$UPLOAD_ID" \
  -H "Authorization: Bearer $EIGENPAL_API_KEY"
```

Expired pending uploads are removed automatically.

## Multipart upload

Small files and deployments with direct transfer disabled return `transport: "multipart"`. Existing clients can also call that operation directly:

```bash theme={null}
curl https://api.eigenpal.com/v1/files \
  -H "Authorization: Bearer $EIGENPAL_API_KEY" \
  -F "file=@invoice.pdf"
```

The same operation remains available at your self-hosted origin under `/api/v1/files` and at `https://studio.eigenpal.com/api/v1/files`.

<Note>
  Vercel rejects function bodies above approximately 4.5 MB before Eigenpal can return its
  structured error. Use storage-direct upload for larger cloud files. Self-hosted ingress can accept
  larger multipart bodies up to Eigenpal's 100 MiB logical file limit when its proxy is configured
  accordingly.
</Note>

## Deployment behavior

Maintained Eigenpal Files clients negotiate before transferring bytes:

1. A file within `deployment.multipartMaxBytes` returns `transport: "multipart"`.
2. A larger file returns a signed upload when direct storage is enabled and reachable.
3. A local or private-storage deployment returns `transport: "multipart"` regardless of size when direct storage is disabled.
4. A failed signed transfer is retried as a signed transfer; clients do not automatically send the same large file through multipart.

For browser-direct uploads, the S3-compatible bucket must allow the browser origin, `PUT`, `Content-Type`, signed `x-amz-meta-*` headers, and the `ETag` response header. Eigenpal cloud configures this automatically. Self-hosted deployments default to multipart; operators can configure equivalent Ceph/S3 CORS, set `EIGENPAL_BROWSER_STORAGE_ORIGINS` to the comma-separated storage origins at image build time, and set `EIGENPAL_DIRECT_FILE_UPLOADS=1` to opt in without changing clients.

The deployment limit is optional and defaults to `4718592` bytes (4.5 MiB). Configure it in the mounted `eigenpal.config.yaml`:

```yaml theme={null}
deployment:
  multipartMaxBytes: 20971520 # 20 MiB
```

Set `multipartMaxBytes: null` to disable size-based direct transfer and keep uploads and downloads on the app server. This is the default in the supplied on-prem manifests. `EIGENPAL_MULTIPART_MAX_BYTES` overrides the YAML value for container/platform configuration; it accepts a non-negative byte count or `none`.

Cloud applies the same reusable-pool storage quota to multipart and storage-direct completion, including temporary `run-input` and `builder-attachment` files. Delete reusable files when they are no longer needed. Self-hosted deployments are unlimited by default and can set `EIGENPAL_REUSABLE_FILES_MAX_BYTES` to enforce a per-tenant byte limit.

## Run pre-uploads (`purpose: run-input`)

Maintained SDKs and the CLI automatically pre-upload large run inputs through the Files API so the run request stays under the configured multipart limit. Their client-side default is also 4.5 MiB:

* TypeScript: `new EigenpalClient({ multipartMaxBytes: 20 * 1024 * 1024 })`
* Python: `EigenpalClient(multipart_max_bytes=20 * 1024 * 1024)`
* All maintained clients and the CLI: `EIGENPAL_MULTIPART_MAX_BYTES=20971520`

Use `null` in an SDK constructor, or `EIGENPAL_MULTIPART_MAX_BYTES=none`, to disable automatic pre-uploads and keep every run file on multipart. Ensure the target proxy and app can accept the resulting aggregate request. Client configuration controls run-request preparation; the deployment YAML controls Files negotiation and server download redirects, so self-hosted operators should set both consistently.

Studio workflow and quick-run forms use the deployment's `deployment.multipartMaxBytes` value directly. Files that would exceed the run multipart budget are uploaded through the negotiated Files API first; `null` keeps every Studio run file in the run multipart request.

Automatic uploads set `purpose: "run-input"`. The server retains these temporary pool files so a lost run-start response or concurrent retry can safely reuse the same `$fileId`, then removes abandoned files after 24 hours.

Explicit `files.upload` / `eigenpal files` uploads omit purpose, enforce supported document MIME types, and remain durable reusable files until you delete them.

On versioned cloud buckets, deletes (pending uploads, temporary pool files, builder intermediaries, and normal file deletion) leave noncurrent versions for a recovery window — **7 days in production** (aligned with bucket backups) and **1 day in staging** — then lifecycle removes those prior versions. Current durable objects are not expired by lifecycle.

## Studio builder attachments (`purpose: builder-attachment`)

The agent builder uploads attachments through the Files API with `purpose: "builder-attachment"`. These intermediaries accept any MIME (ZIP, `application/octet-stream`, etc.) while still enforcing size and filename rules. They are retained briefly for handoff retries and reaped after 24 hours if abandoned. Do not set this purpose on explicit durable `files.upload` calls.

Builder uploads use the same deployment negotiation described above: small/on-prem files go to the app's multipart endpoint; files above the configured limit use a signed storage PUT when direct transfer is enabled.

## Cleanup operation

The 24-hour rule is database-driven, not an S3 scan and not a long-lived AWS/ECS task. The existing `/api/cron/reap-stale-executions` maintenance request also processes one bounded batch of at most 100 temporary file records:

* production has one Vercel Cron HTTP invocation every 15 minutes;
* staging reuses the existing daily reset workflow and makes one request before replacing its database;
* each invocation ends after its request, and creates no background task or service;
* one failed object does not abort other rows; its persisted `cleanupAttemptedAt` moves it out of the next batch for 15 minutes;
* a failed invocation leaves rows in the database for the next invocation, while bucket lifecycle removes old noncurrent versions as defense in depth.

This means repeated storage failures can produce repeated bounded requests and alerts, but cannot accumulate live AWS tasks.

## Downloads

`GET /v1/files/{id}/content` and run artifact downloads authenticate in the API, then:

* When direct transfer is enabled, responses above `deployment.multipartMaxBytes` redirect to a short-lived exact-key signed storage GET that preserves safe `Content-Type` and `Content-Disposition`.
* On local/on-prem deployments, the API streams the bytes as before.
* Dynamically generated ZIP downloads (run artifact zip, dataset export) materialize to a cleanup-safe temporary object when needed, then redirect the same way.
* Cloud run-artifact ZIP assembly is capped at **32 MiB** of uncompressed input so the Vercel function stays within memory and duration. The API checks cumulative object sizes (known metadata and/or storage `HEAD`) before downloading bodies; oversized sets return `413` with `artifacts_zip_too_large` — download files individually via `GET /v1/runs/{id}/artifacts/{path}`, or pass a smaller `files=` subset. On-prem/local keep a higher in-process ceiling.
* Cloud dataset export assembly uses the same **32 MiB** source-byte budget and returns `413 dataset_too_large` before downloading an oversized storage object. Export a selected example subset when the full dataset exceeds the limit.

Browser clients that follow the redirect need storage CORS for `GET`/`HEAD`. Eigenpal cloud configures this automatically.

## API origins

New cloud integrations should use `https://api.eigenpal.com/v1`. Staging uses `https://sapi.eigenpal.com/v1`. The existing `https://studio.eigenpal.com/api/v1` and `https://staging.eigenpal.com/api/v1` routes remain compatible and execute the same handlers. Self-hosted SDK and CLI base URLs remain configurable and continue using `/api/v1`.

To smoke-test upload + download against local or staging:

```bash theme={null}
EIGENPAL_API_KEY=eig_live_... \
EIGENPAL_BASE_URL=http://localhost:3000 \
bun smoke:direct-file-transfers

EIGENPAL_API_KEY=eig_live_... \
EIGENPAL_BASE_URL=https://sapi.eigenpal.com \
bun smoke:direct-file-transfers
```
