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

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(message, recent_tools: [])
  detect_from_message(message) || 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.

Parameters:

  • task_type (Symbol)
  • client (LLM::Client, nil) (defaults to: nil)

    active client (for provider checks)

Returns:

  • (Hash)

    { provider:, model: }



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