Module: RubynCode::LLM::ModelRouter
- Defined in:
- lib/rubyn_code/llm/model_router.rb
Overview
Routes tasks to appropriate model tiers based on complexity. Integrates with the multi-provider adapter layer and reads per-provider model tier overrides from config.yml.
Users can configure tier models per provider in config.yml:
providers:
anthropic:
env_key: ANTHROPIC_API_KEY
models:
cheap: claude-haiku-4-5
mid: claude-sonnet-4-6
top: claude-opus-4-6
openai:
env_key: OPENAI_API_KEY
models:
cheap: gpt-5.4-nano
mid: gpt-5.4-mini
top: gpt-5.4
Constant Summary collapse
- TASK_TIERS =
rubocop:disable Metrics/ModuleLength – tier routing with provider integration
{ cheap: %i[ file_search spec_summary schema_lookup format_code git_operations memory_retrieval context_summary chatting explore ].freeze, mid: %i[ generate_specs simple_refactor code_review documentation bug_fix ].freeze, top: %i[ architecture complex_refactor security_review performance planning ].freeze }.freeze
- TIER_DEFAULTS =
Hardcoded fallbacks when no config override exists.
{ cheap: [ %w[anthropic claude-haiku-4-5], %w[openai gpt-5.4-nano] ].freeze, mid: [ %w[anthropic claude-sonnet-4-6], %w[openai gpt-5.4-mini] ].freeze, top: [ %w[anthropic claude-opus-4-6], %w[openai gpt-5.4] ].freeze }.freeze
- COST_MULTIPLIERS =
{ cheap: 0.07, mid: 0.20, top: 1.0 }.freeze
- DEFAULT_COST_MULTIPLIER =
0.20- MESSAGE_PATTERNS =
[ [/\b(architect|design|restructure|multi.?file)\b/, :architecture], [/\b(security|vulnerab|audit|owasp)\b/, :security_review], [/\b(n\+1|performance|slow|optimize|query)\b/, :performance], [/\b(spec|test|rspec)\b/, :generate_specs], [/\b(fix|bug|error|broken)\b/, :bug_fix], [/\b(refactor|extract|rename|move)\b/, :simple_refactor], [/\b(find|where|search|locate)\b/, :file_search], [/\b(doc|readme|comment|explain)\b/, :documentation] ].freeze
Class Method Summary collapse
-
.cost_multiplier(tier) ⇒ Object
Returns cost estimate multiplier for a tier relative to top tier.
-
.detect_task(message, recent_tools: []) ⇒ Object
Detect task type from a user message and recent tool calls.
-
.model_for(task_type, available_models: []) ⇒ Object
Returns just the model name for a task type (backward-compatible).
-
.resolve(task_type, client: nil) ⇒ Hash
Resolve the best [provider, model] pair for a task type.
-
.tier_for(task_type) ⇒ Object
Determine the appropriate model tier for a task.
Class Method Details
.cost_multiplier(tier) ⇒ Object
Returns cost estimate multiplier for a tier relative to top tier.
138 139 140 |
# File 'lib/rubyn_code/llm/model_router.rb', line 138 def cost_multiplier(tier) COST_MULTIPLIERS.fetch(tier, DEFAULT_COST_MULTIPLIER) end |
.detect_task(message, recent_tools: []) ⇒ Object
Detect task type from a user message and recent tool calls.
133 134 135 |
# File 'lib/rubyn_code/llm/model_router.rb', line 133 def detect_task(, recent_tools: []) () || detect_from_tools(recent_tools) || :chatting end |
.model_for(task_type, available_models: []) ⇒ Object
Returns just the model name for a task type (backward-compatible). – config + defaults search
119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/rubyn_code/llm/model_router.rb', line 119 def model_for(task_type, available_models: []) tier = tier_for(task_type) candidates = build_candidate_list(tier) if available_models.any? candidates.each do |pair| return pair[1] if available_models.any? { |m| m.start_with?(pair[1]) } end end candidates.first&.at(1) || TIER_DEFAULTS[tier].first[1] end |
.resolve(task_type, client: nil) ⇒ Hash
Resolve the best [provider, model] pair for a task type. Checks per-provider config overrides first, then falls back to TIER_DEFAULTS.
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/rubyn_code/llm/model_router.rb', line 87 def resolve(task_type, client: nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- multi-source fallback chain tier = tier_for(task_type) active = active_provider # 1. Config overrides — prefer the active provider's config configured = config_tier_models(tier) active_cfg = configured.find { |p, _| p == active } return pair_to_hash(active_cfg) if active_cfg # 2. Any other configured provider configured.each do |pair| return pair_to_hash(pair) if client.nil? || provider_available?(pair[0]) end # 3. Hardcoded defaults — prefer the active provider defaults = TIER_DEFAULTS[tier] active_default = defaults.find { |p, _| p == active } return pair_to_hash(active_default) if active_default # 4. Active provider not in defaults (e.g. minimax) — use their configured model for all tiers return { provider: active, model: active_model } if provider_available?(active) # 5. Any available default defaults.each do |pair| return pair_to_hash(pair) if client.nil? || provider_available?(pair[0]) end pair_to_hash(defaults.first) end |
.tier_for(task_type) ⇒ Object
Determine the appropriate model tier for a task.
73 74 75 76 77 78 |
# File 'lib/rubyn_code/llm/model_router.rb', line 73 def tier_for(task_type) TASK_TIERS.each do |tier, tasks| return tier if tasks.include?(task_type.to_sym) end :mid end |