-
Notifications
You must be signed in to change notification settings - Fork 1
Model Router
The Model Router enables multi-LLM support by analyzing GitHub issue complexity and routing tasks to the appropriate model. Simple tasks go to lightweight models (8B parameters), complex tasks go to heavyweight models (30B+).
GitHub Issue
↓
Complexity Analysis (heuristic scoring 0-10)
↓
Score < threshold → "light" tier → 8B model
Score >= threshold → "heavy" tier → 30B model
↓
Selected model passed to DockerManager
↓
Minion spawned with the routed model
Issues are scored from 0 to 10 based on these signals:
| Signal | Score Impact | Details |
|---|---|---|
| Simple labels | -2 |
docs, typo, easy, good-first-issue, minor, documentation
|
| Complex labels | +2 |
refactor, architecture, performance, security, breaking-change, multi-file
|
| Long body (>2000 chars) | +2 | Indicates detailed/complex requirements |
| Medium body (>800 chars) | +1 | Moderate complexity |
| Checklist items (>=5) | +2 | Many subtasks |
| Checklist items (>=2) | +1 | Some subtasks |
| Code blocks (>=3) | +1 | Multiple code examples |
| File references (>=5) | +2 | Multi-file changes |
| File references (>=2) | +1 | Some file changes |
| Complex title keywords | +2 | "refactor", "redesign", "migrate", "rewrite", "overhaul" |
| Simple title keywords | -1 | "fix typo", "update readme", "bump version", "rename" |
The final score is clamped to the range 0-10.
Title: "Fix typo in README"
Labels: [docs, typo]
Body: "Line 42 has a spelling error"
Signals: simple labels (-2), simple title keyword (-1) → clamped to 0
Title: "Refactor authentication system"
Labels: [refactor, security]
Body: (2500 chars with 6 checklist items, 3 code blocks, 5 file refs)
Signals: complex labels (+2), long body (+2), checklist (+2), code blocks (+1), file refs (+2), complex title (+2) → clamped to 10, but realistic score ~7-9
ROUTING_ENABLED=true
ROUTING_COMPLEXITY_THRESHOLD=5 # Score threshold for heavy tier
ROUTING_DEFAULT_TIER=heavy # Fallback when routing failsModel profiles are configured via a JSON environment variable:
ROUTING_MODELS='{
"light": {
"name": "llama-3.1-8b",
"base_url": "http://localhost:5000/v1",
"timeout": 300
},
"heavy": {
"name": "qwen3-coder-30b",
"base_url": "http://gpu-server:8080/v1",
"timeout": 600
}
}'| Field | Type | Default | Description |
|---|---|---|---|
name |
string | required | Model name |
tier |
string | required | "light" or "heavy" |
base_url |
string | "" | LLM server URL (inherits default if empty) |
timeout |
int | 600 | Request timeout in seconds |
streaming |
bool | false | Enable streaming |
When routing is enabled:
- Overlord calls
router.select_model(title, body, labels) - Router computes complexity score
- Score determines tier ("light" or "heavy")
- Router looks up the
ModelProfilefor that tier - If no profile found, falls back to
default_tier - If still no profile, returns
None(uses default LLM config) - Selected model is passed to
DockerManager.spawn_minion()
When routing is disabled (default):
-
select_model()returnsNone - DockerManager uses the global
LLMConfigfor all minions
The router is used at two dispatch points in the Overlord:
-
Slack command handler: Fetches issue details via
GitHubQueue.get_issue_details(), then routes -
Cron queue sweep: Uses the
QueuedIssue.bodyfield already fetched during scanning
Slack notifications include the selected model name:
🚀 Spawning minion `minion-abc123` to work on owner/repo#42 (model: `llama-3.1-8b`)
nebulus_swarm/
├── config.py # ModelProfile, RoutingConfig dataclasses
└── overlord/
├── model_router.py # ModelRouter, ComplexityScore
├── main.py # Integration at both dispatch points
└── docker_manager.py # spawn_minion() accepts model_override
- Nebulus Swarm - System overview
- Swarm Overlord - Where routing decisions happen
- Configuration - Environment variables