Architecture
An escalation ladder, not a wrapper
Contrie treats extraction as a control problem: try the cheapest thing that could work, measure the result, and climb only when the measurement says so. This page is the actual control flow of packages/ai — the fleet table below is imported from the same source file the router reads.
Stage internals
What each stage actually does
Heuristic
heuristic-extractor.tsCheerio parses JSON-LD (including @graph unwrapping), microdata, Open Graph, and Twitter Card metadata. If the JSON-LD carries a recognized schema.org type with five or more fields on average, the request returns immediately — no model is ever called. The same waterfall idea shows up in Trafilatura's rules-then-readability fallback and Jina Reader's cheap-fetch-then-render split; Contrie applies it at the extraction layer.
Markdown
html-to-markdown.tsThe page is reduced once to structure-preserving markdown: headings, lists, tables, links and code survive; scripts, navigation, cookie banners and sidebars do not. Main content is chosen by text density so an empty <main> shell cannot win. This is what the model reads (alongside any JSON-LD the page declares), what grounding is checked against, and what format: "markdown" returns with no model call at all.
Classify
classifier.tsRegex fingerprinting scores the page: anti-bot systems (DataDome, Cloudflare challenge, PerimeterX, Kasada, Imperva, hCaptcha and friends) push it to adversarial; heavy JS shells with no structure read as visual; small structured pages read as simple. A JavaScript shell with almost no visible text is refused up front with RENDER_REQUIRED instead of being guessed at. A caller-supplied screenshot forces the visual path — capturing one is deliberate cost, so the pipeline honors the intent.
Route
model-router.tsPlans map to a maximum model tier (free → free models only; pro and up → the full fleet). Within the allowed set, the router picks the cheapest tier that can handle the classified complexity, highest priority first. Simple pages get budget models; adversarial pages start at premium. Free-tier requests hedge their first attempt across two models and keep the first result that clears the bar — free models are priced by observed latency, not parameter count.
Validate
validator.tsEvery result is scored 0–100: does it satisfy the requested JSON Schema or Zod schema (40%), what fraction of leaf fields carry values, counted recursively (40%), and do values pass sanity checks blended with grounding — each extracted string and number is searched for in the page text (20%). Fewer than half the values found caps the score at 60, so an invented answer can never pass. An empty result against an explicit prompt caps it at 20. Nothing unscored leaves the pipeline.
Escalate
extraction-pipeline.tsBelow the 70 threshold, the pipeline walks up the tier order and retries with the next stronger model, keeping the best-scoring attempt. Every stage lands in the response trace and every token is priced into costUsd. The free tier walks all six free models before giving up. Visual tasks cascade UI-TARS → Gemini 3.1 Flash Lite → Claude Sonnet 5.
Scope boundary
by designContrie extracts; it does not drive multi-step agent tasks. The self-host API adds a Playwright chromium route (/v1/browser) for JS-heavy pages — rendering is in scope, autonomous browsing is not. Fetching sits on native fetch and Playwright rather than a bespoke browser engine.
The fleet
17 models, six tiers, one ladder
Rendered at build time from packages/ai/src/models.ts — this table cannot drift from what the router runs.
| Model | Tier | Priority | Vision | Context |
|---|---|---|---|---|
| minimax/minimax-m3:free | free | 10 | yes | 1049k |
| nvidia/nemotron-3-super-120b-a12b:free | free | 9 | — | 262k |
| z-ai/glm-5.2:free | free | 8 | — | 256k |
| nvidia/nemotron-3-ultra-550b-a55b:free | free | 6 | — | 1000k |
| minimax/minimax-m2.7:free | free | 4 | — | 197k |
| google/gemma-4-31b-it:free | free | 2 | yes | 262k |
| deepseek/deepseek-v4-pro-0813 | budget | 15 | — | 1049k |
| deepseek/deepseek-v4-flash | budget | 12 | — | 1049k |
| deepseek/deepseek-chat-v3-0324 | budget | 10 | — | 164k |
| openai/gpt-5.6-luna | standard | 10 | yes | 1050k |
| openai/gpt-5-mini | standard | 8 | yes | 400k |
| anthropic/claude-haiku-4.5 | standard | 6 | yes | 200k |
| bytedance/ui-tars-1.5-7b | vision | 16 | yes | 128k |
| google/gemini-2.5-flash-lite | vision | 10 | yes | 1049k |
| google/gemini-3.1-flash-lite | premium | 12 | yes | 1049k |
| google/gemini-3.7-flash | premium | 8 | yes | 1049k |
| anthropic/claude-sonnet-5 | frontier | 10 | yes | 1000k |
Lineage: the crawling substrate began as a fork of Crawlee (Apify, Apache-2.0 — attribution preserved); browser rendering runs on Playwright. The extraction pipeline, router, validator, and cascade are original work.
The playground runs exactly this flow and shows the score.
Open the playground