SilverElixir commited on
Commit
ea6e80e
·
1 Parent(s): 4583c1a

Route heavy text via Groq head; vision reserve for image freshness (D3: A2-1, A2-2)

Browse files
docs/ARCHITECTURE.md CHANGED
@@ -16,9 +16,9 @@ Routing is fully automatic: the router builds a fresh candidate list for every m
16
 
17
  - **A YouTube or website link** → Gemini only. It's the only provider that can read page content (`url_context`) or analyze a video by URL.
18
  - **A video or audio attachment** → video goes to Gemini only (OpenRouter's multimodal models only accept images as base64). Voice/audio is first transcribed cheaply (Groq Whisper) and the text joins the normal routing below; if transcription fails or is unavailable, the audio goes to Gemini as before.
19
- - **An image attachment, no live-info need** → free OpenRouter vision models first, Gemini as a reserve.
20
  - **Needs current information** (a lightweight keyword heuristic: "now," "today," "price," "who is currently...") → Gemini, prioritizing the models with a real search-grounding quota, then OpenRouter as a reserve, Groq last (knowledge-only answer if both are down).
21
- - **Plain text, no attachments, no freshness need** (the most common case) → Groq first (1000 free requests/day: Qwen, then gpt-oss), then OpenRouter. Heavier requests (code, multi-step reasoning, caught by another lightweight heuristic) skip Groq entirely and start at the stronger free OpenRouter models, Gemini as reserve.
22
 
23
  The reasoning: Gemini's free quota (roughly 20 requests per day for the flagship model) is the scarcest resource in the system, so it's only spent where a capability unique to Gemini is actually needed. Everything else, the bulk of ordinary messages, runs on Groq (1000/day) and OpenRouter's free tiers.
24
 
 
16
 
17
  - **A YouTube or website link** → Gemini only. It's the only provider that can read page content (`url_context`) or analyze a video by URL.
18
  - **A video or audio attachment** → video goes to Gemini only (OpenRouter's multimodal models only accept images as base64). Voice/audio is first transcribed cheaply (Groq Whisper) and the text joins the normal routing below; if transcription fails or is unavailable, the audio goes to Gemini as before.
19
+ - **An image attachment, no live-info need** → free OpenRouter vision models first, Gemini as a reserve. With a freshness need, Gemini search models go first and OpenRouter vision stays as a fallback.
20
  - **Needs current information** (a lightweight keyword heuristic: "now," "today," "price," "who is currently...") → Gemini, prioritizing the models with a real search-grounding quota, then OpenRouter as a reserve, Groq last (knowledge-only answer if both are down).
21
+ - **Plain text, no attachments, no freshness need** (the most common case) → Groq first (1000 free requests/day: Qwen, then gpt-oss), then OpenRouter. Heavier requests (code, multi-step reasoning, caught by another lightweight heuristic) also start at Groq to spare the scarcer OpenRouter/Gemini quotas, then go to the stronger free OpenRouter models, Gemini as reserve.
22
 
23
  The reasoning: Gemini's free quota (roughly 20 requests per day for the flagship model) is the scarcest resource in the system, so it's only spent where a capability unique to Gemini is actually needed. Everything else, the bulk of ordinary messages, runs on Groq (1000/day) and OpenRouter's free tiers.
24
 
lumen_router_config.py CHANGED
@@ -441,10 +441,14 @@ def _build_route(
441
  return _gemini_route(chain)
442
 
443
  if media_mime:
444
- if needs_freshness or is_video_or_audio_media:
445
- # Видео/аудио и медиа+поиск — только Gemini (OpenRouter не примет не-изображение и без поиска).
446
  chain = GEMINI_SEARCH_CHAIN if needs_freshness else GEMINI_HEAVY_CHAIN
447
  return _gemini_route(chain)
 
 
 
 
448
  # Картинка без поиска — сначала vision OpenRouter, Gemini резервом.
449
  return _or_route(_OR_VISION_ORDER) + _gemini_route(GEMINI_HEAVY_CHAIN)
450
 
@@ -453,6 +457,8 @@ def _build_route(
453
  return _gemini_route(GEMINI_SEARCH_CHAIN) + _or_route(_OR_HEAVY_ORDER if is_heavy else _OR_LIGHT_ORDER) + _groq_route(_GROQ_LIGHT_ORDER)
454
 
455
  # Обычный текст — сначала Groq (1000/день против 50 у OpenRouter), дальше OpenRouter, Gemini резервом.
 
 
456
  if is_heavy:
457
- return _or_route(_OR_HEAVY_ORDER) + _gemini_route(GEMINI_HEAVY_CHAIN)
458
  return _groq_route(_GROQ_LIGHT_ORDER) + _or_route(_OR_LIGHT_ORDER) + _gemini_route(GEMINI_SEARCH_CHAIN)
 
441
  return _gemini_route(chain)
442
 
443
  if media_mime:
444
+ if is_video_or_audio_media:
445
+ # Видео/аудио — только Gemini (OpenRouter не примет не-изображение).
446
  chain = GEMINI_SEARCH_CHAIN if needs_freshness else GEMINI_HEAVY_CHAIN
447
  return _gemini_route(chain)
448
+ if needs_freshness:
449
+ # Картинка+свежесть: Gemini с поиском головой, дальше vision
450
+ # OpenRouter без поиска (аудит D3, 30.09.2026: раньше резерва не было).
451
+ return _gemini_route(GEMINI_SEARCH_CHAIN) + _or_route(_OR_VISION_ORDER)
452
  # Картинка без поиска — сначала vision OpenRouter, Gemini резервом.
453
  return _or_route(_OR_VISION_ORDER) + _gemini_route(GEMINI_HEAVY_CHAIN)
454
 
 
457
  return _gemini_route(GEMINI_SEARCH_CHAIN) + _or_route(_OR_HEAVY_ORDER if is_heavy else _OR_LIGHT_ORDER) + _groq_route(_GROQ_LIGHT_ORDER)
458
 
459
  # Обычный текст — сначала Groq (1000/день против 50 у OpenRouter), дальше OpenRouter, Gemini резервом.
460
+ # Тяжёлый — тоже с Groq первой: свободная квота Groq-Tier экономит scarce-квоты
461
+ # OR/Gemini (аудит D3, 30.09.2026); не потянет — цепочка уйдёт дальше сама.
462
  if is_heavy:
463
+ return _groq_route(_GROQ_LIGHT_ORDER) + _or_route(_OR_HEAVY_ORDER) + _gemini_route(GEMINI_HEAVY_CHAIN)
464
  return _groq_route(_GROQ_LIGHT_ORDER) + _or_route(_OR_LIGHT_ORDER) + _gemini_route(GEMINI_SEARCH_CHAIN)
tests/test_lumen_router_config.py CHANGED
@@ -161,8 +161,11 @@ def test_build_route_plain_text_prefers_groq_to_save_quotas():
161
 
162
 
163
  def test_build_route_heavy_plain_text_uses_strong_openrouter_models_first():
 
 
164
  route = lumen_router_config._build_route(needs_youtube=False, needs_website=False, media_mime=None, is_heavy=True, needs_freshness=False)
165
- assert route[0] == ("openrouter", "nvidia/nemotron-3-ultra-550b-a55b:free")
 
166
 
167
 
168
  def test_build_route_image_without_freshness_prefers_openrouter_vision():
@@ -186,6 +189,9 @@ def test_build_route_image_with_freshness_forces_gemini_search_chain():
186
  # См. обновлённый GEMINI_SEARCH_CHAIN (24.07.2026) — реальная квота на search
187
  # grounding подтверждена только у Gemini 2.5, не у 3.x lite-моделей.
188
  assert route[0][1] == "gemini-2.5-flash"
 
 
 
189
 
190
 
191
  def test_or_route_excludes_uncensored_and_dead_models():
 
161
 
162
 
163
  def test_build_route_heavy_plain_text_uses_strong_openrouter_models_first():
164
+ # ОБНОВЛЕНО (аудит D3, 30.09.2026): тяжёлый текст идёт через Groq головой
165
+ # (свободные 1000/день экономят scarce-квоты OR/Gemini), дальше сильные OR.
166
  route = lumen_router_config._build_route(needs_youtube=False, needs_website=False, media_mime=None, is_heavy=True, needs_freshness=False)
167
+ assert route[0] == ("groq", "qwen/qwen3.8-27b")
168
+ assert route[2] == ("openrouter", "nvidia/nemotron-3-ultra-550b-a55b:free")
169
 
170
 
171
  def test_build_route_image_without_freshness_prefers_openrouter_vision():
 
189
  # См. обновлённый GEMINI_SEARCH_CHAIN (24.07.2026) — реальная квота на search
190
  # grounding подтверждена только у Gemini 2.5, не у 3.x lite-моделей.
191
  assert route[0][1] == "gemini-2.5-flash"
192
+ # ОБНОВЛЕНО (аудит D3, 30.09.2026): дальше vision OpenRouter резервом без
193
+ # поиска — раньше при исчерпании Gemini пользователь получал ошибку квоты.
194
+ assert any(p == "openrouter" for p, _ in route)
195
 
196
 
197
  def test_or_route_excludes_uncensored_and_dead_models():