Imported from himanshu231204/jev_model_routers (
AGENTS.md). Install upstream withnpx skills add himanshu231204/jev_model_routers. Copyright stays with the author.
AGENTS.md — JEV Model Router
Agent-agnostic model-routing layer for coding agents (Claude Code, Codex, OpenCode, DeepAgents, Hermes). Flow: coding agent → adapter → normalized request → router → policy → model resolver → provider.
Current state of this repo (read this first)
- All phases are implemented. Every package under
src/jev_router/(cli/,core/,contracts/,adapters/,providers/,jev/,transport/,state/,config/,observability/,security/) has real code, not stubs — e.g.core/router.py,core/policy.py,core/resolver.py,jev/client.py, and full adapters forclaude_code,codex,opencode,hermes,deepagents. Implement inside the existing files/packages, do not restructure or rename packages to suit yourself. tests/exists and is populated undertests/unit/,tests/integration/,tests/contract/,tests/adapters/,tests/routing/,tests/fixtures/(seeARCHITECTURE.md§9).tests/live/coversjev_router_live(§16) the same way.python -m pytestpasses (103 tests as of this writing). Add tests alongside any change per the testing rules below.- No CI workflows, linter, formatter, type-checker, pre-commit, or lockfile exist. Do not invent tool commands or add tooling unless asked. Verification today = import check + pytest.
ARCHITECTURE.md(~204 lines, 15 sections) is the design source of truth — read it directly.docs/holds onlydocs/README.md(points here) anddocs/quickstart.md(integration guide, hand-maintained, may be edited directly to stay accurate tosrc/). The old one-file-per-section split (docs/01–docs/14) and five empty placeholder directories were removed as unnecessary duplication. All ofdocs/is tracked and committed — it is not local scratch. Only.superpowers/,.tmp/, and.agents/are local/untracked work; leave those alone.- The JEV client is wired to TypeSafe's real System One API, not a placeholder.
jev/ client.pycallsPOST https://api.typesafe.ai/v1/systemonewithTYPESAFE_API_KEYas the bearer token — the same env var name TypeSafe's own SDK reads by default, even though this router calls the raw HTTP API directly via stdliburllib(zero runtime deps) rather than depending on the SDK.jev/questions.pybuilds the{model, state, questions}request (a single"tier"choice question);jev/normalize.pyparses the{answers: {tier: {choice, confidence}}}response. Both shapes are verified against TypeSafe's quickstart (docs.typesafe.ai/introduction/quickstart) and SDK docs (docs.typesafe.ai/sdk/python,/sdk/javascript). jev-router run --agent <name>actually launches the agent now, viaAgentAdapter.launch_command(model)+subprocess.run, after one JEV routing decision at session start. This is session-start routing only, not per-turn —jev_router's owntransport/only sends outbound; nothing listens for mid-session requests. Per-turn routing via a live local proxy exists as a separate package,src/jev_router_live/(jev-claude/jev-codex/jev-explain; seeARCHITECTURE.md§16 andsrc/jev_router_live/README.md) — it does not share code withjev_routerand is not part of the adapter/core/contracts pipeline described above.launch_commandwas verified against each real installed CLI's--help, not guessed:claude --model <m>andhermes chat --model <m>are correct;codex --model <m>is unverified (binary not available to test against).opencode.launch_commandanddeepagents.launch_commandboth raiseNotImplementedErrorrather than emit a broken command — opencode's interactive CLI has no top-level--modelflag, and deepagents has no CLI binary at all.HermesAdapter.detect()now usesshutil.which("hermes")like every other adapter (it was hardcoded to always returnFalse). The Reverse Proxy and SDK Adapter integration strategies indocs/quickstart.mdremain unimplemented.cli/run.pyresolveslaunch_command's argv[0] viashutil.which()before callingsubprocess.run. A bare"claude"fails Windows'sCreateProcesseven when it's on PATH andshutil.whichfinds it — the real executable there is a.CMDshim (e.g.claude.CMD) and the extensionless name doesn't resolve the same waycmd.exeresolves it. Passing the resolved full path fixes it on both Windows and POSIX. Verified live:jev-router run --agent claude_codenow actually launchesclaude(previously:FileNotFoundErrordumped as a raw traceback). Missing-binary and launch-failure cases print a clean one-line error and return 1 instead of crashing.jev-router runnow takes--prompt/-p. Without it, JEV is asked to route an empty string and — live-verified — reliably returns confidence around 0.27 (below thelowthreshold), socore/policy.pyfalls back (reason=low_confidence_keep_current) instead of routing. The same real prompt live-verified confidence 0.97 for the same tier. Always pass--promptfor a real routing decision; omitting it is a deliberate no-signal passthrough, not a bug.ClaudeCodeAdapter.launch_commandnow translates catalog ids to real Claude Code aliases (_MODEL_ALIASESinadapters/claude_code/adapter.py:anthropic/claude-sonnet→sonnet,anthropic/claude-opus→opus), fixing the "isn't described by this version's model catalog" errorclaude --model anthropic/claude-sonnetproduced before. Only these two ids are mapped —openai/coding-strongand any other catalog id still pass through unchanged, since only Claude Code's own aliases have been verified.codex/hermes/opencodestill receive the router's internal catalog id as-is; onlycodex's--modelflag is documented to accept a bare model name, and none of the three have a verified translation table the wayclaude_codenow does.cli/run.py's_candidates()now sets realtier/capabilities/compatible_agentsvia_KNOWN_MODELS, fixing model auto-detection. Previously every catalog entry gotModelSpec's bare defaults (tier="balanced", identical capabilities, nocompatible_agents), so a JEV"strong"recommendation could never match any candidate's tier and silently fell back to whichever entry the fallback tie-break happened to prefer. Worse: with no"fast"-tier candidate in the default catalog at all, every"fast"recommendation (the common case — most tasks are simple) fell back to the highest-capability candidate, meaning trivial tasks were silently routed toopus— the opposite of what "fast" means. Fixed by addinganthropic/claude-fable(real aliasfable, verified viaclaude --help) andanthropic/claude-haiku(real aliashaiku— not listed in--help's examples but confirmed working:claude --model haikupasses model validation and proceeds to a real API call, unlike a deliberately fake model name, which errors immediately withunrecognized_model) as genuine fast-tier catalog entries, and assigning real tier/capability/compatible-agent metadata to all five default ids.fableandhaikushare the same fast-tier capability score andfableis listed first, so it wins ties deterministically; reordermodels.allowto preferhaikuinstead. Live-verified all three tiers now resolve distinctly forclaude_code: trivial →anthropic/claude-fable, medium →anthropic/claude-sonnet, complex →anthropic/claude-opus.compatible_agentsalso now correctly excludesopenai/coding-strongfrom ever winning aclaude_codelaunch. Ids not in_KNOWN_MODELS(e.g. a user's custom catalog addition) still fall back toModelSpec's lenient defaults rather than being rejected.- README's Roadmap section was removed (it was pre-implementation and out of date); README no
longer tracks phase-by-phase progress. Trust
src/andARCHITECTURE.mdover README prose if either ever disagrees with it.
Commands
- Python >= 3.11,
src/layout, setuptools build, package namejev_router. - Zero runtime dependencies (
dependencies = []) — stdlib only. Adding a dependency is a significant decision; justify it (ARCHITECTURE.md §29: dependency rules).pytestis a test-only extra ([project.optional-dependencies] test = ["pytest"]), not a runtime dep. - Install:
pip install -e .(exposes thejev-routerconsole script →jev_router.cli.main:main, which dispatches torun/agents/models/status/explain/doctorsubcommands). For development,pip install -e ".[test]"to get pytest. - All tests:
python -m pytest - One file / one test:
python -m pytest tests/unit/test_policy.pyorpython -m pytest tests/unit/test_policy.py::test_name - Config used at runtime:
configs/default.yaml,configs/example.yaml.
Architecture and dependency direction
Packages under src/jev_router/: cli/, core/, contracts/, adapters/, providers/,
jev/, transport/, state/, config/, observability/, security/.
Dependency direction is one-way: CLI → adapters → core → contracts.
core/importscontracts/only. Providers and transports are injected, never imported by policy/router code.- Agent protocol knowledge (parsing, proxies, version quirks) →
adapters/<agent>/only. Neverif agent == "claude":incore/. - Upstream model API knowledge (endpoints, auth, streaming) →
providers/. - HTTP/SSE/WS forwarding →
transport/. Per-turn/session persistence →state/. Secrets/redaction →security/. Logging/metrics/explanations →observability/. - Adding an agent = new
adapters/<foo>/+ fixtures + tests. It must not require touching the core router, policy, JEV auth, state, or registry.
Routing invariants (must not regress)
- JEV is the only routing authority.
TYPESAFE_API_KEYis read exclusively byjev/client.py; no other module may build JEV auth headers. Never hard-code, commit, or print the key.core/classifier.pymust not grow into an independent LLM router. - One routing decision per fresh user turn. Pin the selected model for the whole tool loop; tool results, continuations, telemetry, and title/summary calls bypass routing.
- Explicit user model choice always wins over automatic routing.
- Fail open, but never silently: missing key, timeout, 5xx, or malformed JEV response → fall back to current/default model and record why (current → agent default → configured fallback → clear error).
- State is scoped
agent + session + turn. Sub-agent decisions must not overwrite the parent session's pinned model. No globalcurrent_model. - Policy sits between JEV and execution: JEV output → policy (confidence thresholds, override, model availability, context/downgrade protection) → resolver → concrete model.
- Never log prompts, keys, auth headers, or raw responses by default (
configs/default.yamlshipsprivacy.log_prompts: false,send_repository_content: false— keep those semantics).
Configuration
- Precedence (implemented in
config/loader.py): CLI args > env vars > project config > user config > defaults. configs/default.yaml:jev.timeout_ms: 1500,deadline_ms: 3000,max_retries: 1. Routing is on the interactive hot path — keep calls short; retry only within the deadline.- Routing is disabled (passthrough) when
TYPESAFE_API_KEYis absent — the CLI must report this clearly instead of crashing a running session.
Testing rules
- pytest only;
pyproject.toml[tool.pytest.ini_options] testpaths = ["tests"]. - Mock the JEV API in all default tests. Live tests are opt-in:
JEV_LIVE_TESTS=1plusTYPESAFE_API_KEYin the environment; never put a real key in fixtures. - Adapter tests are fixture-driven (
tests/fixtures/): capture sanitized upstream requests when a protocol changes, note the agent version, and assert expected routing behavior. - Every non-trivial change to policy, resolution, overrides, fresh-turn detection, state isolation, or fallback needs a test — these are the pure-logic hot spots.
Key contracts and modules
contracts/requests.py—NormalizedRequest(agent, session, turn, prompt, current model, available models, context tokens, tools, explicit override, routing mode). Optional fields stay nullable; never invent data an adapter can't reliably provide.contracts/models.py—ModelSpeccapability metadata (coding, reasoning, tool_use, context, speed, cost, context window, vision, availability, agent compatibility).jev/schema.py—JEVDecision(selected model/profile, confidence, task complexity, reasoning/tool signals, explanation). Normalize external JEV responses at this boundary only; raw provider/agent formats must not spread past it.core/overrides.py— explicit human model choice detection; consulted before JEV.core/resolver.py— picks a concrete model:available ∩ agent-compatible ∩ provider-compatible ∩ policy-allowed.state/—memory.py(in-process) andsqlite.py(persistent) behindstore.py; holdsTurnState(session, turn, selected model, selected_at, confidence, reason).adapters/registry.py/providers/registry.py— discovery points; adapters and providers register here instead of being wired intocore/by hand.
Common workflows
Add a new agent adapter foo:
- Create
src/jev_router/adapters/foo/withadapter.py(+ parser/proxy/config as needed). - Implement normalization into
NormalizedRequest, fresh-turn detection, manual-override detection, andapply_model— reusejev/client.pyfor JEV auth, never your own. - Register it in
adapters/registry.py. - Add sanitized request/response fixtures + tests: fresh turn, tool continuation, manual model, auxiliary call, malformed request, unavailable model.
- Do not modify
core/to understandfoo.
Add a provider: implement under providers/, register in providers/registry.py, add
ModelSpec entries, cover streaming + error semantics with tests. Providers never contain
routing policy and never learn why a model was chosen.
Change routing behavior: policy/resolution logic lives in core/policy.py +
core/resolver.py as pure, typed, deterministic functions with thresholds in config — not as
magic constants scattered through adapters. Update ARCHITECTURE.md alongside.
Definition of done
- Code landed in the correct layer (see dependency direction above).
- Explicit overrides, fresh-turn pinning, and fail-open fallback still hold.
- No secrets or prompt content added to logs/errors.
-
python -m pytestpasses; new tests added. - Behavior change reflected in
ARCHITECTURE.md(notdocs/*).
Full behavioral contract: ARCHITECTURE.md. Product/roadmap context: README.md.
