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.
| Selector | Notes |
|---|---|
deepseek-ocr-2 | Logical route; the router resolves the concrete backing route. |
eu/deepseek-ocr-2 | Region-pinned aggregate. |
privatemode/deepseek-ocr-2 | Provider-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.
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.
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.
# 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
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)
import fs from "node:fs";
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.TRESOR_API_KEY,
baseURL: "https://api.tresor.co/v1",
});
const dataUrl = `data:image/png;base64,${fs
.readFileSync("./page-1.png")
.toString("base64")}`;
const ocr = await client.chat.completions.create({
model: "deepseek-ocr-2",
messages: [
{
role: "user",
content: [
{ type: "image_url", image_url: { url: dataUrl } },
{ type: "text", text: "Free OCR." },
],
},
],
});
console.log(ocr.choices[0].message.content);
max_tokens when you need a hard cap on spend.429 upstream_rate_limit responses with backoff, honouring Retry-After.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.
The router rejects a request it knows the provider will refuse, and returns a specific error:
| Status | Code | Cause |
|---|---|---|
400 | unsupported_image_url | image_url.url is a remote https://… URL. Send the image inline as a base64 data URL. |
400 | unsupported_reasoning | A 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:
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.
tresor.receipt_id and the routed route are reported the same way as for chat: see Routing and Receipts.GET /v1/usage with modality = chat and token-based billing, priced at the live route price.