Skip to content

Add jev-router: Jev as a model-selection strategy under Cerberus policy - #28

Merged
Deep-Sixed merged 3 commits into
mainfrom
claude/zealous-edison-ik9ez1
Oct 5, 2026
Merged

Deep-Sixed merged 3 commits into
mainfrom
claude/zealous-edison-ik9ez1

Conversation

@Deep-Sixed

@Deep-Sixed Deep-Sixed commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Summary

Phase 1 of Jev routing. This adds a jev-router alias mode: OpenRouter's hosted Jev Router becomes one model-selection strategy, alongside dedicated paths and Fusion. Cerberus keeps the policy and decides what may run. Jev decides which of those should run for each request.

cerberus/auto:
  mode: jev-router
  candidates:                       # the pool; no preference order
    - { provider: openrouter, credential: primary, model: anthropic/claude-sonnet }
    - { provider: openrouter, credential: primary, model: google/gemini-pro }
  jev_router:
    timeout_seconds: 120
    allow_paid_pool: true

Cerberus filters first; Jev only sees what survives

  • Validation (registry/schema.py):
    • Every candidate is an exact registry slug, and the whole pool uses one OpenRouter credential.
    • Refused: wildcard/~family patterns (they could match models the registry never listed), duplicates, per-candidate reasoning_effort / chat_template_kwargs (Jev chooses reasoning effort per request), and allow_paid_fallback.
    • A paid pool member requires allow_paid_pool.
  • router/pool.py (new) is one filter used by dispatch and /admin/routes. It applies cost first, then health → cooldown → credential, the same order the failover loop uses. It doesn't depend on Jev, so the planned native Jev strategy can reuse it.
  • Only the filtered pool is sent, as the jev-router plugin's models list. An empty pool is never sent: OpenRouter treats a list that matches nothing as no list at all and routes over its whole pool, so Cerberus answers 503 instead.

models_ignored and other out-of-pool responses are rejected

Every call sends X-OpenRouter-Metadata: enabled. A response is accepted only when all of these hold:

  • the jev-router pipeline stage is present
  • it reports no list_fallback
  • the served model and every resolved_models entry are in the pool that was sent (a slug also matches its -YYYYMMDD revisions, as the plugin documents)

Any other response is withheld with 502 and recorded as out_of_policy. Because the call has already been billed, its usage and reported cost are still recorded. openrouter_metadata is stripped from the payload. The decision is reported in the cerberus envelope and in telemetry instead.

Callers can't influence routing

jev-router allows the shared CALLER_FIELDS from Deep-Sixed/Cerberus#23, minus reasoning_effort / chat_template_kwargs, plus function tools only.

  • Dispatch silently drops unknown fields. jev-router instead refuses them with a 400 before any upstream call. That covers models, route, plugins, preset, provider, server tools (openrouter:*, web_search, …), reasoning_effort / reasoning, and any field not on the list.
  • The 400 is recorded as an invalid_request routing event.
  • For that reason jev-router, like fusion, is exempt from Forward only standard chat fields and function tools upstream #23's app-level server-tool check.

Telemetry and console

  • Telemetry schema 6: a jev_router record (pool sent, returned model, router decision, violation) and three new outcomes: out_of_policy, jev_router_unavailable, invalid_request.
  • /admin/routes and the console show the alias as a pool (each member eligible or excluded, never standby), with readiness and decision evidence.
  • The control plane stores binding_type = 'jev-router' and route_members rows with role pool. No migration is needed.

Not in this PR

  • Streaming. The decision has to be checked before any of the answer is returned.
  • A per-request cost ceiling. Cerberus doesn't hold model prices, and a ceiling checked after a routed call is too late.
  • Phase 2: native Jev, where Cerberus asks Jev's decision API to choose among every eligible backend (local, Cloudflare, OpenRouter, …) and runs the winner itself.

Test plan

  • ./scripts/ci.sh on the branch with Forward only standard chat fields and function tools upstream #23 merged in: ruff clean, 376 passed.
  • Mutation check: disabling the pool check and the caller-field gate fails 21 of the new dispatch tests.
  • uv build --no-build-isolation; the wheel includes cerberus/jev_router.
  • node --check src/cerberus/ui/static/app.js
  • python scripts/check-public-history.py: 0 findings.
  • Live probe against OpenRouter, to confirm the openrouter_metadata stage shape matches the documentation. If it differs, every jev-router response is withheld as decision_unverifiable; that is the safe failure.

🤖 Generated with Claude Code

https://claude.ai/code/session_01P4Nm4MVSMmG6JyZi8WumtK

claude added 3 commits October 5, 2026 12:14
A jev-router alias hands model selection to OpenRouter's hosted Jev Router
while Cerberus keeps the policy. Cerberus answers "what may run?"; Jev
answers "which of those should run?" for one request.

- Schema: new `jev-router` mode with a `jev_router` block (backend,
  timeout_seconds, allow_paid_pool). The pool is exact registry slugs
  under one OpenRouter credential; patterns, duplicates, per-candidate
  reasoning budgets and allow_paid_fallback are refused at validation.
- router/pool.py: one strategy-neutral pool filter (cost, then health,
  cooldown and credential in the failover loop's order), shared by
  dispatch and /admin/routes and reusable by a native Jev strategy.
- Dispatch sends only the filtered pool as the jev-router plugin's
  include list, never an empty one, and always requests router metadata.
  A response is accepted only when the jev-router stage is present,
  reports no list_fallback, and names served and resolved models inside
  the pool sent; models_ignored and every other miss is withheld with
  502 as out_of_policy while its billed usage is still recorded.
- Callers may send an allowlist of generation fields and function tools;
  model lists, plugins, route, preset, provider, server tools, reasoning
  controls and unknown fields are refused with 400 before any call.
- Telemetry schema 6 adds a jev_router record and the out_of_policy,
  jev_router_unavailable and invalid_request outcomes. The console
  projects the alias as a pool with readiness and decision evidence.

Not streamed: the decision is verified before the answer is returned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P4Nm4MVSMmG6JyZi8WumtK
The merged caller-field allowlist refuses non-function tools in app.py for
every alias except fusion, ahead of mode dispatch and without a routing
event. jev-router enforces a stricter allowlist of its own with an
invalid_request event, so it is exempted the same way fusion is, and its
allowlist is now the shared CALLER_FIELDS less the reasoning controls
rather than a second copy that could drift.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P4Nm4MVSMmG6JyZi8WumtK
@Deep-Sixed
Deep-Sixed marked this pull request as ready for review October 5, 2026 12:58
@Deep-Sixed
Deep-Sixed merged commit 3e05756 into main Oct 5, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants