Skip to content

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.

POST /v1/scrape · /v1/extract · /v1/browserHEURISTICjson-ld · microdata · og · twittersufficient?RETURNno model call · quality 85not sufficientCLASSIFYsimple · complex · visual · adversarialscreenshot?VISION CASCADEui-tars 7b → gemini 3.1 flash lite → claude sonnet 5ROUTEcheapest capable tier winsEXTRACTmarkdown + json-ld (+ screenshot) → LLMVALIDATEschema 40 · completeness 40 · sanity+grounding 20≥ 70RETURNscored result + metadata< 70 · ESCALATE next tier

Stage internals

What each stage actually does

Heuristic

heuristic-extractor.ts

Cheerio 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.ts

The 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.ts

Regex 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.ts

Plans 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.ts

Every 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.ts

Below 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 design

Contrie 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.

ModelTierPriorityVisionContext
minimax/minimax-m3:freefree10yes1049k
nvidia/nemotron-3-super-120b-a12b:freefree9262k
z-ai/glm-5.2:freefree8256k
nvidia/nemotron-3-ultra-550b-a55b:freefree61000k
minimax/minimax-m2.7:freefree4197k
google/gemma-4-31b-it:freefree2yes262k
deepseek/deepseek-v4-pro-0813budget151049k
deepseek/deepseek-v4-flashbudget121049k
deepseek/deepseek-chat-v3-0324budget10164k
openai/gpt-5.6-lunastandard10yes1050k
openai/gpt-5-ministandard8yes400k
anthropic/claude-haiku-4.5standard6yes200k
bytedance/ui-tars-1.5-7bvision16yes128k
google/gemini-2.5-flash-litevision10yes1049k
google/gemini-3.1-flash-litepremium12yes1049k
google/gemini-3.7-flashpremium8yes1049k
anthropic/claude-sonnet-5frontier10yes1000k

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