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

# Presigned S3 Inputs

> Run production batch inference from private CSV or Parquet objects in S3, R2, Google Cloud Storage, and compatible stores.

For production batch jobs, place the input file in private object storage and give Sutro a time-limited HTTPS download URL. The SDK forwards the URL instead of loading the file or including every row in the submission request.

Amazon S3, Cloudflare R2, Google Cloud Storage, and other S3-compatible stores can all work. Sutro does not use your cloud credentials or storage API; it performs an ordinary HTTPS GET with the signed URL.

## Input contract

| Requirement             | Guidance                                                                                                                                                                                                                                       |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| URL                     | Provide an `https://` URL that authorizes a **GET** without additional headers. Cloud URIs such as `s3://` and `gs://` are not accepted.                                                                                                       |
| File format             | CSV and Parquet are supported. Use an object key ending in `.csv` or `.parquet`.                                                                                                                                                               |
| Large production jobs   | Prefer an unwrapped Parquet file with normal Parquet compression, such as Snappy or Zstandard. For standalone-model priority-1 jobs, Sutro tokenizes this format in bounded chunks after downloading and mirroring the complete source object. |
| Standalone model schema | Put the text sent to the model in one column and pass its name with `column`.                                                                                                                                                                  |
| Sutro Function schema   | Use one column per Function input field. Required Function columns must be present. Do not pass `column`.                                                                                                                                      |
| URL lifetime            | Use the shortest lifetime that covers worst-case asynchronous pickup before the worker starts its GET, with a safety buffer. Provider policies or temporary credentials can shorten the effective lifetime.                                    |

<Warning>
  Treat a presigned URL like a temporary credential. It is reusable until it expires or is otherwise invalidated. Do not commit it, put it in a job name or description, or write the complete URL to application logs. Redact the entire query string in logs and error-reporting tools.
</Warning>

Sutro currently makes one full-object GET and does not resume or retry a failed input download. For Amazon S3, expiration is checked when the request starts, and a URL can expire earlier than `ExpiresIn` when its temporary credentials or a bucket-policy restriction expires. See [AWS's expiration guidance](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html).

## Prepare a Parquet input

For standalone model inference, create a prompt column. Other columns are not sent to the model or copied into results. Sutro converts the selected cells to strings; null and NaN values become empty prompts, and embedded line breaks become the literal `\n` sequence.

```python theme={null}
import polars as pl

inputs = pl.DataFrame(
    {
        "record_id": ["review-001", "review-002"],
        "prompt": [
            "The product was easy to use and worked well.",
            "The package arrived damaged.",
        ],
    }
)

inputs.write_parquet(
    "reviews-2026-07-29.parquet",
    compression="zstd",
)
```

Keep the exact source object until you reconcile the results. Sutro preserves input order, so join results by row position. A stable identifier such as `record_id` makes your pipeline easier to audit, although unused columns are not copied into standalone-model results.

For a Sutro Function, columns must instead match the Function's declared inputs. Required columns must exist; missing optional columns and extra columns are allowed. Values become strings and nulls become empty strings, so validate domain-specific values before submission.

## Sign an Amazon S3 input

In production, use separate AWS identities for producing the object and signing read access:

* The producer needs `s3:PutObject`. For a customer-managed AWS KMS key, single-part upload also needs `kms:GenerateDataKey`; multipart upload needs both `kms:GenerateDataKey` and `kms:Decrypt`.
* The read-only signer needs `s3:GetObject` or, when signing a specific version, `s3:GetObjectVersion`. For a customer-managed AWS KMS key, it also needs `kms:Decrypt`. Prefer short-lived role credentials whose session remains valid for the URL lifetime.

The producer should upload to a unique key and record the version ID when bucket versioning is enabled. The following signing step assumes a versioned bucket and a signer authenticated with the read-only identity:

```python theme={null}
import boto3

bucket = "my-production-inputs"
key = "sutro/review-sentiment/2026-07-29/reviews.parquet"
bucket_region = "us-west-2"  # Replace with the bucket's actual AWS region.
version_id = "VERSION_ID_RECORDED_BY_THE_UPLOAD_PIPELINE"

signer = boto3.client("s3", region_name=bucket_region)

input_url = signer.generate_presigned_url(
    ClientMethod="get_object",
    Params={
        "Bucket": bucket,
        "Key": key,
        "VersionId": version_id,
    },
    # One hour is illustrative. Choose the shortest practical lifetime for
    # your worst-case scheduling and pickup window, with a safety buffer.
    ExpiresIn=60 * 60,
)
```

Including `VersionId` pins the URL to the uploaded version. Otherwise, use a unique key for every run and never overwrite it. Keep all four S3 Block Public Access settings enabled.

## Use R2, Google Cloud Storage, or another provider

The same input contract applies to other object stores:

| Provider                   | How to create the URL                                                                                                                                                                                                                                                                                                                                                                      |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Cloudflare R2              | Generate a presigned `GetObject` URL with an S3-compatible client. With Boto3, set `endpoint_url` to `https://<ACCOUNT_ID>.r2.cloudflarestorage.com` and `region_name` to `auto`, then call `generate_presigned_url` as above. R2 presigned URLs must use the R2 S3 API domain, not a custom domain. See [R2 presigned URLs](https://developers.cloudflare.com/r2/api/s3/presigned-urls/). |
| Google Cloud Storage       | Generate a native V4 signed download URL with a Cloud Storage client library or `gcloud storage sign-url gs://BUCKET/OBJECT --duration=1h`. Submit the returned `https://storage.googleapis.com/...` XML API URL. See [Cloud Storage signed URLs](https://cloud.google.com/storage/docs/access-control/signing-urls-with-helpers).                                                         |
| Other S3-compatible stores | Configure the vendor's S3 endpoint and signing region in an AWS SDK, then generate a presigned GET. Verify that the resulting HTTPS URL downloads the object from outside your private network without extra authentication headers.                                                                                                                                                       |

Provider-specific expiration, permission, encryption, and versioning rules still apply. Keep the object private and immutable, give the signing identity read access only, and test the exact URL before submitting the full job.

## Submit a standalone model job

Pass the URL as `data`. The SDK forwards it to Sutro instead of downloading the object locally.

```python theme={null}
import sutro as so

job_id = so.infer(
    data=input_url,
    column="prompt",
    model="gpt-oss-20b",
    system_prompt="Classify each review as positive, neutral, or negative.",
    job_priority=1,
    stay_attached=False,
    name="review-sentiment-2026-07-29",
)

print(job_id)
```

Always specify `column` for standalone-model production jobs. Otherwise Sutro uses the first column, which can silently change when an upstream schema is reordered.

<Note>
  The `POST /batch-inference` response only confirms that the job was created. Download and quota validation happen asynchronously. Keep access valid while the worker begins and performs its one full-object GET.
</Note>

## Submit a Sutro Function job

When the object contains columns matching a published Function's inputs, call the same batch runtime through `infer()` and use the Function name as `model`:

```python theme={null}
import sutro as so

job_id = so.infer(
    data=input_url,
    model="lead-qualifier",
    job_priority=1,
    stay_attached=False,
    truncate_rows=False,
    name="lead-qualifier-2026-07-29",
)
```

Do not pass `column`, `system_prompt`, or `output_schema` for this form. Sutro reads the Function input columns and uses the prompt and output schema from the published Function.

<Note>
  `infer()` defaults to `truncate_rows=True`; set it to `False` if an oversized rendered prompt should fail instead. This URL-based path cannot create the SDK's per-row LangSmith traces because the SDK never downloads the rows.
</Note>

The bounded Parquet tokenization path currently applies only to standalone-model jobs. Presigned URLs keep Function submissions and SDK memory small, but Function ingestion materializes the input after download. Split very large Function inputs into independently submitted objects.

## Estimate, monitor, and retrieve results

Check the priority-1 row and token quotas, run a small canary, and create an estimate before the full workload:

```python theme={null}
print(so.get_quotas())

estimate_job_id = so.infer(
    data=input_url,
    column="prompt",
    model="gpt-oss-20b",
    system_prompt="Classify each review as positive, neutral, or negative.",
    job_priority=1,
    dry_run=True,
)

print(estimate_job_id)
```

`dry_run=True` creates an estimate job, prints its estimate, and returns its ID without launching the full job. Priority-1 estimates at or above the sampling threshold run inference on a prefix of approximately 1 million input tokens.

Persist the full job's ID before doing other work. Job creation is not idempotent. The SDK deliberately does not retry `POST /batch-inference` after an HTTP 524 because the job might already have been created. Do not add a blind outer retry loop; after an ambiguous response, inspect recent jobs before resubmitting.

```python theme={null}
status = so.get_job_status(job_id)
print(status)

so.await_job_completion(
    job_id,
    timeout=6 * 60 * 60,
    obtain_results=False,
)

if so.get_job_status(job_id) != "SUCCEEDED":
    raise RuntimeError("Job did not complete successfully")
```

`obtain_results=False` avoids materializing results as JSON but returns `None` regardless of outcome. Confirm `SUCCEEDED` before requesting `GET /jobs/{job_id}/results-url?format=parquet` and downloading `urls.get`. That endpoint supports resumable range requests; see [Presigned Results URLs](/batch-api-reference/job-results-url). For smaller jobs, see [Retrieving Results](/batch-api-reference/job-results).

Results preserve the original input order. Keep the source object available until you have downloaded and reconciled the result artifact.

## Troubleshooting

| Symptom                                                                  | Likely cause                                                                                                                                                               |
| ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The provider returns `403 Forbidden`, or Sutro cannot start the download | The URL or its signing credentials expired; it does not authorize GET; it requires signed headers Sutro does not send; or a storage/network policy blocks Sutro's request. |
| Unsupported file type                                                    | The object is not CSV or Parquet, or its key does not make the format clear. Use a `.csv` or `.parquet` suffix.                                                            |
| Column not found                                                         | `column` does not exactly match the CSV/Parquet column name. Column names are case-sensitive.                                                                              |
| Function input validation failure                                        | A required Function input column is missing. Validate values upstream because Sutro converts values to strings and nulls to empty strings.                                 |
| Job remains pending after a download or file-preparation error           | Some early ingestion failures may occur before a terminal status is recorded. Keep the job ID and contact Sutro support rather than blindly submitting duplicates.         |
| A very large standalone-model CSV job fails during preparation           | Convert the input to unwrapped Parquet so priority-1 tokenization can process it in bounded chunks after the complete source download.                                     |

If the full-object GET fails, inspect the original job before generating a fresh URL and submitting a replacement. Standalone-model URL jobs mirror the raw source into Sutro-managed object storage before tokenization; the mirror and derived artifacts follow your configured [data retention policy](/concepts/data-retention). The original object remains under your provider's lifecycle policy.
