Document OCR

Read text out of page images and scanned documents with the DeepSeek OCR route on the Tresor API.

OpenAI-compatible APIs have no dedicated OCR endpoint, so neither does Tresor. deepseek-ocr-2 is a vision model on the same POST /v1/chat/completions route as every other chat model: you send the page image as an image_url content part and get the transcription back as the assistant message.

Discovery works through the normal catalogue — deepseek-ocr-2 appears in GET /v1/models with type: chat, so anything that already speaks the chat-completions protocol can drive it without special-casing.

Route

SelectorNotes
deepseek-ocr-2Logical route; the router resolves the concrete backing route.
eu/deepseek-ocr-2Region-pinned aggregate.
privatemode/deepseek-ocr-2Provider-pinned. The only concrete backing route today, running in eu.

Because the provider serves this model from a single region, its concrete route is published as the provider-pinned privatemode/deepseek-ocr-2 rather than a longer region/provider/model ID. The model does not support reasoning controls — leave reasoning and reasoning_effort out of the request, or the router rejects it with 400 unsupported_reasoning.

Prompting constraint

This is a prompt-conditioned OCR model, not a general assistant. It was trained on exactly two prompts and its own documentation warns that other prompts degrade output quality:

Free OCR.
<|grounding|>Convert the document to markdown.

The first returns a plain transcription of the page text. The second returns markdown plus ground/box markers for per-region attribution.

Send one of these verbatim, as the only text part. Do not add a system prompt or extra instructions. Anything you want to wrap around the transcription (page markers, chunking, headings) belongs in your own post-processing of the returned text.

Image input requirements

Images must be data URLs. The provider rejects remote image URLs in vision requests. Send the page inline as data:image/png;base64,…; a https://… URL in image_url.url fails with 400 unsupported_image_url.

Supported formats include png, jpeg, tiff, webp, bmp, gif, jpeg2000, avif, ppm, tga, and pcx. Prefer PNG or JPEG.

Keep the page image small. The context window is 8192 tokens total, and image tokens scale with resolution — a full-resolution screenshot or a phone photo of a page can blow past the window on its own. Downscale to roughly 1536px on the longest edge before encoding.

Do not override temperature or skip_special_tokens: the provider applies temperature=0 and skip_special_tokens=False by default and both are tuned for this model.

Curl

# Base64-encode the page (Linux: base64 -w0; macOS: base64 -i page-1.png | tr -d '\n').
PAGE_B64=$(base64 -w0 page-1.png)

curl https://api.tresor.co/v1/chat/completions \
  -H "Authorization: ******" \
  -H "Content-Type: application/json" \
  -d @- <<JSON
{
  "model": "deepseek-ocr-2",
  "messages": [{
    "role": "user",
    "content": [
      { "type": "image_url", "image_url": { "url": "data:image/png;base64,${PAGE_B64}" } },
      { "type": "text", "text": "Free OCR." }
    ]
  }]
}
JSON

OpenAI SDK

import base64
from openai import OpenAI

client = OpenAI(
    api_key="YOUR_API_KEY",
    base_url="https://api.tresor.co/v1",
)

with open("page-1.png", "rb") as page:
    data_url = "data:image/png;base64," + base64.b64encode(page.read()).decode()

ocr = client.chat.completions.create(
    model="deepseek-ocr-2",
    messages=[
        {
            "role": "user",
            "content": [
                {"type": "image_url", "image_url": {"url": data_url}},
                {"type": "text", "text": "Free OCR."},
            ],
        }
    ],
)

print(ocr.choices[0].message.content)

Practical limits

  • One page per request. The model reads a single page image at a time. Split multi-page PDFs and fan out client-side.
  • Context window is 8192 tokens — small compared with the chat models on the API. A dense page returns roughly 600–1300 tokens of text, so large page images with long transcripts can push the ceiling. Set max_tokens when you need a hard cap on spend.
  • Concurrency is the binding constraint, not latency. A page takes roughly 10–20 seconds of inference, and provider completion-token rate limits are shared across your key, so a wide fan-out hits rate limits before it hits your bandwidth. Run a handful of pages in flight — six is a reasonable starting point — and retry 429 upstream_rate_limit responses with backoff, honouring Retry-After.
  • Failures are per page. Treat each page as its own unit of work so one unreadable page does not sink a whole document.

Transcription is per page, so attribution is your job on the way out: keep page order explicit in your own fan-out and re-attach page numbers to the text you get back.

Troubleshooting

The router rejects a request it knows the provider will refuse, and returns a specific error:

StatusCodeCause
400unsupported_image_urlimage_url.url is a remote https://… URL. Send the image inline as a base64 data URL.
400unsupported_reasoningA reasoning or reasoning_effort field was sent; this route has no reasoning controls.

Anything the router cannot classify before forwarding comes back as a generic 502 upstream error. The router maps unrecognised upstream failures onto bad_gateway deliberately, so the provider's own wording is not echoed in the body.

If you get a 502, check in this order:

  1. Is the page too large? Image tokens count against the same 8192-token window as the transcription. Downscale to ~1536px on the longest edge.
  2. Did you send one of the two trained prompts? A different prompt degrades output rather than erroring, so you get plausible-looking but wrong text instead of a failure.
  3. Was the image part actually included? If content is a plain string, or the image is nested somewhere other than a content array, the provider silently ignores it and the model answers from the text alone — which looks like a successful call that invented text.

The provider's actual error message is recorded in the router log, keyed by route and model:

forwarder: provider=privatemode model=deepseek-ocr-2 modality=chat kind=... status=... detail=...

The detail field carries the upstream message.

Receipts and usage

  • Receipts are enabled by default, same as every other chat completion.
  • tresor.receipt_id and the routed route are reported the same way as for chat: see Routing and Receipts.
  • Usage rows land in GET /v1/usage with modality = chat and token-based billing, priced at the live route price.

See also