We run Hyred, an AI CV review platform for job seekers in Indonesia. A free review has to come back in about two minutes and cost almost nothing. A paid rewrite has to be good enough that someone was happy to pay for it. Those two requirements pull in opposite directions, so this post is about how we split the work between models.
One entry point for every LLM call
Every model call in the codebase goes through a single function, executeStructuredAi. It takes a workflow name, a step, a JSON schema and the input. Then it:
- picks the model for that step from one tier table,
- requires structured output that matches the schema,
- logs the cost of the call,
- falls back to OpenAI automatically if the Claude call fails.
The payoff came when we changed providers. The migration touched one module instead of every background task.
Tiering by workflow step
The tier table is a plain map from workflow:step to a model. Trimmed:
const CLAUDE_MODEL_BY_WORKFLOW_STEP = {
"cv_review:pdf_structured_extraction": CLAUDE_HAIKU_MODEL,
"cv_review:free_grading": CLAUDE_HAIKU_MODEL,
"paid_review:paid_feedback": CLAUDE_OPUS_MODEL,
"paid_review:cv_rewrite": CLAUDE_OPUS_MODEL,
"linkedin_optimization:linkedin_optimization": CLAUDE_SONNET_MODEL,
"interview_scoring:interview_scoring": CLAUDE_SONNET_MODEL,
};
Haiku handles the high-volume free funnel. Opus writes the deliverables people pay for. Sonnet covers the paid analysis in between. Effort and thinking settings come from the same per-workflow policy, so one table decides both which model runs a step and how hard it reasons.
No OCR step
The first version ran a dedicated parsing model, then a formatting pass, then grading. Now the signed PDF goes straight to the model, which returns the structured CV (contact details, experience, education, skills) in one call. That is one fewer model to pay for and one fewer hand-off where a layout can get mangled between steps.
Durable steps with Trigger.dev
Extraction and grading run as separate Trigger.dev tasks. If grading fails, the extracted CV is already stored, so a retry does not pay for extraction again. Scheduled jobs like follow-up emails and SEO metrics live in the same repo as cron tasks, so there is no separate worker or Redis to run.
What the grader is scored against
The rubric was built from 1,500+ real CVs and the criteria recruiters actually hire on: keywords that match the role, achievements instead of a list of duties, a structure an ATS can parse, and contact details that are not hidden in a header or a table cell. The point of the free review is not the score itself. It is showing someone why their applications go quiet and what to change first.
What I would do differently
- Build the single LLM entry point on day one. Retrofitting it later meant touching every task once anyway.
- Ship the free checker publicly first. Ours sat behind a login for months, and a competitor with a public checker took the search traffic.
The first review on Hyred is free. If you want to see what the output looks like before uploading anything, the ATS check page has example results.
Top comments (0)