From b222ac8a086394bd4a32a2fc996d96ed1d817c2f Mon Sep 17 00:00:00 2001 From: "Claude (backend session)" Date: Sat, 22 Aug 2026 00:36:26 +0300 Subject: [PATCH] Record the Gemini quirk that silently drops extra system messages and merge them for that vendor only --- docs/experiments/00-provider-quirks.md | 1 + eval/dovodka/rol.py | 27 +++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/experiments/00-provider-quirks.md b/docs/experiments/00-provider-quirks.md index 060e3a06..9bd8854c 100644 --- a/docs/experiments/00-provider-quirks.md +++ b/docs/experiments/00-provider-quirks.md @@ -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). diff --git a/eval/dovodka/rol.py b/eval/dovodka/rol.py index b1bfa4f5..e720f364 100644 --- a/eval/dovodka/rol.py +++ b/eval/dovodka/rol.py @@ -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