4040 "fallback" ,
4141)
4242
43+ TRIVIAL_KEYWORDS = (
44+ "lint" ,
45+ "test retry" ,
46+ "file search" ,
47+ "summary" ,
48+ "summaries" ,
49+ "classification" ,
50+ "simple refactor" ,
51+ )
52+
53+ ESCALATION_KEYWORDS = (
54+ "architecture" ,
55+ "security risk" ,
56+ "stuck" ,
57+ "escalate" ,
58+ )
59+
4360
4461@dataclass (frozen = True )
4562class RoutingPlan :
@@ -104,6 +121,7 @@ def build_routing_plan(
104121 requested_model = requested_model ,
105122 default_provider = primary_provider ,
106123 default_model = primary_model ,
124+ messages = messages ,
107125 )
108126 return RoutingPlan (
109127 mode = "auto" if lane == "auto" and routing_mode == "auto" else "fixed" ,
@@ -129,8 +147,9 @@ def _build_lane_chain(
129147 requested_model : str | None ,
130148 default_provider : str ,
131149 default_model : str ,
150+ messages : Iterable [Message ] = (),
132151) -> tuple [ChainStep , tuple [ChainStep , ...]]:
133- lane_primary , lane_fallbacks = _lane_default_chain (settings , lane = lane , tier = tier )
152+ lane_primary , lane_fallbacks = _lane_default_chain (settings , lane = lane , tier = tier , messages = messages )
134153 primary = ChainStep (
135154 requested_provider or lane_primary .provider ,
136155 requested_model or lane_primary .model ,
@@ -155,39 +174,36 @@ def _build_lane_chain(
155174 return primary , tuple (fallback_chain )
156175
157176
158- def _lane_default_chain (settings : Settings , * , lane : RouteLane , tier : str ) -> tuple [ChainStep , tuple [ChainStep , ...]]:
159- normalized_tier = tier if lane == "auto" else _tier_for_lane (lane )[0 ]
160- if settings .ollama_base_url :
161- ollama_primary = ChainStep ("ollama" , settings .ollama_model )
162- gemini_fallbacks = _gemini_fallbacks_for_tier (settings , normalized_tier )
163- return ollama_primary , gemini_fallbacks
164- if normalized_tier == "simple" :
165- primary = ChainStep ("gemini" , "gemini-2.5-flash-lite" )
166- fallbacks = [
167- ChainStep ("gemini" , "gemini-2.5-flash" ),
168- ChainStep ("gemini" , "gemini-2.5-pro" ),
169- ChainStep ("claude-cli" , settings .claude_cli_model or "haiku" ),
170- ChainStep ("codex-cli" , settings .codex_cli_model or "gpt-5-mini" ),
171- ChainStep ("gemini-cli" , settings .gemini_cli_model or "gemini-2.5-flash" ),
172- ]
173- elif normalized_tier == "deep" :
174- primary = ChainStep ("gemini" , "gemini-2.5-pro" )
175- fallbacks = [
176- ChainStep ("gemini" , "gemini-2.5-flash" ),
177- ChainStep ("gemini" , "gemini-2.5-flash-lite" ),
178- ChainStep ("claude-cli" , settings .claude_cli_model or "sonnet" ),
179- ChainStep ("codex-cli" , settings .codex_cli_model or "gpt-5-codex" ),
180- ChainStep ("gemini-cli" , settings .gemini_cli_model or "gemini-2.5-pro" ),
181- ]
182- else :
183- primary = ChainStep ("gemini" , "gemini-2.5-flash" )
184- fallbacks = [
185- ChainStep ("gemini" , "gemini-2.5-pro" ),
186- ChainStep ("gemini" , "gemini-2.5-flash-lite" ),
187- ChainStep ("claude-cli" , settings .claude_cli_model or "sonnet" ),
188- ChainStep ("codex-cli" , settings .codex_cli_model or "gpt-5" ),
189- ChainStep ("gemini-cli" , settings .gemini_cli_model or "gemini-2.5-flash" ),
190- ]
177+ def _is_escalation_allowed (messages : Iterable [Message ]) -> bool :
178+ full_text = "\n " .join (_extract_last_user_text ([m ]) for m in messages ).lower ()
179+
180+ if any (keyword in full_text for keyword in TRIVIAL_KEYWORDS ):
181+ return False
182+
183+ error_count = full_text .count ("build failed" ) + full_text .count ("test failed" )
184+ if error_count >= 3 :
185+ return True
186+
187+ if any (keyword in full_text for keyword in ESCALATION_KEYWORDS ):
188+ return True
189+
190+ return False
191+
192+ def _lane_default_chain (settings : Settings , * , lane : RouteLane , tier : str , messages : Iterable [Message ] = ()) -> tuple [ChainStep , tuple [ChainStep , ...]]:
193+ primary = ChainStep ("openai" , settings .requested_model or "gemini-free" )
194+ fallbacks = [
195+ ChainStep ("openai" , "groq-fast-free" ),
196+ ChainStep ("openai" , "openrouter/free" ),
197+ ChainStep ("openai" , "github-models" ),
198+ ChainStep ("openai" , "cerebras-free" ),
199+ ChainStep ("openai" , "mistral-free" ),
200+ ChainStep ("openai" , "huggingface-free" ),
201+ ChainStep ("openai" , "qwen2.5-coder:7b" ),
202+ ]
203+
204+ if _is_escalation_allowed (messages ):
205+ fallbacks .append (ChainStep ("openai" , "claude-paid-escalation" ))
206+
191207 return primary , tuple (fallbacks )
192208
193209
0 commit comments