Spaces:
Running
Running
SilverElixir commited on
Add files via upload
Browse files- bot.py +1 -0
- lumen_formatting.py +42 -0
- lumen_router_config.py +38 -6
- test_lumen_formatting.py +53 -0
- test_lumen_router_config.py +13 -0
bot.py
CHANGED
|
@@ -4643,6 +4643,7 @@ async def _webhook_startup() -> None:
|
|
| 4643 |
|
| 4644 |
commands = [
|
| 4645 |
BotCommand(command="start", description="О боте и список команд"),
|
|
|
|
| 4646 |
BotCommand(command="draw", description="Нарисовать изображение по описанию"),
|
| 4647 |
BotCommand(command="tts", description="Озвучить текст"),
|
| 4648 |
BotCommand(command="imgmodel", description="Выбрать модель для генерации изображений"),
|
|
|
|
| 4643 |
|
| 4644 |
commands = [
|
| 4645 |
BotCommand(command="start", description="О боте и список команд"),
|
| 4646 |
+
BotCommand(command="reset", description="Очистить историю диалога"),
|
| 4647 |
BotCommand(command="draw", description="Нарисовать изображение по описанию"),
|
| 4648 |
BotCommand(command="tts", description="Озвучить текст"),
|
| 4649 |
BotCommand(command="imgmodel", description="Выбрать модель для генерации изображений"),
|
lumen_formatting.py
CHANGED
|
@@ -159,6 +159,38 @@ _BULLET_MARKER_RE = re.compile(r"^([ \t]*)[-*][ \t]+", re.MULTILINE)
|
|
| 159 |
def _normalize_bullet_markers(text: str) -> str:
|
| 160 |
return _BULLET_MARKER_RE.sub(lambda m: m.group(1) + "• ", text)
|
| 161 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
# ── Markdown-цитаты "> текст" → Telegram <blockquote> ───────────────────────
|
| 163 |
# Тот же принцип, что и у _normalize_bullet_markers выше: "> " — обычный
|
| 164 |
# GFM-синтаксис цитаты, модели он известен без единого слова в system_prompt.py
|
|
@@ -229,6 +261,10 @@ def _md_to_html(text: str) -> str:
|
|
| 229 |
1.4. Маркеры списков "-"/"* " → "•" (_normalize_bullet_markers) — ДО таблиц,
|
| 230 |
чтобы строка-разделитель таблицы ("|---|---|") успела обработаться первой
|
| 231 |
и не была принята за маркер списка.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
1.45. Markdown-цитаты "> " → сентинелы \x00BQS\x00/\x00BQE\x00 (_convert_
|
| 233 |
blockquotes) — сентинелы, не сразу <blockquote>, т.к. Phase 2 экранировал
|
| 234 |
бы буквальный тег; настоящий тег подставляется после Phase 3 (см. ниже).
|
|
@@ -306,6 +342,12 @@ def _md_to_html(text: str) -> str:
|
|
| 306 |
# чтобы строка-разделитель таблицы ("|---|---|") успела обработаться первой.
|
| 307 |
text = _normalize_bullet_markers(text)
|
| 308 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
# ── Phase 1.45: markdown-цитаты "> " → сентинелы blockquote (см.
|
| 310 |
# _convert_blockquotes выше) — ДО HTML-экранирования, т.к. решение "это
|
| 311 |
# строка цитаты" принимается по буквальному "> " в начале строки; сама
|
|
|
|
| 159 |
def _normalize_bullet_markers(text: str) -> str:
|
| 160 |
return _BULLET_MARKER_RE.sub(lambda m: m.group(1) + "• ", text)
|
| 161 |
|
| 162 |
+
# ── Markdown-заголовки "#"/"##"/"###" → **жирный текст** ────────────────────
|
| 163 |
+
# РЕГРЕССИЯ, найденная при живом тестировании (18 августа 2026, реальные скриншоты
|
| 164 |
+
# из Telegram): system_prompt.py прямо запрещает markdown-заголовки ("Также никогда
|
| 165 |
+
# не используй markdown-заголовки (#, ##, ### и т.п.)... Для выделения структуры
|
| 166 |
+
# используй жирный текст (**...**) вместо заголовков"), но модели (особенно
|
| 167 |
+
# бесплатные модели OpenRouter) регулярно их всё равно пишут — то же самое
|
| 168 |
+
# несоблюдение инструкции моделью, которое уже потребовало защитных сеток для
|
| 169 |
+
# таблиц (_convert_markdown_tables_to_lists) и маркеров списков
|
| 170 |
+
# (_normalize_bullet_markers) выше. Без скрубера здесь "### Как она выводится?"
|
| 171 |
+
# уходит в Telegram буквально с "###" в начале строки — реальный найденный баг,
|
| 172 |
+
# видимый на скриншотах. ATX-заголовок распознаётся по стандартному правилу
|
| 173 |
+
# CommonMark: 1-6 "#" в начале строки, затем обязательно пробел/таб — это
|
| 174 |
+
# исключает "C#"/"#tag" и подобные легитимные "#" не в начале строки (те тут
|
| 175 |
+
# не попадают под шаблон вообще, т.к. якорь "^" требует начала строки).
|
| 176 |
+
_HEADER_MARKER_RE = re.compile(r"^[ \t]*#{1,6}[ \t]+(.*)$", re.MULTILINE)
|
| 177 |
+
|
| 178 |
+
def _normalize_headers(text: str) -> str:
|
| 179 |
+
def _repl(m: re.Match) -> str:
|
| 180 |
+
content = m.group(1).rstrip()
|
| 181 |
+
if not content:
|
| 182 |
+
# Голая строка из одних "#" без текста — нечего выделять жирным,
|
| 183 |
+
# просто убираем маркер целиком, а не оставляем пустую "****".
|
| 184 |
+
return ""
|
| 185 |
+
if content.startswith("**") and content.endswith("**") and len(content) > 4:
|
| 186 |
+
# Модель сама уже обернула текст заголовка в **bold** (нередкий
|
| 187 |
+
# случай — "### **Важно**") — оборачивать ЕЩЁ раз дало бы "****Важно****"
|
| 188 |
+
# и сломало бы парность звёздочек в Phase 3 ниже. Раз обёртка уже
|
| 189 |
+
# есть, только снимаем сам маркер "#", остальное не трогаем.
|
| 190 |
+
return content
|
| 191 |
+
return f"**{content}**"
|
| 192 |
+
return _HEADER_MARKER_RE.sub(_repl, text)
|
| 193 |
+
|
| 194 |
# ── Markdown-цитаты "> текст" → Telegram <blockquote> ───────────────────────
|
| 195 |
# Тот же принцип, что и у _normalize_bullet_markers выше: "> " — обычный
|
| 196 |
# GFM-синтаксис цитаты, модели он известен без единого слова в system_prompt.py
|
|
|
|
| 261 |
1.4. Маркеры списков "-"/"* " → "•" (_normalize_bullet_markers) — ДО таблиц,
|
| 262 |
чтобы строка-разделитель таблицы ("|---|---|") успела обработаться первой
|
| 263 |
и не была принята за маркер списка.
|
| 264 |
+
1.41. Markdown-заголовки "#"/"##"/"###" → **жирный текст** (_normalize_headers) —
|
| 265 |
+
после списков (не пересекаются по синтаксису), до Phase 3, чтобы
|
| 266 |
+
получившиеся "**...**" были обработаны обычным bold-регэкспом ниже
|
| 267 |
+
на общих основаниях, а не отдельной веткой.
|
| 268 |
1.45. Markdown-цитаты "> " → сентинелы \x00BQS\x00/\x00BQE\x00 (_convert_
|
| 269 |
blockquotes) — сентинелы, не сразу <blockquote>, т.к. Phase 2 экранировал
|
| 270 |
бы буквальный тег; настоящий тег подставляется после Phase 3 (см. ниже).
|
|
|
|
| 342 |
# чтобы строка-разделитель таблицы ("|---|---|") успела обработаться первой.
|
| 343 |
text = _normalize_bullet_markers(text)
|
| 344 |
|
| 345 |
+
# ── Phase 1.41: markdown-заголовки "#"/"##"/"###" → **жирный текст** (см.
|
| 346 |
+
# _normalize_headers выше) — после списков, до HTML-экранирования и до Phase 3
|
| 347 |
+
# (получившийся "**...**" обрабатывается обычным bold-регэкспом на общих
|
| 348 |
+
# основаниях, отдельная ветка не нужна).
|
| 349 |
+
text = _normalize_headers(text)
|
| 350 |
+
|
| 351 |
# ── Phase 1.45: markdown-цитаты "> " → сентинелы blockquote (см.
|
| 352 |
# _convert_blockquotes выше) — ДО HTML-экранирования, т.к. решение "это
|
| 353 |
# строка цитаты" принимается по буквальному "> " в начале строки; сама
|
lumen_router_config.py
CHANGED
|
@@ -333,6 +333,30 @@ _OR_MODEL_HEALTH: dict[str, _ModelHealthNote] = {
|
|
| 333 |
"уверенность и в оставшихся двух (вторая — dolphin-mistral, уже была исключена по "
|
| 334 |
"другой причине выше)."
|
| 335 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
}
|
| 337 |
|
| 338 |
# Вычисляется ИЗ _OR_MODEL_HEALTH выше — единственное место, где решается, какие
|
|
@@ -387,11 +411,13 @@ def _gemini_route(models: list[str]) -> list[tuple[str, str]]:
|
|
| 387 |
# мелкие модели ниже в цепочке дадут похожие инциденты, тогда стоит
|
| 388 |
# рассмотреть полное исключение, а не просто понижение приоритета.
|
| 389 |
#
|
| 390 |
-
# Модели, убранные из списка целиком (провайдер снял с бесплатного тира
|
| 391 |
-
#
|
| 392 |
-
# _OR_MODEL_HEALTH выше): meta-llama/llama-3.3-70b-instruct,
|
| 393 |
-
# 80b-a3b-instruct, z-ai/glm-4.5-air, meta-llama/llama-3.2-3b-
|
| 394 |
-
# liquid/lfm-2.5-1.2b-instruct
|
|
|
|
|
|
|
| 395 |
#
|
| 396 |
# nemotron-3.5-lightning ДОБАВЛЕНА (аудит моделей, 17 августа 2026, по
|
| 397 |
# актуальному "Top Weekly free" каталогу OpenRouter): 30B MoE (3B активных),
|
|
@@ -400,8 +426,14 @@ def _gemini_route(models: list[str]) -> list[tuple[str, str]]:
|
|
| 400 |
# Вышла всего неделю назад (11 августа 2026) — калибровочного сравнения с
|
| 401 |
# Claude Sonnet ещё не было ни разу, поэтому поставлена последней перед
|
| 402 |
# generic-резервом, а не выше уже проверенных калибровкой моделей.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
_OR_LIGHT_ORDER: list[str] = [
|
| 404 |
-
"nvidia/nemotron-nano-9b-v2:free",
|
| 405 |
"inclusionai/ling-3.0-flash:free",
|
| 406 |
"nvidia/nemotron-3-nano-30b-a3b:free",
|
| 407 |
"openai/gpt-oss-20b:free",
|
|
|
|
| 333 |
"уверенность и в оставшихся двух (вторая — dolphin-mistral, уже была исключена по "
|
| 334 |
"другой причине выше)."
|
| 335 |
),
|
| 336 |
+
# ── Найдено при калибровочном прогоне против Claude Sonnet 5 (18 августа 2026,
|
| 337 |
+
# реальные продакшен-логи + скриншоты Telegram) ──
|
| 338 |
+
"nvidia/nemotron-nano-9b-v2:free": _ModelHealthNote(
|
| 339 |
+
reason="ПОДТВЕРЖДЕНО ПО РЕАЛЬНЫМ ЛОГАМ ПРОДА И СКРИНШОТАМ (18 августа 2026): стояла первой в "
|
| 340 |
+
"_OR_LIGHT_ORDER и реально ответила на 37 из 45 (82%) успешных лёгких текстовых запросов "
|
| 341 |
+
"за сессию — но КАЖДЫЙ развёрнутый ответ (2+ предложения) содержал грубую порчу текста: "
|
| 342 |
+
"случайные фрагменты слов из десятка не относящихся к разговору языков, вклиненные "
|
| 343 |
+
"прямо ВНУТРЬ русских слов и предложений (подтверждённые примеры из одной сессии: "
|
| 344 |
+
"испанское 'modelo' вместо 'модель', португальское 'mesmo'/'ambiental', французское "
|
| 345 |
+
"'sommet'/'fermer'/'pente'/'Série', арабское 'زراعة' и 'دان', корейское '택' и '종료', "
|
| 346 |
+
"китайское '欧洲' и '諜', японская катакана 'キュ', деванагари 'कंपनी', вьетнамское "
|
| 347 |
+
"'tiền', итальянское 'militarizzazione', полностью вымышленное имя сооснователя "
|
| 348 |
+
"Anthropic 'lagoo Isaacs' вместо реальных Дарио/Дэниэлы Амодей, и медицинский термин "
|
| 349 |
+
"'carcinogenesis', случайно подставленный вместо слова 'убийство' в разборе причин "
|
| 350 |
+
"Первой мировой войны). Модель НЕ мертва технически (не 404, отвечает и тратит "
|
| 351 |
+
"заметную часть бесплатного дневного лимита), но выдаёт продакшен-трафику постоянно "
|
| 352 |
+
"испорченный на нечитаемость текст — тот самый класс проблемы, ради которого этот "
|
| 353 |
+
"реестр уже исключает uncensored/некачественные модели (см. dolphin-mistral выше), а "
|
| 354 |
+
"не только официально снятые с тарифа. Дополнительно (не решающий, но подтверждающий "
|
| 355 |
+
"аргумент): вопреки размеру 'nano'/9B и позиции головы 'лёгкой' цепочки, реальные "
|
| 356 |
+
"длительности стриминга по логам — от 12 до 92 секунд на ответ, то есть заметно "
|
| 357 |
+
"медленнее, чем у более крупных моделей дальше по списку — таблоид-эффект 'самая "
|
| 358 |
+
"быстрая и лёгкая' на практике не подтвердился ни разу за сессию."
|
| 359 |
+
),
|
| 360 |
}
|
| 361 |
|
| 362 |
# Вычисляется ИЗ _OR_MODEL_HEALTH выше — единственное место, где решается, какие
|
|
|
|
| 411 |
# мелкие модели ниже в цепочке дадут похожие инциденты, тогда стоит
|
| 412 |
# рассмотреть полное исключение, а не просто понижение приоритета.
|
| 413 |
#
|
| 414 |
+
# Модели, убранные из списка целиком (провайдер снял с бесплатного тира, слаг
|
| 415 |
+
# подтверждённо не обслуживается, или подтверждена грубая порча текста — полные
|
| 416 |
+
# причины и даты см. в _OR_MODEL_HEALTH выше): meta-llama/llama-3.3-70b-instruct,
|
| 417 |
+
# qwen/qwen3-next-80b-a3b-instruct, z-ai/glm-4.5-air, meta-llama/llama-3.2-3b-
|
| 418 |
+
# instruct, liquid/lfm-2.5-1.2b-instruct, nvidia/nemotron-nano-9b-v2 (стояла
|
| 419 |
+
# первой здесь — см. подробный разбор порчи текста на реальном трафике в записи
|
| 420 |
+
# реестра выше, калибровка 18 августа 2026).
|
| 421 |
#
|
| 422 |
# nemotron-3.5-lightning ДОБАВЛЕНА (аудит моделей, 17 августа 2026, по
|
| 423 |
# актуальному "Top Weekly free" каталогу OpenRouter): 30B MoE (3B активных),
|
|
|
|
| 426 |
# Вышла всего неделю назад (11 августа 2026) — калибровочного сравнения с
|
| 427 |
# Claude Sonnet ещё не было ни разу, поэтому поставлена последней перед
|
| 428 |
# generic-резервом, а не выше уже проверенных калибровкой моделей.
|
| 429 |
+
#
|
| 430 |
+
# ПОСЛЕ ИСКЛЮЧЕНИЯ nemotron-nano-9b-v2 (см. выше) ling-3.0-flash становится
|
| 431 |
+
# НОВОЙ головой списка — ни разу не поймана в порче текста в той же сессии
|
| 432 |
+
# логов, но и калибровочного сравнения с Claude Sonnet у неё тоже пока не было
|
| 433 |
+
# (см. её же комментарий в докстринге секции ниже про 1.49T токенов/неделю).
|
| 434 |
+
# Стоит присмотреться к первым реальным ответам именно в этой новой роли головы
|
| 435 |
+
# списка — если порча обнаружится и там, кандидат на аналогичное исключение.
|
| 436 |
_OR_LIGHT_ORDER: list[str] = [
|
|
|
|
| 437 |
"inclusionai/ling-3.0-flash:free",
|
| 438 |
"nvidia/nemotron-3-nano-30b-a3b:free",
|
| 439 |
"openai/gpt-oss-20b:free",
|
test_lumen_formatting.py
CHANGED
|
@@ -256,6 +256,59 @@ def test_md_to_html_full_pipeline_converts_bullet_list_with_bold():
|
|
| 256 |
assert "*" not in result.replace("</b>", "").replace("<b>", "")
|
| 257 |
|
| 258 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
# ─────────────────── spoiler-тег: защитная сетка (тот же принцип, что <u>) ───────────────────
|
| 260 |
|
| 261 |
def test_md_to_html_strips_literal_spoiler_tag():
|
|
|
|
| 256 |
assert "*" not in result.replace("</b>", "").replace("<b>", "")
|
| 257 |
|
| 258 |
|
| 259 |
+
# ─────────────── markdown-заголовки "#"/"##"/"###" → **жирный текст** ───────────────
|
| 260 |
+
# РЕГРЕССИЯ, найденная на реальных скриншотах Telegram (18 августа 2026):
|
| 261 |
+
# system_prompt.py запрещает markdown-заголовки, но модели (особенно бесплатные
|
| 262 |
+
# модели OpenRouter) регулярно их всё равно пишут — раньше "###" уходило в
|
| 263 |
+
# Telegram буквально, без единой защитной сетки (в отличие от таблиц/списков).
|
| 264 |
+
|
| 265 |
+
def test_normalize_headers_converts_h3_to_bold():
|
| 266 |
+
assert lumen_formatting._normalize_headers("### Как она выводится?") == "**Как она выводится?**"
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
def test_normalize_headers_converts_h1_and_h2():
|
| 270 |
+
assert lumen_formatting._normalize_headers("# Заголовок") == "**Заголовок**"
|
| 271 |
+
assert lumen_formatting._normalize_headers("## Подзаголовок") == "**Подзаголовок**"
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def test_normalize_headers_does_not_touch_hash_mid_line():
|
| 275 |
+
# "C#" / "#tag" не в начале строки — ATX-заголовок ТОЛЬКО в начале строки.
|
| 276 |
+
text = "Язык C# отличается от C++.\nПодробнее: #tag"
|
| 277 |
+
assert lumen_formatting._normalize_headers(text) == text
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
def test_normalize_headers_requires_space_after_hashes():
|
| 281 |
+
# "#без_пробела" — не заголовок по правилам CommonMark ATX, не трогаем.
|
| 282 |
+
assert lumen_formatting._normalize_headers("#без_пробела текст") == "#без_пробела текст"
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
def test_normalize_headers_bare_hashes_with_no_text_removed():
|
| 286 |
+
assert lumen_formatting._normalize_headers("### \nследующая строка") == "\nследующая строка"
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
def test_normalize_headers_does_not_double_wrap_already_bold_content():
|
| 290 |
+
# "### **Важно**" — модель сама уже обернула текст в bold; повторная обёртка
|
| 291 |
+
# дала бы "****Важно****" и сломала бы парность "**" в Phase 3.
|
| 292 |
+
assert lumen_formatting._normalize_headers("### **Важно**") == "**Важно**"
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
def test_md_to_html_full_pipeline_strips_stray_header_markers():
|
| 296 |
+
# Регрессия на реальный найденный баг: пользователь видел буквальные "###" в
|
| 297 |
+
# сообщении бота вместо жирного текста — полный конвейер должен это исправлять.
|
| 298 |
+
result = lumen_formatting._md_to_html("### Как она выводится?\nобычный текст")
|
| 299 |
+
assert "###" not in result
|
| 300 |
+
assert result.startswith("<b>Как она выводится?</b>")
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
def test_md_to_html_header_hash_inside_code_block_untouched():
|
| 304 |
+
# "#" внутри блока кода (например, комментарий Python) не должен считаться
|
| 305 |
+
# заголовком — код уже вынесен плейсхолдером до этой фазы.
|
| 306 |
+
text = "```python\n# обычный комментарий\nprint(1)\n```"
|
| 307 |
+
result = lumen_formatting._md_to_html(text)
|
| 308 |
+
assert "# обычный комментарий" in result
|
| 309 |
+
assert "<b>" not in result
|
| 310 |
+
|
| 311 |
+
|
| 312 |
# ─────────────────── spoiler-тег: защитная сетка (тот же принцип, что <u>) ───────────────────
|
| 313 |
|
| 314 |
def test_md_to_html_strips_literal_spoiler_tag():
|
test_lumen_router_config.py
CHANGED
|
@@ -232,6 +232,19 @@ def test_or_light_order_no_longer_starts_with_dead_or_worst_offender_models():
|
|
| 232 |
assert lumen_router_config._OR_LIGHT_ORDER[0] not in {"openai/gpt-oss-20b:free", "nvidia/nemotron-3-nano-30b-a3b:free"}
|
| 233 |
|
| 234 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
# ─────────────────── единый реестр "нездоровых" моделей OpenRouter (аудит техдолга) ───────────────────
|
| 236 |
# Раньше "эта модель сейчас плохая" отслеживалось тремя независимыми механизмами
|
| 237 |
# (_TEMPORARY_FREE_MODELS/_ROUTER_EXCLUDED_OR_MODELS/точечные вычёркивания из
|
|
|
|
| 232 |
assert lumen_router_config._OR_LIGHT_ORDER[0] not in {"openai/gpt-oss-20b:free", "nvidia/nemotron-3-nano-30b-a3b:free"}
|
| 233 |
|
| 234 |
|
| 235 |
+
def test_nemotron_nano_9b_v2_excluded_after_calibration_run():
|
| 236 |
+
# РЕГРЕССИЯ (калибровочный прогон против Claude Sonnet 5, 18 августа 2026):
|
| 237 |
+
# раньше стояла ПЕРВОЙ в _OR_LIGHT_ORDER и реально отвечала на 82% лёгких
|
| 238 |
+
# текстовых запросов сессии — каждый развёрнутый ответ содержал грубую порчу
|
| 239 |
+
# текста (случайные фрагменты слов на десятке не относящихся к разговору
|
| 240 |
+
# языков внутри русских предложений, см. полный разбор в _OR_MODEL_HEALTH).
|
| 241 |
+
assert "nvidia/nemotron-nano-9b-v2:free" in lumen_router_config._ROUTER_EXCLUDED_OR_MODELS
|
| 242 |
+
assert "nvidia/nemotron-nano-9b-v2:free" not in lumen_router_config._OR_LIGHT_ORDER
|
| 243 |
+
assert lumen_router_config._OR_MODEL_HEALTH["nvidia/nemotron-nano-9b-v2:free"].reason
|
| 244 |
+
route = lumen_router_config._or_route(["nvidia/nemotron-nano-9b-v2:free", "inclusionai/ling-3.0-flash:free"])
|
| 245 |
+
assert [m for _, m in route] == ["inclusionai/ling-3.0-flash:free"]
|
| 246 |
+
|
| 247 |
+
|
| 248 |
# ─────────────────── единый реестр "нездоровых" моделей OpenRouter (аудит техдолга) ───────────────────
|
| 249 |
# Раньше "эта модель сейчас плохая" отслеживалось тремя независимыми механизмами
|
| 250 |
# (_TEMPORARY_FREE_MODELS/_ROUTER_EXCLUDED_OR_MODELS/точечные вычёркивания из
|