Module: Assistant::ComplexityEscalator
- Defined in:
- app/services/assistant/complexity_escalator.rb
Overview
"Start cheap, upgrade when the task proves complex."
Sunny always begins a session on the cheap default model. Once the session
reveals its complexity — the model declares a multi-step plan, or the
conversation has grown long — this picks the lowest-cost model still strong
enough for the work and climbs the capability ladder toward it.
Two invariants:
- It only ever CLIMBS the ladder, never descends — switching down mid-task
would throw away accumulated reasoning context. - It only climbs while the user's monthly budget allows. Out of budget it
returns nil and the caller stays on the cheap tier — the budget is a
governor (soft-degrade), never a hard block on an in-flight conversation.
The plan-step count is the primary complexity signal: a declared multi-step
plan is the model's own admission that the task is non-trivial. Conversation
length is a weaker secondary signal for long, unplanned grinds.
See doc/tasks/202606031730_SUNNY_BUDGET_AND_AUTO_ESCALATION.md.
Constant Summary collapse
- LADDER =
Capability ladder, cheapest → strongest. Keys match ChatService::MODELS.
No Sonnet rung — from Gemini Pro the next jump is straight to Opus, then
to Opus with the 1M-token context window for marathon / huge-context work. %w[gemini-flash gemini-pro claude-opus claude-opus-1m].freeze
- OPUS_1M_MIN_STEPS =
Plan-step thresholds for each rung above the cheap default.
6- OPUS_MIN_STEPS =
very large job — also wants the 1M context window
3- PRO_MIN_STEPS =
substantial multi-step job
1- LONG_CONVERSATION_MESSAGES =
Message counts above which an unplanned conversation is treated as complex
(long → Pro) or context-heavy enough to warrant the 1M window (very long). 20- VERY_LONG_CONVERSATION_MESSAGES =
60
Class Method Summary collapse
-
.floor_index(plan_step_count:, history_length:) ⇒ Integer
Lowest ladder index a session of this realized complexity should run on.
-
.upgrade(current_model:, plan_step_count:, history_length:, user_context: {}, budget: nil) ⇒ Hash?
Decide whether to upgrade the model for a session that has revealed its complexity.
Class Method Details
.floor_index(plan_step_count:, history_length:) ⇒ Integer
Lowest ladder index a session of this realized complexity should run on.
45 46 47 48 49 50 51 52 53 |
# File 'app/services/assistant/complexity_escalator.rb', line 45 def floor_index(plan_step_count:, history_length:) return 3 if plan_step_count >= OPUS_1M_MIN_STEPS || history_length > VERY_LONG_CONVERSATION_MESSAGES # claude-opus-1m return 2 if plan_step_count >= OPUS_MIN_STEPS # claude-opus if plan_step_count >= PRO_MIN_STEPS || history_length > LONG_CONVERSATION_MESSAGES return 1 # gemini-pro end 0 # gemini-flash end |
.upgrade(current_model:, plan_step_count:, history_length:, user_context: {}, budget: nil) ⇒ Hash?
Decide whether to upgrade the model for a session that has revealed its
complexity. Climbs LADDER to the complexity floor, gated on remaining
budget.
66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'app/services/assistant/complexity_escalator.rb', line 66 def upgrade(current_model:, plan_step_count:, history_length:, user_context: {}, budget: nil) current_index = LADDER.index(current_model) || 0 floor = floor_index(plan_step_count: plan_step_count, history_length: history_length) return nil if floor <= current_index budget ||= MonthlyBudget.for_user_context(user_context) return nil unless budget.allows_escalation? target = LADDER[floor] trigger = plan_step_count.positive? ? "#{plan_step_count}-step plan" : 'long session' { model: target, reason: "Auto-upgraded to #{target} — complex #{trigger}, " \ "$#{format('%.2f', budget.remaining_usd)} budget left" } end |