From 56670e3d6722bdfe0c698634c5dfc12eb237f06a Mon Sep 17 00:00:00 2001 From: "Claude (backend session)" Date: Sat, 4 Jul 2026 15:49:27 +0300 Subject: [PATCH] Scope CheckKeys to reachable models so backend runs on two keys; escalation keys required only when gates enabled --- backend/.env.example | 23 ++++++++++++--- backend/internal/config/config_test.go | 20 +++++++++---- backend/internal/config/models.go | 27 ++++++++++++------ backend/internal/config/pipeline.go | 6 ++++ docs/PROGRESS.md | 39 +++++++++++++++++++++++++- 5 files changed, 96 insertions(+), 19 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index a687603..068b1d7 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,10 +1,25 @@ # Скопируй в backend/.env (gitignored) и заполни. tmctl подхватывает .env из # каталога book.yaml и из CWD; уже установленные переменные окружения не # перетираются. -DEEPSEEK_API_KEY= -ZAI_API_KEY= -KIMI_API_KEY= -ANTHROPIC_API_KEY= +# +# Два набора моделей в проекте служат РАЗНЫМ задачам: +# backend/.env — продакшн-стек Р4, ФАЗОВО (нужно только то, что реально +# зовётся в текущей фазе); +# eval/.env — бенчмарк-сеть сессии «Полигон» (широкий набор провайдеров +# для замеров отказов/токенов), НЕ обязан совпадать с продакшн-стеком. + +# --- НУЖНЫ СЕЙЧАС (Фаза 0: пайплайн C1 draft→edit) --- +DEEPSEEK_API_KEY= # черновик — DeepSeek V4 Flash (лучший zh-токенизатор, дешевле всех) +ZAI_API_KEY= # редактура — GLM-5 (ru-стиль), международный контур z.ai + +# --- ОПЦИОНАЛЬНО, Фаза 1+ (эскалация; зовутся только при включённых гейтах) --- +# KIMI_API_KEY= # альтернативный редактор / эскалация канала adult (Р4) +# ANTHROPIC_API_KEY= # эскалация/судья SFW, ТОЛЬКО прямой контур; в RU-контуре + # экономически недоступен (Р5). Не нужен для Фазы 0. + # Держи пустым, пока eval не подтвердит SFW-поведение + # Sonnet-5 (эскалация «стерильного» канала — открытый + # вопрос к полигону, см. PROGRESS). # LOG_LEVEL=debug # LOG_FORMAT=json +# LOG_LLM_BODIES=1 # логировать сырые тела LLM-обменов (только с LOG_LEVEL=debug) diff --git a/backend/internal/config/config_test.go b/backend/internal/config/config_test.go index d5c4019..cf6809d 100644 --- a/backend/internal/config/config_test.go +++ b/backend/internal/config/config_test.go @@ -59,13 +59,23 @@ func TestCheckKeysOnlyUsedNonLocalModels(t *testing.T) { if err := m.CheckKeys(cloudPipe); err != nil { t.Fatalf("present key must pass: %v", err) } - // Escalation-цепочки тоже учитываются. - escPipe := &Pipeline{ + // Эскалационные ключи НЕ требуются, пока гейты выключены (эскалация + // недостижима — Фаза 0): local-стадия + облачная цепочка при gates off. + t.Setenv("TEST_CLOUD_KEY", "") + escOffPipe := &Pipeline{ Stages: []Stage{{Model: "local-model"}}, Escal: Escalation{Chains: map[string][]string{"default": {"cloud-model"}}}, } - t.Setenv("TEST_CLOUD_KEY", "") - if err := m.CheckKeys(escPipe); err == nil { - t.Fatal("escalation chain model without a key must fail-fast") + if err := m.CheckKeys(escOffPipe); err != nil { + t.Fatalf("escalation keys must not be required while gates are off: %v", err) + } + // С включённым гейтом эскалация достижима → ключ цепочки обязателен. + escOnPipe := &Pipeline{ + Stages: []Stage{{Model: "local-model"}}, + Escal: Escalation{Chains: map[string][]string{"default": {"cloud-model"}}}, + Gates: Gates{Coverage: CoverageGate{Enabled: true}}, + } + if err := m.CheckKeys(escOnPipe); err == nil { + t.Fatal("escalation chain model without a key must fail-fast when gates are enabled") } } diff --git a/backend/internal/config/models.go b/backend/internal/config/models.go index 77b96f3..03ca02d 100644 --- a/backend/internal/config/models.go +++ b/backend/internal/config/models.go @@ -185,20 +185,29 @@ func (p Provider) APIKey() string { return os.Getenv(p.APIKeyEnv) } -// CheckKeys verifies that every non-local model REFERENCED by the pipeline has -// its provider API key resolvable in the environment — preflight fail-fast, -// чтобы пустой ключ не всплывал 401-м уже ПОСЛЕ reserve/списания слота -// (находка ревью). Проверяются только используемые модели: local-only прогон -// не должен падать на незаполненных облачных ключах. Escalation-цепочки тоже -// учитываются — их модели вызываются в Фазе 1. +// CheckKeys verifies that every non-local model the pipeline can ACTUALLY call +// has its provider API key resolvable in the environment — preflight fail-fast, +// чтобы пустой ключ не всплывал 401-м уже ПОСЛЕ reserve/списания слота. +// +// Проверяются только достижимые модели, не весь Р4-стек: +// - модели СТАДИЙ — всегда (зовутся на каждом чанке); +// - модели ЭСКАЛАЦИОННЫХ цепочек — ТОЛЬКО если включён хоть один гейт: +// эскалация запускается исключительно по провалу гейта, поэтому при +// gates.*.enabled=false (Фаза 0) она недостижима и её ключи не нужны. +// Без этого условия перевод одного чанка (DeepSeek+GLM) требовал бы ещё +// ключи Anthropic/Kimi из дефолтных цепочек, которые в Фазе 0 не зовутся. +// +// local-only прогон облачных ключей не требует. func (m *Models) CheckKeys(pipe *Pipeline) error { needed := map[string]struct{}{} for _, st := range pipe.Stages { needed[st.Model] = struct{}{} } - for _, chain := range pipe.Escal.Chains { - for _, mdl := range chain { - needed[mdl] = struct{}{} + if pipe.gatesEnabled() { + for _, chain := range pipe.Escal.Chains { + for _, mdl := range chain { + needed[mdl] = struct{}{} + } } } var problems []string diff --git a/backend/internal/config/pipeline.go b/backend/internal/config/pipeline.go index 8b87c9f..15fe61e 100644 --- a/backend/internal/config/pipeline.go +++ b/backend/internal/config/pipeline.go @@ -93,6 +93,12 @@ type Fanout struct { Candidates int `yaml:"candidates"` } +// gatesEnabled reports whether any QA-gate is on. Эскалация запускается только +// по провалу гейта, поэтому это сигнал «эскалация вообще достижима» (Фаза 1+). +func (p *Pipeline) gatesEnabled() bool { + return p.Gates.Coverage.Enabled +} + // CheckRunnable rejects a config whose mechanics the Phase-0 runner does NOT // implement — раздельно от schema-валидации (LoadPipeline), чтобы скелеты C2/C3 // парсились и валидировались как схема, но НЕ исполнялись молча. Без этого diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index f42a124..7621036 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -107,6 +107,38 @@ brief_hash, chunker/estimator version); перечанкование инвал timeout-путь «оплачено-но-потеряно» (≤1 вызов, укладывается в Р6); cache_ttl↔cache_write под один TTL (5m); гонка `Runner.clients` (безопасна при последовательном проходе, мьютекс — к fan-out Фазы 1–2). +**Ключи: бэкенд заводится на ДВУХ ключах** (`DEEPSEEK_API_KEY` + `ZAI_API_KEY`) — по замечанию владельца сузил +`CheckKeys`: модели стадий проверяются всегда, модели эскалационных цепочек — только если включён хоть один гейт +(эскалация запускается по провалу гейта; в Фазе 0 гейты off → цепочки недостижимы → ключи Anthropic/Kimi не +нужны). Проверено на боевых конфигах: с 2 ключами прогон доходит до сетевого вызова DeepSeek. `.env.example` +размечен «нужны сейчас / Фаза 1+ опционально». Anthropic/Kimi для Фазы 0 не требуются. + +### Вопросы от бэкенда — сессии «Полигон» и оркестратору (04.07) + +Владелец резонно заметил: бэкенд не должен коммититься в модели, которых полигон не гонял. Прежде чем в Фазе 1 +включать эскалацию и фиксировать редактора-дефолт, нужны замеры: + +1. **Полигону — Anthropic на «стерильном» канале (SFW).** Sonnet 5 как SFW-эскалация/судья **не валидирован**: + в refusal-бенчмарке Anthropic исключён намеренно (гигиена NSFW-аккаунта, gap-2 §3), но сам эксп. 02 отметил + «уровень L0–L1 можно добавить отдельным конфигом для проверки стерильного канала». Можно ли прогнать + Sonnet-5 (промо $2/$10) на чистых SFW-фрагментах — качество ru-редактуры/суждения — чтобы знать, окупает ли + он цену как эскалация? Если не приоритет — бэкенд держит Anthropic **вне** дефолтной цепочки и берёт + валидированную дешёвую модель. +2. **Полигону — качество редактора ru-стиля.** Эксп. 02 мерил роль ЧЕРНОВИКА (отказы/вырезания), но не роль + РЕДАКТОРА. Дефолт C1 — GLM-5 на редактуре — стоит на утверждении Р4, без замера. Планируется ли маленькое + сравнение GLM-5 vs Kimi-K2.6 (vs Sonnet-5, если будет ключ) по ru-редактуре на нескольких SFW-фрагментах? + От этого зависит дефолт редактора Фазы 1. +3. **Оркестратору — эскалационный дефолт в RU-контуре.** Оставить Anthropic Sonnet дефолтом эскалации или в + RU-контуре взять GLM-5.1/Gemini (Sonnet — только прямой контур, где per-book-премиум оправдан)? Это правка + Р4/экономики; бэкенд — только исполнитель конфига. +4. **Оркестратору — per-role provider-fallback?** Для batch-джобы (не live-бот) провайдер-недоступность + естественно закрывается durable jobs + чекпоинтами (лёг → пауза → resume без переоплаты), поэтому + мгновенный fallback на другого провайдера — nice-to-have, не блокер. Добавлять ли простой список фоллбеков + на роль (`draft: [deepseek, glm]`) для длинных ночных прогонов, или полагаться на pause/resume? Решение — за + оркестратором (владеет Р4/экономикой). + +Не блокирует Фазу 0 (принята на DeepSeek+Z.ai); ответы нужны к старту Фазы 1 (эскалация, дефолты ролей). + Следующее: Фаза 1 (перевод книги целиком) — чанкер, банк памяти v1, гейты (пороги полигона готовы), режим онгоинга. ## Полигон @@ -128,10 +160,15 @@ timeout-путь «оплачено-но-потеряно» (≤1 вызов, у - **Эксперимент 03 (задача 3) — первый замер + эталон** → [experiments/03-local-stand.md](experiments/03-local-stand.md). По п.3 вводных: (а) 30b-a3b в WSL **влезает** (mmap, без OOM), но на ollama даёт лишь **10 tok/s** — «Расхождение» с research/06 (25–30 обещаны через llama.cpp `--n-cpu-moe`; ставится в фоне — перемерить; Р4 менять пока не надо: рекомендация там и так llama-server); частично виноват и лимит WSL RAM 20GB — поднят до 26GB (`.wslconfig`, вступит после рестарта WSL); (б) 8b @ 27 tok/s (6.6GB VRAM) — на роли скрининга/экстракции годен. **Эталон DeepSeek снят: разрыв качественный** — API даёт корректные реалии («ворота Расёмон на улице Судзаку», «Новогоднее жертвоприношение») за ~$0.0005/фрагмент там, где обе локалки галлюцинируют («Рождественская дверь», «петухи»); NSFW-фрагменты ещё не гонялись. Для бэкенда: лег-таймаут vojo 45 с несовместим с чанками (63–278 с) — нужен свой профиль + стриминг. План расширения пула: ваниль qwen3:8b (цена абляции) → Qwen3.5-9B → RuadaptQwen3 (докачиваются в фоне). - Бэклог принят: проверка проброса cache-hit агрегаторами (п.4, нужны ключи) — в шагах ниже. Задачи 4–5 не начаты (bge-m3 уже на стенде). +### 2026-07-04 — Сессия 2 полигона (после рестарта WSL с RAM 26GB) + +- **llama.cpp `--n-cpu-moe` для 30b-a3b — перемерено, «Расхождение» с research/06 подтверждено**: потолок генерации на GTX 1070 **~13.5 tok/s** (не 25–30). Таблица конфигов в [03](experiments/03-local-stand.md): ollama 10 → llama.cpp `--cpu-moe` 11.2 → `--n-cpu-moe 33` 13.5 (VRAM 2.9→7.8GB, prefill 105→208). Узкое место — пропускная способность CPU-RAM (DDR4/Pascal), не рантайм; llama.cpp честно лучше ollama (+35% ген., ×2–3 prefill), но 30b-a3b на этом железе — только фоновые роли (~2.5 мин/главу). Скрипт: `eval/llama_moe_bench.sh`. +- **Замер 4 новых dense-моделей** (ваниль qwen3:8b / qwen3.5:9b / +abliterated / ruadapt) — готово, таблица в [03](experiments/03-local-stand.md). Три вывода: (1) **абляция бесплатна** — qwen3.5 vanilla vs abliterated идентичны по скорости и SFW-качеству → 18+-роль можно на abliterated без штрафа; (2) поколение 3.5>3 умеренно (лучше понимание, −25% токенов, −10% скорости); (3) **RuAdapt: токен-выигрыш подтверждён (−40% ru-токенов), но перевод развалился** (羅生門→«дерево с колокольчиками») → редактор ru-стиля, не переводчик (как и предупреждал research/06). Разрыв с API держится у всех. **Побочная находка для бэкенда:** thinking у Qwen3.5+/GLM/Gemini для роли переводчика обязателен к отключению (иначе пустой/обрезанный вывод — ловилось дважды: эксп. 02 и здесь). + ### Следующие шаги полигона - [x] Прогон refusal-бенчмарка (SFW/L1/L2-violence часть) — 66/66 ok, пороги в 02; explicit-часть ждёт фрагментов владельца. - [x] Эталон DeepSeek для сравнения черновиков — в 03. -- [ ] llama.cpp `--n-cpu-moe` для 30b-a3b — **сборка готова** (CUDA 12.6 user-space, пути/флаги в 03), замер после рестарта WSL с memory=26GB. Пул моделей для сравнения поколений/абляции/ru-адаптации докачан: qwen3:8b, qwen3.5:9b (+abliterated), ruadapt-qwen3:8b. +- [x] llama.cpp `--n-cpu-moe` для 30b-a3b — перемерено (потолок 13.5 tok/s, см. выше). - [ ] Прогон Qwen Model Studio (`data_inspection_failed`) и OpenRouter — при появлении ключей. - [ ] Проверка ru-агрегаторов: проброс cache-hit DeepSeek (два одинаковых запроса, сравнить usage) — разблокирует экономику ru-контура. - [ ] Довалидация калибровки на 3–5 главах реальных вебновелл (файлы владельца → `eval/data/samples/` + manifest).