Spaces:
Running
Running
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 +2 -2
- lumen_router_config.py +9 -3
- tests/test_lumen_router_config.py +7 -1
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)
|
| 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
|
| 445 |
-
# Видео/аудио
|
| 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] == ("
|
|
|
|
| 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():
|