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

# Flexible Document OCR

> End-to-end recipe for OCR and document extraction on the VLM Run Gateway

This guide walks through running a document (PDF) through the VLM Run Gateway,
from picking a model to handling the response. Document requests fan out
one page at a time and default to `ocr` for pp-ocrv6.
For a quick copy-paste example, see [Quickstart](/gateway/quickstart#document-and-image-ocr).
For the cost/latency/accuracy rationale behind picking a model, method, and
DPI, see [Why the Gateway](/gateway/introduction#why-the-gateway).

## 1. Pick a model

Start with `paddleocr/pp-ocrv6` for general-purpose PDF OCR. See
[Models](/gateway/models#document-and-image-ocr) for the full catalog and
[Methods](/gateway/methods) if you need a specific operation like
text detection (`detect`) instead of full OCR. `zai-org/glm-ocr` and
`rednote-hilab/dots.mocr` are also available for reading-order markdown output via
`method: "markdown"`.

## 2. Send the document

Pass the PDF as a `document_url` content part. Prefer a hosted URL over a
base64 data URI. See [Multimodal Inputs](/gateway/multimodal-inputs) for size
limits and [Method Parameters](/gateway/methods#method-parameters) for
`document_dpi` and other knobs.

```python theme={"theme":{"light":"github-light","dark":"dark-plus"}}
from openai import OpenAI

client = OpenAI(
    base_url="https://gateway.vlm.run/v1/openai",
    api_key="<VLMRUN_API_KEY>",
)

response = client.chat.completions.create(
    model="paddleocr/pp-ocrv6",
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "document_url",
                    "document_url": {"url": "https://example.com/invoice.pdf"},
                }
            ],
        }
    ],
    extra_body={"method": "ocr", "document_dpi": 150},
)
```

<h3 id="request-knobs">
  Request knobs
</h3>

| Field                | Default | Notes                                                                                                      |
| -------------------- | ------- | ---------------------------------------------------------------------------------------------------------- |
| `document_dpi`       | `150`   | Rasterization DPI per page. See [Method Parameters](/gateway/methods#method-parameters).                   |
| `document_max_pages` | `500`   | Hard page cap. See [Multimodal Inputs](/gateway/multimodal-inputs#document-inputs).                        |
| `method`             | `ocr`   | Per-page backend operation. See [Methods](/gateway/methods#document-and-image-ocr).                        |
| `stream`             | `false` | When true, emits ordered SSE page blocks. See [Streaming vs non-streaming](#3-streaming-vs-non-streaming). |

### Document rasterization

When the request includes a `document_url` part, `document_dpi` controls the
rasterization DPI for each page before OCR. Set it at the top level or inside
`method_params` (either location works; if both are present, `method_params`
wins).

| DPI             | Tradeoff                                                                            |
| --------------- | ----------------------------------------------------------------------------------- |
| `150` (default) | Good balance of legibility and per-page inference cost.                             |
| `300`+          | Preserves fine print and small text, at higher inference cost and latency per page. |
| Under 150       | Faster and cheaper, but risks losing small text or fine table borders.              |

Start at the default and only raise `document_dpi` if you see missed text on
dense or small-font pages. Page count and file-size limits are documented under
[Multimodal Inputs](/gateway/multimodal-inputs#document-inputs).

## 3. Streaming vs non-streaming

Streaming is scoped to document requests today. Image-only and text-only
requests do not support streaming on the VLM Run Gateway.

| Mode                           | When to use                                                                           |
| ------------------------------ | ------------------------------------------------------------------------------------- |
| **Non-streaming** (default)    | Short documents, batch pipelines, or when you need the full result before continuing. |
| **Streaming** (`stream: true`) | Long documents where you want to render or process pages as they finish.              |

Set `stream: true` to receive each page over SSE as it completes, in ascending
page order, instead of waiting for the entire document:

```python theme={"theme":{"light":"github-light","dark":"dark-plus"}}
stream = client.chat.completions.create(
    model="paddleocr/pp-ocrv6",
    stream=True,
    messages=[
        {
            "role": "user",
            "content": [
                {"type": "document_url", "document_url": {"url": "https://example.com/report.pdf"}}
            ],
        }
    ],
    extra_body={"method": "ocr", "document_dpi": 150},
)

for chunk in stream:
    delta = chunk.choices[0].delta.content
    if delta:
        print(delta, end="")
```

Each `delta.content` value is one full page block (same shape as in the next section).

## 4. Parse the response

Every page arrives as a self-contained block:

```text theme={"theme":{"light":"github-light","dark":"dark-plus"}}
<page index="0" dpi="150" width="1275" height="1650">
… page 0 markdown …
</page>
<page index="1" dpi="150" width="1275" height="1650">
… page 1 markdown …
</page>
```

**Non-streaming:** blocks are concatenated in order inside
`choices[0].message.content`.

**Streaming:** the VLM Run Gateway emits one OpenAI-style SSE chunk per page block.
Each chunk's `delta.content` holds a single block from the example above.

Split on page open and close tags to recover per-page markdown, or treat the
whole string as one document if you do not need page boundaries.

## 5. Track cost and handle errors

**Non-streaming:** read `response.usage.cost` for per-request metering during
the alpha.

**Streaming:** the last SSE event carries `usage` (including `usage.cost`) on
the chunk instead of `delta.content`. Check for it before treating every chunk
as page content:

```python theme={"theme":{"light":"github-light","dark":"dark-plus"}}
usage = None
pages: list[str] = []

for chunk in stream:
    choice = chunk.choices[0]
    if choice.delta.content:
        pages.append(choice.delta.content)
    if chunk.usage:
        usage = chunk.usage

print(f"Cost: {usage.cost if usage else 'n/a'}")
```

For both modes:

* Retry `429`/`500` with backoff; do not retry `400` capability violations.
  See [Error Codes](/gateway/error-codes).
* Keep the `x-request-id` response header if you need to contact support.

## Related

<CardGroup cols={2}>
  <Card title="Multimodal Inputs" icon="images" href="/gateway/multimodal-inputs">
    Content part types, document limits, and URL vs base64 tradeoffs.
  </Card>

  <Card title="Methods" icon="list-check" href="/gateway/methods">
    Per-model `method` and `method_params` reference.
  </Card>

  <Card title="Error Codes" icon="circle-exclamation" href="/gateway/error-codes">
    Status codes, error bodies, and retry guidance.
  </Card>

  <Card title="Models" icon="table" href="/gateway/models#document-and-image-ocr">
    Document and image OCR model catalog.
  </Card>
</CardGroup>
