Skip to content

feat(router): Executive Router v1 — intelligent multi-provider model routing#662

Open
tonyskifo wants to merge 5 commits into
outsourc-e:mainfrom
tonyskifo:feature/executive-router-v1
Open

feat(router): Executive Router v1 — intelligent multi-provider model routing#662
tonyskifo wants to merge 5 commits into
outsourc-e:mainfrom
tonyskifo:feature/executive-router-v1

Conversation

@tonyskifo

Copy link
Copy Markdown

Summary

Adds a fully opt-in Executive Router pipeline to Hermes Workspace. When
routing.enabled: true in ~/.hermes/config.yaml, every chat request is
classified and dispatched to the most appropriate provider and model based
on task type, complexity, and a configurable daily Opus budget cap.

Routing is disabled by default — no behaviour changes until opted in.

What's included

Step Description
1–3 Type system (RoutingConfig, PolicyRule, RouterPoolEntry), routing config schema, DeepSeek provider
4–5 Pure task classifier and executive router with manual overrides (use:opus, use:deepseek, model:<id>)
6 Provider-agnostic model usage tracker with daily Opus budget enforcement
7 Router integrated into send-stream.ts with automatic routing and usage recording
8 Settings UI for Smart Routing, including enable toggle, default provider/model, Opus threshold and daily budget

Key design properties

  • Pure routing engine with no I/O
  • Disabled by default
  • Manual per-message overrides
  • Daily Opus budget enforcement
  • Provider-agnostic design
  • Graceful fallback if routing fails

Test coverage

  • hermes-config-store.routing.test.ts
  • provider-catalog.test.ts
  • task-classifier.test.ts
  • executive-router.test.ts
  • model-usage-tracker.test.ts
  • routing-config.test.ts

111 tests added

Test plan

  • Routing disabled behaves identically to current main
  • Routing enabled correctly routes requests
  • Manual overrides (use:opus, model:...) work
  • Daily Opus budget is enforced
  • Settings page saves routing configuration
  • No new TypeScript errors introduced

Notes

This feature is completely opt-in and preserves existing behaviour unless routing is explicitly enabled.

  • feat(router): steps 1-3 — types, routing config schema, DeepSeek provider
  • feat(router): steps 4-5 — task classifier and executive router
  • feat(router): step 6 — provider-agnostic model usage tracker
  • feat(router): step 7 — wire executive router into send-stream
  • feat(router): step 8 — add routing settings UI

root and others added 5 commits June 28, 2026 17:35
…ider

Add the foundational layer for Executive Router v1.  No dispatch logic
is wired yet; these are the schema, config, and catalog primitives that
Steps 4-7 will build on.

src/types/router-config.ts (new)
  Complete type system for the routing pipeline: TaskType, ContextLen,
  Urgency, TaskClassification, RouteDecision, RouterPoolEntry,
  PolicyMatch, PolicyRule, RouterEscalationConfig, RoutingConfig.

src/server/hermes-config-store.ts (additive)
  - SetRoutingConfigPatch action added to HermesConfigPatch union
  - DEFAULT_ROUTING_CONFIG constant (Sonnet default, Opus threshold
    0.75, 5 USD/day cap, empty pool/policy)
  - readRoutingConfig(paths?) safe parser for the routing: block in
    ~/.hermes/config.yaml; validates every field, falls back to defaults
    on missing or malformed values, never returns partial data
  - applySetRoutingConfig shallow-merges patch into existing routing:
    block without touching other config keys
  - Existing applyHermesConfigPatch switch extended; exhaustive check
    still passes

src/lib/provider-catalog.ts (additive)
  DeepSeek entry added between MiniMax and Ollama. Uses OpenAI-
  compatible API so no new backend handler is needed; baseUrl override
  in openai-compat-api.ts already supports it.

Tests: 23 tests, all passing (vitest run)
  src/server/hermes-config-store.routing.test.ts: 15 tests covering
    readRoutingConfig and set-routing-config patch with real temp-dir
    YAML I/O
  src/lib/provider-catalog.test.ts: 8 tests covering DeepSeek catalog
    entry, getProviderInfo, getProviderDisplayName

No changes to gateway-capabilities.ts, chat dispatch, or PR outsourc-e#661.
Pure-function pipeline for intelligent model routing:

task-classifier.ts
- parseManualOverride(): use:opus/sonnet/codex/gemini/deepseek/local/fast
  and model:<id> (model: takes precedence over use:X)
- classifyTask(): token estimation, keyword-based task_type detection,
  log-scaled complexity 0–1, urgency from last user message

executive-router.ts
- route(): 6-step decision tree — manual override → routing disabled →
  policy rules (first match, skip disabled providers) → complexity
  escalation to Opus → default; Opus budget cap enforced at every path

Tests: 52 tests across task-classifier.test.ts and executive-router.test.ts
No new tsc errors introduced.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Persists per-request usage to ~/.hermes/model-usage.json.

- ModelUsageRecord: id, ts, provider, model, input/output tokens,
  cost_usd, latency_ms, success, error
- estimateCost(): pricing table for 13 models across Anthropic, OpenAI,
  Google, DeepSeek, Ollama; prefix-match for versioned IDs; zero for local
- recordUsage(): computes cost, appends to JSON array; never throws
- readUsageLog(): reads full log; returns [] on missing/corrupt file
- sumCostToday(): aggregates today's cost with optional provider/model filter
- getOpusSpendToday(): convenience wrapper for router budget enforcement

All paths injectable for testing. 29/29 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wires the executive router pipeline into the hot chat path.
Router is opt-in (routing.enabled: false by default) and never
overrides an explicit user-selected model.

- Import readRoutingConfig, classifyTask, parseManualOverride,
  route, getOpusSpendToday, recordUsage, RouteDecision
- After local-model detection: classify task, call route(), apply
  RouteDecision — sets chatMode/localBaseUrl for non-Anthropic providers
- Thread routerDecision?.model into Responses API, openaiChat,
  and streamChat dispatch points
- Record usage (tokens, cost, latency) on openaiChat success and failure
- Router block is wrapped in try/catch — failure always falls through
  to existing defaults; no change in behaviour when routing disabled

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a Smart Routing settings panel at /settings?section=routing.

settings-sidebar.tsx
- Add 'routing' to SettingsNavId (fixes pre-existing tsc error at line 360)
- Add 'Smart Routing' nav item between Agent Behavior and Voice

hermes-config-route.ts
- Import readRoutingConfig; expose routingConfig field in GET response
- Add set-routing-config to PatchActionSchema (zod-validated) and ACTION_MESSAGES

settings/index.tsx
- Add RoutingConfigSnapshot type and routingConfig? to ClaudeConfigData
- Add 4 routing state vars; sync from fetchConfig via syncInputsFromData
- Add saveRoutingConfig() — sends action: set-routing-config PATCH
- Replace renderSmartRouting() with renderExecutiveRouting():
  - Executive Router section: enabled toggle, default provider, default model
  - Opus Escalation section: complexity threshold, daily USD budget
  - Pool & Policy section: read-only entry count when config has entries

src/routes/api/-routing-config.test.ts (new, 7 tests)
- GET returns routingConfig with defaults
- GET reflects persisted routing block
- PATCH writes fresh config, merges without losing keys, saves escalation
- PATCH 400 on missing routing field, 503 when capability unavailable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@tonyskifo
tonyskifo requested a review from outsourc-e as a code owner June 28, 2026 16:54
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.

1 participant