feat(routing): smart model routing for Nous Portal (session + delegation) - #43379
Closed
OutThisLife wants to merge 2 commits into
Closed
feat(routing): smart model routing for Nous Portal (session + delegation)#43379OutThisLife wants to merge 2 commits into
OutThisLife wants to merge 2 commits into
Conversation
Opt-in, cache-safe "Auto" model picker. A cheap classifier labels an incoming request's complexity tier (light/standard/heavy) and routes it to a tier-appropriate model — at the only two points with no cached prefix to invalidate: the start of a fresh session (before the first API call) and each delegate_task boundary (subagents start fresh). It never swaps the main model mid-conversation (that stays /model's job). - agent/model_router.py: classifier via auxiliary.routing_classifier, tier->model resolution with min_tier floor, fail-open everywhere, no-op when the chosen model matches the current one (no cache break). - conversation_loop.py: _maybe_apply_session_routing fires once per fresh session before the system prompt is built. - delegate_tool.py: _route_task_creds picks each subtask's model by goal; explicit delegation.model still wins. - config.py: smart_model_routing section (off by default) + auxiliary.routing_classifier task. - Docs + 18 unit tests.
Contributor
🔎 Lint report:
|
| Rule | Count |
|---|---|
unsupported-operator |
10 |
unresolved-attribute |
6 |
invalid-argument-type |
2 |
unresolved-import |
1 |
First entries
tests/cli/test_resume_display.py:716: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["resume_display"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 32 union elements`
tests/hermes_cli/test_aux_config.py:47: [unsupported-operator] unsupported-operator: Operator `not in` is not supported between objects of type `Literal["session_search"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 32 union elements`
tests/tools/test_browser_console.py:341: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["record_sessions"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 32 union elements`
tests/tools/test_web_providers.py:219: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["extract_backend"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 32 union elements`
tests/hermes_cli/test_mcp_reload_confirm_gate.py:33: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 32 union elements`
hermes_cli/config.py:4738: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 32 union elements`
tests/cli/test_fast_command.py:484: [invalid-argument-type] invalid-argument-type: Argument to bound method `TestCase.assertIn` is incorrect: Expected `Iterable[Any] | Container[Any]`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 32 union elements`
tests/cli/test_reasoning_command.py:552: [invalid-argument-type] invalid-argument-type: Argument to bound method `TestCase.assertIn` is incorrect: Expected `Iterable[Any] | Container[Any]`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 32 union elements`
tests/tools/test_web_providers.py:218: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["search_backend"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 32 union elements`
tests/agent/test_curator.py:1038: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["curator"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 32 union elements`
tests/hermes_cli/test_aux_config.py:54: [unresolved-attribute] unresolved-attribute: Attribute `keys` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 32 union elements`
tests/tools/test_web_providers.py:217: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["backend"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 32 union elements`
tests/hermes_cli/test_aux_config.py:37: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["title_generation"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 32 union elements`
tests/agent/test_model_router.py:19: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/gateway/test_whatsapp_reply_prefix.py:119: [unsupported-operator] unsupported-operator: Operator `>=` is not supported between objects of type `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 32 union elements` and `int`
tests/hermes_cli/test_kanban_core_functionality.py:3375: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 32 union elements`
tests/hermes_cli/test_destructive_slash_confirm_gate.py:32: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 32 union elements`
tests/tools/test_browser_lightpanda.py:242: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["engine"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 32 union elements`
hermes_cli/config.py:4728: [unresolved-attribute] unresolved-attribute: Attribute `items` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 32 union elements`
✅ Fixed issues (18):
| Rule | Count |
|---|---|
unsupported-operator |
10 |
unresolved-attribute |
6 |
invalid-argument-type |
2 |
First entries
hermes_cli/config.py:4684: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 31 union elements`
tests/tools/test_web_providers.py:218: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["search_backend"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 31 union elements`
hermes_cli/config.py:4674: [unresolved-attribute] unresolved-attribute: Attribute `items` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 31 union elements`
tests/tools/test_browser_console.py:341: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["record_sessions"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 31 union elements`
tests/hermes_cli/test_destructive_slash_confirm_gate.py:32: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 31 union elements`
tests/tools/test_browser_lightpanda.py:242: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["engine"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 31 union elements`
tests/cli/test_resume_display.py:716: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["resume_display"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 31 union elements`
tests/agent/test_curator.py:1038: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["curator"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 31 union elements`
tests/hermes_cli/test_aux_config.py:54: [unresolved-attribute] unresolved-attribute: Attribute `keys` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 31 union elements`
tests/tools/test_web_providers.py:219: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["extract_backend"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 31 union elements`
tests/hermes_cli/test_aux_config.py:47: [unsupported-operator] unsupported-operator: Operator `not in` is not supported between objects of type `Literal["session_search"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 31 union elements`
tests/gateway/test_whatsapp_reply_prefix.py:119: [unsupported-operator] unsupported-operator: Operator `>=` is not supported between objects of type `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 31 union elements` and `int`
tests/cli/test_fast_command.py:484: [invalid-argument-type] invalid-argument-type: Argument to bound method `TestCase.assertIn` is incorrect: Expected `Iterable[Any] | Container[Any]`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 31 union elements`
tests/cli/test_reasoning_command.py:552: [invalid-argument-type] invalid-argument-type: Argument to bound method `TestCase.assertIn` is incorrect: Expected `Iterable[Any] | Container[Any]`, found `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 31 union elements`
tests/tools/test_web_providers.py:217: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["backend"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 31 union elements`
tests/hermes_cli/test_kanban_core_functionality.py:3375: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 31 union elements`
tests/hermes_cli/test_aux_config.py:37: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["title_generation"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 31 union elements`
tests/hermes_cli/test_mcp_reload_confirm_gate.py:33: [unresolved-attribute] unresolved-attribute: Attribute `get` is not defined on `str`, `list[Unknown]`, `list[str]`, `None`, `int` in union `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 31 union elements`
Unchanged: 5567 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
Collaborator
Smart model routing now ships as a Nous Portal capability. The router
only engages when the active (session) / parent (delegation) model is on
Nous Portal, and short-circuits BEFORE the classifier call so off-Portal
users never incur a picker cost. Every tier resolves through the Nous
provider (the Portal fronts frontier models across vendors behind one
credential), so tiers are configured as bare Nous model ids.
- model_router: Nous-only gate in route() ahead of classification; tiers
resolve via the nous provider; _tier_model accepts a bare id (legacy
{provider, model} dict still accepted, provider ignored).
- config: routing_classifier defaults to provider nous; tiers default to
bare Portal model ids.
- docs: Prerequisites + Nous-only framing.
- tests: route() gate (off-Portal no-op skips classifier), tier-resolves
-through-nous; all 22 pass.
Collaborator
Author
|
@teknium1 closing this for now |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
An "Auto" model picker for Hermes, scoped to the Nous Portal. A cheap classifier labels each incoming request's complexity tier (
light/standard/heavy) and runs it on a tier-appropriate Portal model. Hard tasks get a frontier model; trivial ones get something small and fast.Nous Portal only. Every tier resolves through the
nousprovider — the Portal already fronts frontier models across vendors (anthropic/…,openai/…,google/…,x-ai/…) behind one credential, so a single Portal key covers every tier. The router only engages when the active (session) / parent (delegation) model is itself on Nous Portal, and it short-circuits before the classifier call, so off-Portal users never incur a picker cost and are never silently moved onto Nous.Cache-safety
Hermes' per-conversation prompt cache is sacred, so routing only happens where there is no cached prefix to invalidate:
delegate_tasksubtask's goal and picks the subagent's model (subagents start from fresh context).It never swaps the main model mid-conversation — that stays
/model's job (which deliberately resets the cache). An explicitdelegation.model//modelalways wins.Design
smart_model_routing.enabled.default_tierand never wedges a turn.min_tierfloor lets you forbid thelighttier.Changes
agent/model_router.py— classifier + tier resolution + Nous-only gate.agent/conversation_loop.py— session-start routing (_maybe_apply_session_routing).tools/delegate_tool.py— delegation routing (_route_task_creds).hermes_cli/config.py—smart_model_routingblock +auxiliary.routing_classifier(defaults to Nous Portal).website/docs/user-guide/features/smart-model-routing.md— feature docs.Test plan
scripts/run_tests.sh tests/agent/test_model_router.py— 22/22 pass.min_tierfloor honored; classifier fail-open todefault_tier.delegation.modelbeats routing.