mineru-runpod
Getting Started

Migrate from the MinerU API

Evaluate a move from the MinerU cloud API to your own RunPod endpoint with a compatibility client for the create, poll, and download lifecycle.

Already calling the official MinerU cloud API (mineru.net/api/v4/...) and hitting a daily page quota, a region requirement, or a need to control cost and versions? You can deploy the open-source MinerU runtime in your RunPod account and preserve the create, poll, and download shape for the supported subset below. Cloud and self-hosted output can still differ when model versions, backends, or configuration differ, so compare real documents before migrating.

MineruApiClient provides compatible create_task and get_task response shapes over a RunPod endpoint. It is a migration facade, not a complete clone of the cloud API.

What this is — and isn't

This is a trial and migration on-ramp, not a permanent production interface. It lets you evaluate self-hosting against the SaaS with near-identical code. Once you've switched, the native MineruClient gives you the worker's richer features (inline markdown, format filtering, volume_path, the hybrid / http-client backends). Use the compat client to get in the door; use the native client long-term.

Prerequisites

  • A deployed mineru-runpod endpoint and RunPod API key
  • An S3-compatible bucket configured through the endpoint's four BUCKET_* variables; the compatibility client needs it to return full_zip_url
  • A representative test set and expected outputs from the cloud API

Install

pip install "mineru-client @ git+https://github.com/sergeyshmakov/mineru-runpod"

The lowest-risk way to evaluate self-hosting is to run your real documents through both the MinerU SaaS and a RunPod endpoint with the same loop, then diff the markdown and compare cost. The compat client makes the two paths almost identical:

import requests
from mineru_client import MineruApiClient

pdf_url = "https://example.com/report.pdf"

# A — official MinerU cloud API
saas = requests.post(
    "https://mineru.net/api/v4/extract/task",
    headers={"Authorization": f"Bearer {MINERU_TOKEN}"},
    json={"url": pdf_url, "model_version": "vlm"},
).json()
saas_task_id = saas["data"]["task_id"]
# ... poll GET /api/v4/extract/task/{saas_task_id} until state == "done" ...

# B — self-hosted on RunPod (compatible lifecycle)
rp = MineruApiClient(endpoint_id="<endpoint-id>", api_key="<runpod-key>")
rp_task_id = rp.create_task(pdf_url, model_version="vlm")["data"]["task_id"]
rp_done = rp.wait_for_task(rp_task_id)        # convenience: poll to completion
rp.download_results(rp_done, "./out")          # unpack the result archive

Compare the two full.md outputs side by side; pair this with the cost math in the launch post to estimate $/page for your volume.

Side-by-side migration

The lifecycle is the same on both sides — create a task, poll until done, fetch the result archive — so most code changes are just the client object.

import requests, time

H = {"Authorization": f"Bearer {MINERU_TOKEN}", "Content-Type": "application/json"}

res = requests.post(
    "https://mineru.net/api/v4/extract/task",
    headers=H,
    json={"url": pdf_url, "model_version": "vlm", "enable_table": True},
).json()
task_id = res["data"]["task_id"]

while True:
    data = requests.get(
        f"https://mineru.net/api/v4/extract/task/{task_id}", headers=H
    ).json()["data"]
    if data["state"] in ("done", "failed"):
        break
    time.sleep(2)

zip_url = data["full_zip_url"]   # download + unzip yourself

The response dicts have the same shape ({"code": 0, "msg": "ok", "data": {...}}), so any code that reads data["state"], data["full_zip_url"], or data["err_msg"] keeps working.

Parameter mapping

create_task(url, ...) accepts the official parameter names and translates them to the worker's:

MinerU API paramMaps toNotes
urlworker file_urlURL-based submission (as on the SaaS single-task endpoint)
model_versionbackendpipelinepipeline, vlmvlm-auto-engine
enable_formula / enable_tableformula_enable / table_enabledirect
languagelangdefault ch, like MinerU; affects the pipeline backend only
page_rangesstart_page / end_pagesingle contiguous 1-based range ("5" or "2-6")
data_idechoed back in get_tasktracking id; see the gaps note below
callbacknot supported → raisesRunPod's webhook payload (raw /status) differs from MinerU's signed {checksum, content} callback; poll with get_task / wait_for_task

Task states

get_task returns the same state machine, mapped from RunPod's job status:

RunPod statusdata.state
IN_QUEUEpending
IN_PROGRESSrunning
COMPLETEDdone (carries full_zip_url)
FAILED / TIMED_OUT / CANCELLEDfailed (carries err_msg)

Getting the result: full_zip_url

full_zip_url is a presigned .zip — the compat client requests archive_format: "zip", so the container matches the official cloud API. One requirement:

  • Your endpoint must have object storage configured. Set the BUCKET_* env vars (BUCKET_ENDPOINT_URL, BUCKET_NAME, BUCKET_ACCESS_KEY_ID, BUCKET_SECRET_ACCESS_KEY) on the endpoint — any S3-compatible store (Cloudflare R2, Backblaze B2, MinIO) works. See Output modes → s3. If they're missing, the task comes back failed with an actionable message.

client.download_results(done, "./out") fetches and unpacks the archive for you (autodetecting .zip vs .tar.gz), so you usually don't need to touch the URL directly.

Known gaps

The compat client raises a clear error rather than silently mis-parsing when you ask for something the self-hosted worker can't do:

FeatureStatus
create_task / get_task, state machine, full_zip_url✅ supported (S3 configured)
model_version: "MinerU-HTML"❌ no HTML-specialised model → raises
extra_formats (docx / html / latex)❌ worker emits markdown + content_list + middle + images only → raises
page_ranges multi-range ("2,4-6")❌ one contiguous slice per job → raises (submit separate tasks)
is_ocr, no_cache, cache_toleranceaccepted, no-op (no worker equivalent)
callback / seedcallback raises — RunPod webhooks deliver a different, unsigned payload than MinerU's signed {checksum, content} callback; seed accepted but unused
data_idechoed from an in-process map; a get_task call in a different process won't echo it
data.extract_progress (page counts)not populated — RunPod's status API exposes state, not per-page progress
Batch (POST /extract/task/batch, file-urls/batch)not offered — submit tasks individually and raise workers_max; RunPod's queue runs them in parallel across workers (Scaling)

When to switch to the native client

Once you've validated the output and decided to stay, move to MineruClient. Self-hosting's whole payoff is the features the SaaS API doesn't expose:

  • Inline markdown without unpacking an archive (transport: "inline", formats: ["markdown"])
  • Local / volume inputs (file_b64, volume_path) — no need to host files at a URL first
  • Every backend, including hybrid-* and *-http-client
  • No S3 requirement for getting results back

The compat client's job is to get you comparing and migrating; the native client is where you live afterward.

Last updated on

On this page