Record the Gemini quirk that silently drops extra system messages and merge them for that vendor only

This commit is contained in:
Claude (backend session) 2026-08-22 00:36:26 +03:00
parent 2b665fd5c4
commit b222ac8a08
2 changed files with 27 additions and 1 deletions

View file

@ -154,6 +154,7 @@
## Контент-фильтры / safety
- ⛔⛔ **Gemini, ДВА СИСТЕМНЫХ СООБЩЕНИЯ = ПРОМТ ТЕРЯЕТСЯ МОЛЧА (полигон 22.08, живая проба `eval/dovodka/proba_gemini.py`).** Слой OpenAI у Gemini принимает ОДНО системное сообщение; при двух и более лишние выбрасываются без ошибки. Замер на одном и том же системном тексте в 7970 знаков: ОДНИМ сообщением → `prompt_tokens` 2994 и маркерная инструкция ИСПОЛНЕНА; ДВУМЯ → **535 токенов (это размер одного user-сообщения) и инструкция НЕ исполнена**. Ответ приходит с `finish=stop` и выглядит правильным переводом — отказа нет, есть тихая потеря инструкции, невидимая всем гейтам годности. ⚠ Так были куплены 16 клеток арма `RE` (промт-роль на gemini): они переведены ВООБЩЕ БЕЗ ИНСТРУКЦИИ, и вердикт о переносимости из них был бы ложным. Лечение: склеивать системные в одно ДЛЯ ЭТОГО ВЕНДОРА (`rol.ONE_SYSTEM`/`fit_msgs`) — общая склейка сменила бы форму запроса у всех моделей и рассогласовала бы уже купленные клетки. ⚠ Проверено, что у `deepseek-v4-pro`, `gpt-5.6-luna`, `glm-5` и `grok-4.3` два системных доезжают целиком.
- **Gemini**: `safety_settings` через OpenAI-совместимый слой **не передаются** (HTTP 400 «Unknown name safety_settings»). Дефолт фильтров у 2.5/3 — OFF (gap-2). Для явного контроля фильтров (роль судьи 18+) нужен **нативный** Gemini API, не OpenAI-слой. ⚠ **D22.6: на ГРАФИЧНОЙ эротике Gemini 3.x fail-closed `PROHIBITED_CONTENT` (8× wire-verified, exp11) — фильтр НЕконфигурируем, нативный API не спасает; судья эротики — только Grok. На violence/SFW Gemini-судья жив.** `finish_reason` приходит СОСТАВНОЙ строкой `'content_filter: PROHIBITED_CONTENT'` (точные матчеры мертвы — D22.8а). Биллинг Gemini: thinking ТОЛЬКО в `total_tokens``reasoning: additive_total`, иначе недоучёт 146×/вызов (D22.3). ⚠ **В роли редактора Gemini течёт сервис-преамбулой в выход («Вот отредактированный…», 6/6 чанков — exp12) → за output-санитайзером D30.3.****Дополнение эксп-21 (08.08):** fail-closed ответ OpenAI-слоя приходит вовсе БЕЗ объекта `message` (не с пустым `content`) — парсер обязан переживать его отсутствие; на вебновелльном violence-материале срабатывания массовые (10 клеток судейского прогона с `content_filter: PROHIBITED_CONTENT`) ⇒ оговорка «на violence/SFW Gemini-судья жив» целиком НЕ держится — Gemini непригоден как ЕДИНСТВЕННЫЙ второй контур на этом материале (эксп-21 §1/§8).
- **Qwen Model Studio**: внешний фильтр `data_inspection_failed` на вход/выход, неотключаем (риск молчаливых вырезаний) — не тестировано (нет ключа).
- Детект отказов/вырезаний — см. `eval/refusal_bench.py` (паттерны soft-отказа + coverage-гейт по len_ratio/sent_cov, пороги в эксп. 02).

View file

@ -467,6 +467,31 @@ THINK_LEVEL: dict = {
}
# ⛔⛔ ВЕНДОР-КВИРК, ВСКРЫТ ЖИВОЙ ПРОБОЙ 22.08 (`proba_gemini.py`). Слой OpenAI у Gemini МОЛЧА
# ВЫБРАСЫВАЕТ системные сообщения, когда их больше одного. Один и тот же системный текст в 7970
# знаков: ОДНИМ сообщением → 2994 входных токена и инструкция исполнена; ДВУМЯ → **535 токенов и
# инструкция НЕ исполнена**. Ответ при этом приходит с `finish=stop` и выглядит правильным
# переводом — то есть отказа нет, есть тихая потеря промта.
# Наш арм шлёт ДВА системных (ядро роли + банк-закон), поэтому первые 16 клеток `RE` переведены
# ВООБЩЕ БЕЗ ИНСТРУКЦИИ. Вердикт «gemini не годится» из них был бы ложным — четвёртый случай того
# же класса за два дня (склейка · glm-5 · grok · этот).
# Лечение узкое и по вендору, а не общее: склеивать системные в ОДНО только там, где вендор иначе
# их теряет. Общая склейка сменила бы форму запроса у ВСЕХ моделей и рассогласовала бы уже
# купленные клетки с будущими.
ONE_SYSTEM = {"gemini-3.1-flash-lite", "gemini-3.6-flash", "gemini-3.1-pro-preview"}
def fit_msgs(model: str, msgs: list[dict]) -> list[dict]:
"""Приводит форму сообщений к тому, что вендор реально принимает (см. `ONE_SYSTEM`)."""
if model not in ONE_SYSTEM:
return msgs
sysm = [x["content"] for x in msgs if x["role"] == "system"]
if len(sysm) < 2:
return msgs
return ([{"role": "system", "content": "\n\n".join(sysm)}]
+ [x for x in msgs if x["role"] != "system"])
def call_kwargs(model: str, msgs: list[dict], arm: str = "") -> dict:
"""Тело вызова с ЯВНО объявленным уровнем размышления арма (см. `THINK_LEVEL`).
@ -474,7 +499,7 @@ def call_kwargs(model: str, msgs: list[dict], arm: str = "") -> dict:
и едет вендор-дефолт · `low`/`medium`/`high` уровень задаётся явно, потому что вендор-дефолт
у этой модели не тот, который нужен арму.
"""
kw = ROSTER.call_kwargs(model, msgs, max_out=MAX_OUT)
kw = ROSTER.call_kwargs(model, fit_msgs(model, msgs), max_out=MAX_OUT)
lvl = THINK_LEVEL.get(arm)
if lvl in (None, "off"):
return kw