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

# File parser

> Attach PDFs to a chat completion — RouteShift extracts bounded text from the file (or passes it through natively where the provider supports it) and forwards the augmented request upstream.

The **file parser plugin** lets any chat completion accept PDF file parts alongside the usual `text`/`image` content. RouteShift decodes each PDF, enforces per-file and per-request byte, page, and text-length budgets, and replaces the raw file with either extracted text or a validated native-PDF part before dispatching upstream.

Like [web search](/routeshift/web-search), file parsing runs entirely proxy-side. You don't need to swap providers or bolt on a document-extraction service — any model you can call through RouteShift can accept a PDF.

## When to use it

Turn on file parsing when a user turn (or system prompt) needs to reference the contents of a PDF — a report the user uploaded, a spec pulled from a URL, a receipt, a contract. The plugin is a good fit for:

* Client apps that already send OpenAI-style `input_file` parts and want the same shape to work across every provider RouteShift routes to.
* Workflows that hand the model a URL to a PDF instead of pre-extracting text.
* Multi-provider setups where you want fail-closed passthrough on Anthropic and Gemini (native PDF understanding) with automatic text fallback everywhere else.

Skip it for non-PDF documents (unsupported today), for images (use the provider's native image parts), or when your prompt is already text.

## Opt in per request

The file parser is triggered in one of three ways:

1. **A `file` or `input_file` content part** on any user message. The plugin runs automatically as soon as a request contains one — no `plugins` array entry is required.
2. **An explicit `plugins` entry** with `id: "file-parser"`. Use this when you want to mark the plugin as `required` (so a parse failure short-circuits the request instead of degrading), or to force text extraction on a provider that would otherwise get native PDF passthrough.
3. **A `file` part inside the `system_prompt`**. System documents are always extracted to text, regardless of the target provider.

The `plugins` array is stripped from the payload before dispatch — upstream providers never see it.

### Base64 inline PDF

```bash theme={null}
curl https://proxy.routeshift.io/v1/chat/completions \
  -H "Authorization: Bearer sk-proxy-…" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-sonnet-5",
    "messages": [
      {
        "role": "user",
        "content": [
          { "type": "text", "text": "Summarize the attached research paper in three bullets." },
          {
            "type": "file",
            "filename": "paper.pdf",
            "file": {
              "file_data": "data:application/pdf;base64,JVBERi0xLjQK…"
            }
          }
        ]
      }
    ]
  }'
```

### URL-backed PDF

```json theme={null}
{
  "model": "gemini-3.1-pro",
  "messages": [
    {
      "role": "user",
      "content": [
        { "type": "text", "text": "What are the top three risks in this filing?" },
        {
          "type": "file",
          "filename": "10-k.pdf",
          "file": { "file_url": "https://example.com/reports/10-k.pdf" }
        }
      ]
    }
  ]
}
```

### Explicit plugin entry (mark as required, or force text extraction)

```json theme={null}
{
  "model": "claude-sonnet-5",
  "messages": [
    {
      "role": "user",
      "content": [
        { "type": "text", "text": "Extract the invoice total and due date." },
        { "type": "file", "filename": "invoice.pdf", "file": { "file_data": "data:application/pdf;base64,…" } }
      ]
    }
  ],
  "plugins": [
    { "id": "file-parser", "required": true }
  ]
}
```

An explicit `file-parser` entry also **opts out of native PDF passthrough** — every file goes through the text-extraction path even when the target provider natively supports PDFs. Use this when you want consistent, provider-agnostic text output.

## Supported inputs

The plugin accepts either `file` or `input_file` content parts. The PDF payload can be provided as:

| Field              | Where                            | Format                                                      |
| ------------------ | -------------------------------- | ----------------------------------------------------------- |
| `file_data`        | top level or nested under `file` | Raw base64, or a `data:application/pdf;base64,…` URI        |
| `file_url` / `url` | top level or nested under `file` | `https://` URL that returns `Content-Type: application/pdf` |

A `filename` (or `name`) may be supplied at the top level or nested under `file`; RouteShift sanitizes it before forwarding — non-alphanumeric characters become underscores, and the length is capped. If no filename is supplied, `document.pdf` is used.

Only PDFs (`application/pdf` with a `%PDF-` magic header) are accepted. Anything else surfaces an `unsupported_file_type` warning.

## Native PDF passthrough vs. text extraction

RouteShift picks between two output shapes for each user-message file part:

* **Native PDF passthrough.** When the resolved target model — *and every model in the fallback chain* — is explicitly marked as supporting native PDF (currently Claude and Gemini families), the plugin forwards a bounded, validated PDF part directly to the provider. This preserves layout, tables, and figures that text extraction would lose.
* **Text extraction.** In every other case, the plugin extracts text with a page-count cap and replaces the file part with a `text` part of the form `[Extracted from <filename>]\n<extracted text>`.

The switch is **fail-closed**: native passthrough only kicks in when the primary route *and* every possible fallback candidate for the request supports it. Any mixed chain falls back to text extraction so the request never depends on an untested adapter. System-prompt files are always text-extracted regardless of provider. An explicit `file-parser` plugin entry (see above) forces text extraction as well.

## Budgets and limits

Every request runs against a set of per-file and per-request budgets. All of them are configurable per-deployment; the defaults are:

| Budget                           | Default           | Env var                                 | What it protects                                                                                                                                    |
| -------------------------------- | ----------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| Files per request                | `10`              | `PLUGIN_MAX_FILES`                      | Total file-part attempts, including malformed URLs and base64 (attempted before any validation, so a burst of broken inputs can't bypass this cap). |
| Bytes per file                   | `10 MiB`          | `PLUGIN_MAX_FILE_BYTES`                 | Decoded PDF size after base64 or download.                                                                                                          |
| Pages per file                   | `100`             | `PLUGIN_MAX_FILE_PAGES`                 | Declared page count; exceeding the cap fails the file rather than silently truncating.                                                              |
| Extracted text per file          | `1,000,000` chars | `PLUGIN_MAX_EXTRACTED_TEXT_CHARS`       | Text extracted from a single PDF.                                                                                                                   |
| Total bytes per request          | `10 MiB`          | `PLUGIN_MAX_TOTAL_FILE_BYTES`           | Sum of decoded bytes across every file in the request.                                                                                              |
| Total extracted text per request | `1,000,000` chars | `PLUGIN_MAX_TOTAL_EXTRACTED_TEXT_CHARS` | Sum of extracted text across every file.                                                                                                            |
| Fetch timeout                    | `5000` ms         | `PLUGIN_FETCH_TIMEOUT_MS`               | Single URL-fetch deadline (DNS + transport combined).                                                                                               |

Every fetch error conservatively **exhausts** the request's remaining byte budget: a failed request can already have read a partial body, and the plugin can't know how much. That's deliberate — it makes each file attempt a hard, all-or-nothing charge against the aggregate cap.

## Optional vs. required plugins

The file parser is **optional by default**. When a file can't be parsed — the URL was blocked, the payload was too large, the content type wasn't PDF, extraction failed — the request continues without the file, and a `PluginWarning` is attached to the response and the request log.

Set `"required": true` when the model shouldn't answer without the file. A required-plugin failure short-circuits the request with:

```http theme={null}
HTTP/1.1 502 Bad Gateway
Content-Type: application/json

{
  "error": {
    "code": "plugin_required_failed",
    "message": "Required plugin file-parser failed: <detail>"
  }
}
```

## Warning codes

Each warning surfaces as `{ plugin: "file-parser", code: "<code>" }` on the response and is persisted in the request log's plugin-run audit row. Codes are stable and deliberately do **not** include filenames, URLs, or response bodies.

| Code                    | Meaning                                                                                                                                          |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `file_url_blocked`      | URL resolved to a private/loopback/link-local address, an unsupported scheme, or was blocked by SSRF checks.                                     |
| `file_too_large`        | Exceeded a per-file or per-request byte/page/text budget.                                                                                        |
| `unsupported_file_type` | Not a PDF (bad magic bytes or content type), or neither `file_data` nor `file_url` was supplied.                                                 |
| `empty_file`            | PDF parsed successfully but contained no extractable text (common for scanned image-only documents when native PDF passthrough isn't available). |
| `file_parse_failed`     | Decoder rejected the payload.                                                                                                                    |
| `file_fetch_failed`     | URL fetch returned a non-2xx status, a bad redirect, or a non-PDF content type.                                                                  |
| `file_fetch_timeout`    | URL fetch didn't complete within `PLUGIN_FETCH_TIMEOUT_MS`.                                                                                      |

## Security

URL-backed PDFs are fetched through RouteShift's SSRF-safe fetch path: every candidate address is validated and DNS-pinned before transport, so a URL cannot rebind to a private address between the check and the connection. Loopback, IPv4 private/special-use ranges, ULA, link-local and site-local IPv6, multicast, discard-only, IPv4-embedded IPv6, and NAT64 forms are all rejected. Redirects are followed up to three hops and re-validated at each hop.

Filenames are sanitized before they reach an upstream provider. Extracted text is wrapped with a clearly labeled `[Extracted from <filename>]` header so the model treats the content as supplied context, not authoritative instructions.

## Billing and audit

Every plugin execution — file parser included — writes a sanitized audit row that captures the plugin id, status (`ok` / `warning` / `error` / `skipped`), a stable detail code, cost, and latency. Rows are keyed by `(request_id, plugin_id)` and are visible in your [observability feed](/routeshift/observability) alongside the underlying request.

The file parser itself does not add a per-request surcharge — the cost of extraction is folded into the request. Plugin surcharges (currently applied by [web search](/routeshift/web-search)) are tracked separately from provider model cost so plugin fees never get relabeled as routing spend. See [Savings and pricing](/routeshift/savings-and-pricing) for how the two components appear in usage reports.

## Errors

| Response                 | Status | When                                                                                                             |
| ------------------------ | ------ | ---------------------------------------------------------------------------------------------------------------- |
| `invalid_plugin`         | 400    | `plugins` isn't an array, contains a non-object entry, or references an unsupported plugin id.                   |
| `file_*` (warning)       | 200    | Optional file parser skipped a file for one of the codes above. Underlying model response is delivered normally. |
| `plugin_required_failed` | 502    | A `required: true` file-parser plugin couldn't process a file — no upstream call was made.                       |
