Skip to content

Feature request: per-issue model selection based on complexity and content #1417

Description

@TornadoStorm

Problem

Model selection is currently a single static string per repository (or one global default). Resolution happens in EdgeWorker.js:

const model = session.metadata?.model ||
repoConfig.claudeDefaultModel ||
repoConfig.model ||
"claude-opus-4-6";
There's no way to route different issues to different models based on anything about the issue itself. In practice this means either every issue runs on the (expensive, slower) default, or someone has to eyeball each issue and manually edit config.json before delegating — which defeats the point of unattended delegation.

Worth noting: session.metadata?.model is checked with the highest priority in that chain, but nothing in the codebase ever sets it — it looks like scaffolding for a per-session override that was never wired up. That seems like the natural landing spot for this feature.

Also relevant: labelPrompts (LabelPromptConfigSchema) already lets per-label config restrict allowedTools/disallowedTools, but has no model field — so today there's no way to do this even manually via labels, let alone automatically.

Proposed behavior

Let a repository config define tiered model selection by complexity, with content/label-based overrides that take precedence. Example of the behavior we want:

Complexity ≤ 3 → Haiku
Complexity ≤ 8 → Sonnet
Complexity > 8 → Opus
Regardless of complexity: if the issue is a critical security patch or an architectural migration → always Opus
"Complexity" maps naturally onto Linear's native issue estimate field, so no new Linear-side concept is needed — just reading a field Cyrus likely already has access to via the issue payload/API.

The override condition (security patch / architectural migration) needs to work off more than a fixed label list, since that kind of intent is expressed in free-form title/description text as often as in labels. A keyword/regex match against title+description, in addition to label matching, would cover both cases without requiring the user to remember to tag everything correctly.

Proposed config shape

{
"repositories": [
{
"id": "...",
"model": "sonnet", // fallback default, unchanged from today
"modelSelection": {
"complexitySource": "linear.estimate", // or a future alternative source
"byComplexity": [
{ "maxComplexity": 3, "model": "haiku" },
{ "maxComplexity": 8, "model": "sonnet" }
],
"aboveMaxModel": "opus", // complexity higher than every tier above
"overrides": [
{
"model": "opus",
"matchLabels": ["security", "critical"],
"matchContent": ["security patch", "CVE-", "architectural migration", "architecture migration"]
}
]
}
}
]
}
Suggested precedence, consistent with the existing resolution chain: session.metadata.model (still the manual escape hatch) → modelSelection.overrides (first match wins) → modelSelection.byComplexity → modelSelection.aboveMaxModel → repoConfig.model → hardcoded default.

Why this matters

Without it, users are stuck choosing between running everything on the expensive model (wasteful for the bulk of routine issues) or running everything on a cheaper model (risky for the security/architecture-sensitive minority that actually needs the strongest model). Automatic tiering with a content-aware override lets Cyrus be delegated a full backlog unattended without either failure mode.

Happy to help test this against a real workspace if useful — currently running Cyrus self-hosted via systemd with a single repository.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions