Skip to main content

LLM Routing

reeve/llm/router.py routes each task kind to a model tier via a fixed lookup table:

HAIKU = "claude-haiku-4-5-20251001"
SONNET = "claude-sonnet-4-6"
OPUS = "claude-opus-4-8"

_TASK_MODEL: dict[TaskKind, str] = {
TaskKind.CLASSIFY_FUNCTIONS: HAIKU,
TaskKind.ANALYZE_FUNCTION: SONNET,
TaskKind.FORM_HYPOTHESIS: SONNET,
TaskKind.TEST_HYPOTHESIS: SONNET,
TaskKind.SYNTHESIZE_COMPONENT: SONNET,
TaskKind.DEOBFUSCATE_FUNCTION: SONNET,
TaskKind.GLOBAL_SYNTHESIS: OPUS,
TaskKind.GENERATE_REPORT: OPUS,
TaskKind.ANSWER_QUESTION: SONNET,
}

def model_for_task(kind: TaskKind) -> str:
return _TASK_MODEL.get(kind, SONNET)

Haiku handles cheap classification, Sonnet handles per-function and per-component reasoning, and Opus handles whole-binary synthesis and the final report.

What Each Model Handles

Task kindModelWhat it does
classify_functionsHaikuLightweight per-function classification.
analyze_functionSonnetPer-function naming and typing — see below.
form_hypothesis, test_hypothesisSonnetTurns a claim template into a specific, falsifiable claim, then tests it.
synthesize_componentSonnetPer-component summary.
answer_questionSonnetreeve chat/reeve ask answers.
global_synthesis, generate_reportOpusWhole-binary synthesis and the final report.

The analyze_function Prompt

LLMReasoner.analyze_function() sends a system prompt that requires a strict JSON response (name, confidence, prototype, params, comment, struct_proposals, evidence_summary) and a user message built from already-computed graph facts: known callee names, import categories, type inferences, clustered string samples, the component hypothesis, obfuscation notes, and the decompiled body.

Response parsing extracts the JSON object from the response text and validates it against the expected fields. A malformed response degrades to a placeholder name with confidence 0.0 rather than raising.

Cost Tracking

CostTracker accumulates token usage per model and prices it from a per-1M-token table (llm/usage.py), separately tracking cache writes and cache reads:

_PRICING = {
"claude-haiku-4-5-20251001": {"input": 0.80, "output": 4.00, "cache_write": 1.00, "cache_read": 0.08},
"claude-sonnet-4-6": {"input": 3.00, "output": 15.00, "cache_write": 3.75, "cache_read": 0.30},
"claude-opus-4-8": {"input": 15.00, "output": 75.00, "cache_write": 18.75, "cache_read": 1.50},
}

session.print_status() prints total cost; cost_tracker.summary() breaks it down per model with input/output/cache-read/cache-write token counts. --budget/-b sets a hard cost ceiling for the run — the executor checks it before starting each task and stops scheduling new LLM tasks once it's reached.

Prompt Caching

AnthropicClient caches the system prompt block via cache_control: {"type": "ephemeral"}. Since every function-analysis call in a run shares the same system prompt string, this caches well across a single session.