# Отчёт бэкенд-сессии: ПАК-12 «транспорт-гигиена» (по research/21) **Дата:** 2026-07-24. **Роль:** бэкенд. **Промт:** `docs/BACKEND_TRANSPORT_PACK_SESSION_PROMPT.md` (обновлён 99ec074 — перекройка границы 23.07). **Сессия НЕ коммитит — лендит оркестратор (пак-12 лендится ПЕРВЫМ).** ## Итог одной строкой Реализованы все пункты пака-12 в моей зоне (`internal/llm/**` + новый валидация-тест в `internal/config/`): точечные, wire-нейтральные добавки транспорта. **Инвариант «ноль wire-байтов» соблюдён** (ни одна правка не трогает тело запроса → snapshot не двигается, `--resnapshot` не требуется). Зона зелёная: `go build/vet` + `go test ./internal/llm/... ./internal/config/... -race` — OK. Пункты 7 и 10 **переданы паку-13** (перекройка границы), мои out-of-zone правки по ним **откачены** (чужие файлы не тронуты). Пункт 8b **перенесён в llm-адаптер** (зона `disposition.go` теперь пака-13). ## Дельта скоупа (перекройка границы 23.07) | Пункт | Статус | Зона | |---|---|---| | 1 валидация-тест models.yaml | ✅ сделано | `internal/config/` | | 2 quota-429→terminal | ✅ | `internal/llm/httpllm.go` | | 3 parseRetryAfter дробн.+ms | ✅ | `httpllm.go` | | 4 HTTP/2 keepalive + гейт-проба | ✅ | `httpllm.go` + `go.mod` | | 5 пропорциональный джиттер | ✅ | `httpllm.go` | | 6 xAI reasoning identity-гард | ✅ | `provider_openai.go` | | **7 CostSource-маркер** | **ПЕРЕДАН паку-13** — правки откачены | (была pipeline/store/tmctl) | | 8a пин status→retryable-мапы | ✅ | `httpllm.go` | | 8b композит-finish классификация | ✅ **перенесён в llm-адаптер** | `provider_openai.go` (не `disposition.go`) | | 9 кап billed-decode-ре-биллов | ✅ | `httpllm.go` | | **10 сегмент-луп-гард** | **ПЕРЕДАН паку-13** — правки откачены | (была pipeline/quality) | | вендор-сверка (a) GLM finish | ✅ подтверждено → реализовано | `provider_openai.go` | | вендор-сверка (b) kimi temp | ✅ подтверждено «force:1 валиден» → без изменений | report-only | ### Про откат пунктов 7/10 (координация) Оркестратор передал пункты 7 и 10 паку-13 (они pipeline-зона). Я успел их сделать до перекройки; после — **откатил ТОЛЬКО свои out-of-zone правки** `git checkout HEAD -- <файлы>`, предварительно построчно убедившись, что в этих файлах нет содержимого пака-13 (был только мой код). Файлы пака-13 (`internal/pipeline/{seeding,chunker,mineddelta_collision_test}.go`, `internal/lang/langpack.go`, `configs/langpacks/zh-ru/heading.txt`) **не тронуты**. Полный диф моей реализации 7/10 сохранён в скретчпаде (`pack12_modified.diff`) — доступен паку-13 как референс, если пригодится. ### Про пункт 8b (перенос в llm-зону) Пункт 8b остаётся за паком-12, но его исходная реализация была в `internal/pipeline/disposition.go` (`classify`), которая теперь зона пака-13. Перенёс в **llm-адаптер**: `normalizeOpenAIFinish` (`provider_openai.go:171`) сворачивает Gemini-композит `content_filter: PROHIBITED_CONTENT` → нейтральный `content_filter` **на границе адаптера** (как `mapAnthropicStopReason`), не трогая пайплайн-классификатор. Downstream-классификатор (exact-match) получает нейтральное значение и классифицирует его — цель пункта 8b (композит должен классифицироваться) достигнута. Незнакомый finish проходит RAW → остаётся fail-loud. **Это чище оригинала** (wire→neutral маппинг там, где ему место). ## Реализация по пунктам (file:line) ### 1. Валидация-тест `models.yaml` — `internal/config/models_catalog_test.go:25` (`TestShippedModelsCatalogValid`) Загружает **боевой** `configs/models.yaml` через `LoadModels` и падает на дрейфе каталога ДО платного прогона (аналог goose `all_bundled_providers_are_valid`). Пинит: `LoadModels` проходит (ловит устаревший `prices_checked`, необъявленного провайдера, битый enum, эхо-мину); + инварианты, которые сам загрузчик не проверяет: каждый **не-local** провайдер, на который ссылается модель, объявляет `api_key_env`; reasoning ∈ {subset,additive,additive_total}; цены >0 у не-local; каждая модель ссылается на объявленного провайдера; resolved `min_max_tokens` ∈ [0, 200000]. Итерирует по реальным провайдерам/моделям (не хардкод имён). *(Боевой каталог: `prices_checked: 2026-07-10`, 14 дней — свеж.)* ### 2. Quota-429 → terminal — `httpllm.go:449` (`retryableStatus`) + `httpllm.go:466` (`isQuotaExhausted`) Узкий body-маркер (`insufficient_quota` / `quota_exceeded`) на 429 → терминальная ошибка (не 3 ретрая под USD-резервацией). НЕ широкая regex-батарея (та самая «exact matchers dead» хрупкость, которую пак избегает). HTTP 402 уже был терминальным (падал в default-ветку). Тест `TestQuota429IsTerminal` (`pack12_transport_test.go:57`): оба маркера → 1 вызов; обычный 429 без маркера → 3 ретрая (retryable). ### 3. `parseRetryAfter`: дробные секунды + `Retry-After-Ms` — `httpllm.go:540` Сигнатура сменена `(string)` → `(http.Header)`; приоритет: `Retry-After-Ms` (float ms) → `Retry-After` (float ИЛИ integer секунды через `ParseFloat`) → HTTP-дата. Старый `strconv.Atoi` резал `1.5`/ms → 0 → слепой exp-backoff. Значение по-прежнему под 5-мин капом (`maxRetryAfterWait` в `nextBackoff`). Оба call-site обновлены (`httpllm.go` + `provider_anthropic.go:230`). Тест `TestParseRetryAfterFractionalAndMs` (`pack12_transport_test.go:95`). ### 4. HTTP/2 keepalive — `httpllm.go:92` (`keepAliveHTTPClient`), константы `httpllm.go:80` (15s/5s) Клонирует `http.DefaultTransport` (proxy-from-env, dial/TLS-таймауты) + `http2.ConfigureTransports` с `ReadIdleTimeout=15s`/`PingTimeout=5s` (по образу офиц. grok-build). HTTPS-провайдеры получают keepalive; plaintext-httptest (без ALPN) остаётся HTTP/1.1 на том же базовом транспорте. Local-провайдер (`httpc != nil`, no-proxy) **не тронут**. Зависимость `golang.org/x/net@v0.26.0` добавлена (из module-cache; прямая, `go mod tidy` чист). **Гейт-проба (без платных вызовов):** live-дым-проба на 6 фронтов провайдеров с tuned-транспортом (`ReadIdleTimeout=1s`, idle-hold 3s → PING-фреймы летят, проверка reuse коннекта через httptrace). Результат — **все 6 PING-толерантны** (HTTP/2, коннект переиспользован после idle-PING'ов), fallback на h1 **не нужен ни одному**: ``` api.deepseek.com HTTP/2 code 401/401 reused=true → PING-TOLERANT api.z.ai HTTP/2 code 301/301 reused=true → PING-TOLERANT api.x.ai HTTP/2 code 421/421 reused=true → PING-TOLERANT api.mistral.ai HTTP/2 code 404/404 reused=true → PING-TOLERANT generativelanguage.googleapis.com HTTP/2 code 404/404 reused=true → PING-TOLERANT api.moonshot.ai HTTP/2 code 404/404 reused=true → PING-TOLERANT ``` *(Проба — throwaway `_test.go`, реальный сетевой вызов, в дереве НЕ оставлена; копия в скретчпаде `zz_h2probe_test.go.bak`.)* ### 5. Пропорциональный джиттер — `httpllm.go:191` (`proportionalJitter`), вызовы в `nextBackoff:162` `rand.Intn(250)ms` (фикс) → **±25% пропорционально backoff** (симметрично, mean-preserving) для exp-backoff; для honored-Retry-After — **только положительный [0,+25%]** (де-синк N-∥ волн, но НИКОГДА не ретраим раньше серверной подсказки). Тест `TestProportionalJitterBounds` (`pack12_transport_test.go:221`): границы ±25%, не-отрицательность, положительная-только ветка. ### 6. xAI reasoning identity-гард — `provider_openai.go:92` (`additiveReasoning`), вызов `:130` Additive-фолд `reasoning_tokens` только при `total == prompt+completion+reasoning`; при `total>0` и нарушении identity → reasoning уже внутри completion (subset) → 0 + **WARN** (кандидат в квирк-реестр, правило двух направлений). При `total==0` (не отдан) — неверифицируемо → держим известный xAI-additive дефолт (не слепим ledger). Кап латентного 30–44% overcount. Тест `TestAdditiveReasoningIdentityGuard` (`pack12_transport_test.go:132`): identity-holds→50, broken→0+WARN, total-unreported→50. Существующий `TestAdditiveReasoningSemantics` (total не отдан) — по-прежнему зелёный. ### 8a. Пин status→retryable-мапы — `httpllm.go:449` (`retryableStatus`) Единая пиннабельная точка решения retry/terminal, использована в `attempt()` (`httpllm.go:414`) И в anthropic-адаптере (`provider_anthropic.go:227`, консистентность; 529 покрыт ≥500). Тест `TestRetryableStatusMap` (`pack12_transport_test.go:24`): пинит 400/401/402/403/404/408/409/422→terminal, 429-plain→retry, 429-quota→terminal, 500/502/503/529→retry. ### 8b. Композит-finish классификация — `provider_openai.go:171` (`normalizeOpenAIFinish`), вызов `:147` См. «перенос в llm-зону» выше. Сворачивает `content_filter:` (Gemini-композит) И GLM `sensitive` → нейтральный `content_filter`. Всё прочее (`network_error`, `model_context_window_exceeded`, любой unknown) — RAW → fail-loud (НЕ litellm-стиль unknown→stop). Тест `TestNormalizeOpenAIFinish` (`pack12_transport_test.go:182`) + end-to-end через httptest. ### 9. Кап billed-decode-ре-биллов — `httpllm.go:348-363` (в `complete`) `BilledDecodeError` ретраится **≤1 раза** (счётчик `billedDecodeSeen` в замыкании ОДНОГО `complete()`-вызова, не разделяем между вызовами): первый — retryable (транзиентный обрыв прокси стоит одной попытки), второй billed-decode → terminal. Платный 2xx-мусор больше не жжёт полный MaxAttempts штормом (grok-build-регрессия «laundering serialization → full-budget storm»). Runner получает типизированный `BilledDecodeError` после 2 вызовов и сеттлит estimate (тип сохранён на терминальном возврате). Тест — обновлён `TestBilledDecodeErrorReBillCap` (`httpllm_test.go:305`): пинит 2 вызова (было 3). ## Вендор-сверка (правило 10.07 — реализация ТОЛЬКО при подтверждении в офиц. доке) **(a) GLM finish-словарь `sensitive`/`network_error` — ПОДТВЕРЖДЕНО → реализовано.** Источник: `docs.z.ai/api-reference/llm/chat-completion` (сверено 2026-07-24). Документированные значения `finish_reason`: **`stop | tool_calls | length | sensitive | model_context_window_exceeded | network_error`**. Реализация: `sensitive`→`content_filter` (наш редактор — GLM; фильтр должен читаться как фильтр, а не clean stop); `network_error` — НЕ маппим в stop, оставлен RAW (fail-loud). **Бонус-находка:** `model_context_window_exceeded` тоже документирован как finish_reason — относится к отложенному пункту «GLM context-overflow гард» (в NOT-do списке): **не реализовано**, оставлен RAW; **кандидат для решения оркестратора** по отложенному overflow-гарду. **Строки-кандидаты для `00-provider-quirks.md` (абсорбирует оркестратор ПОСЛЕ решения):** > `glm-zai` | finish_reason вне OpenAI-словаря: `sensitive` (контент-фильтр GLM), `network_error` (сетевой обрыв — НЕ маппить в stop), `model_context_window_exceeded` (переполнение контекста). | NEW·vendor-verified | docs.z.ai/api-reference/llm/chat-completion (2026-07-24) **(b) kimi reasoning-модели: temperature omit vs force:1 — ПОДТВЕРЖДЕНО «force:1 валиден» → БЕЗ изменений.** Источник: `platform.kimi.ai/docs/guide/migrating-from-openai-to-kimi` (сверено 2026-07-24). Цитата: *«Thinking mode uses a fixed `temperature=1.0`»*, non-thinking — `0.6`, *«Any other value will result in an error. We recommend not explicitly setting the temperature… or following the above requirements.»* Наш `force:1` (для thinking-Kimi) — **документированно-валидный выбор** (одна из двух рекомендованных опций). Omit — эквивалентная альтернатива, но: 1. наш `force:1` НЕ баг (работает для thinking-пути); 2. **смена temperature force:1→omit ДВИГАЕТ WIRE** (поле temperature присутствует→отсутствует) → snapshot сдвиг → `--resnapshot` → **нарушает инвариант пака «ноль wire-байтов»**. Вывод: **capability-твик не делаем** (не подтверждена НЕОБХОДИМОСТЬ + противоречит инварианту пака). Report-only. ## Инвариант «ноль wire-байтов» — верификация Ни одна правка не трогает сборку тела запроса (`openAIRequest.MarshalJSON` / `Capability.applyToBody` / снапшот-фолд `capability.go`). Изменения касаются: (1) классификации retry/terminal ответа, (2) парсинга backoff-заголовков, (3) джиттера, (4) транспортного keepalive (PING-фреймы, не DATA), (5) учёта usage (reasoning), (6) нормализации finish_reason ответа. Всё — **response-side / transport-layer**, request-payload неизменен → request_hash неизменен → snapshot неизменен. ## Тесты (мандат самопроверки 12.07 — ревью ИСПОЛНЕНИЕМ) Новые/обновлённые тесты, все зелёные под `-race`: - `TestShippedModelsCatalogValid` (п.1), `TestQuota429IsTerminal` (п.2), `TestParseRetryAfterFractionalAndMs` (п.3), `TestProportionalJitterBounds` (п.5), `TestAdditiveReasoningIdentityGuard` (п.6), `TestRetryableStatusMap` (п.8a), `TestNormalizeOpenAIFinish` (п.8b), `TestBilledDecodeErrorReBillCap` (п.9, обновлён с 3→2). ## Приёмка - `go build ./...` — **OK** (всё дерево, включая in-flight пака-13, компилируется). - `go vet ./internal/llm/... ./internal/config/...` — **OK**. `go test ./internal/llm/... ./internal/config/... -race` — **OK**. - **Полный `go vet ./...` / `go test ./... -race` СЕЙЧАС падает ТОЛЬКО в `internal/pipeline`** — это in-flight состояние пака-13 (`internal/pipeline/chunker_test.go:27`: `SplitChunks` получил новый параметр `*lang.HeadingRule`, тест не догнан). **Не моя зона, не мой код, не чиню** (гардрейл координации). Финальный полный `./... -race` — за оркестратором на СОВМЕСТНОМ лендинге пака-12 + пака-13. - **Golden:** по обновлённому промту «golden тебя не касается» (моя зона в golden-путь не входит); инвариант «ноль wire-байтов» держит golden байт-идентичным по построению (request-payload не тронут). - gofmt: мои файлы чисты. *(`internal/llm/llm.go` числится gofmt-«грязным» — это pre-existing состояние HEAD, мной не тронуто, не правлю.)* ## Адверсариальный селф-ревью (author≠reviewer, ≥3 линзы) — ИСПОЛНЕН Воркфлоу: 4 линзы (детерминизм/wire-нейтральность · деньги · конкурентность · корректность+тест-адекватность), каждая находка верифицируется НЕЗАВИСИМЫМ скептиком (default REFUTED, high-effort). 6 агентов, 0 ошибок. **Итог: 0 ПОДТВЕРЖДЁННЫХ дефектов.** - Линзы **деньги** и **конкурентность** — чисты (пусто): identity-гард не двоит/не недоучитывает; `billedDecodeSeen` — per-`complete()` замыкание (не разделяем, гонки нет); keepalive-клиент concurrency-safe; джиттер не уводит backoff в ≤0 и не ретраит раньше honored-Retry-After. - Линзы **детерминизм** и **корректность** — обе подняли ОДНУ и ту же находку (low): пропорциональный джиттер honored-Retry-After применяется ПОСЛЕ клампа `maxRetryAfterWait`, поэтому реальный сон может превысить 5-мин потолок на ≤+25% (≤75с). **Оба независимых скептика — REFUTED:** (1) пре-диф Retry-After-путь УЖЕ овершутил кламп (был `backoff=ra` + `rand.Intn(250)ms`), диф лишь расширил мягкий овершут 250мс→75с, а не ввёл его; (2) джиттер ПОСЛЕ клампа — намеренный анти-thundering-herd дизайн: кламп ДО джиттера схлопнул бы все N-∥ волны с `Retry-After≥5min` в один инстант = ровно тот herd, против которого джиттер; (3) фактический контракт `maxRetryAfterWait` — «не висеть часами, оставаться ctx-прерываемым», что ограниченный +75с ctx-прерываемый сон полностью удовлетворяет. **Не wire-move, не мис-биллинг, не гонка, не спек-violation.** **Единственная реальная субстанция** (оба ревьюера сошлись): моя правка сделала комментарий `maxRetryAfterWait` («never for longer than this») чуть неточным (пре-диф овершут ≤250мс, мой ≤75с). **Правка применена** (`httpllm.go:106-113`): комментарий уточнён — кламп бьёт по ХИНТУ, реальный сон может +25% от де-синк-джиттера, добавляемого после клампа НАМЕРЕННО; ctx-прерываем. **Ноль изменений поведения**, тесты зелёные. Поведение оставлено как есть (post-cap джиттер — верный дизайн; кламп-после-джиттера воссоздал бы herd на потолке — что скептик и отметил). ## НЕ делал (явные не-цели, подтверждено) Рефакторинг транспорта · стриминг · assistant-prefill continuation · native-Gemini судья · GLM cache_control · GLM context-overflow гард (но вендор-находка `model_context_window_exceeded` зафиксирована выше) · дата-фикация каталога · `prompt_cache_key`. ## Заметки оркестратору (координация) 1. **Лендить пак-12 ПЕРВЫМ** (по промту), затем пак-13. 2. **Миграция БД:** пак-12 миграцию НЕ добавляет (пункт-7, который её содержал, ушёл паку-13). Если пак-13 добавляет миграцию для CostSource — версия `v10` свободна. 3. **Совместный полный `./... -race`** — прогнать после лендинга ОБОИХ паков (сейчас блокирован in-flight `SplitChunks` пака-13). 4. **Кандидат-строки для `00-provider-quirks`** (GLM finish-словарь) — см. секцию вендор-сверки; абсорбировать после решения. 5. **Зависимость `golang.org/x/net@v0.26.0`** добавлена в `go.mod`/`go.sum` (нужна для http2-keepalive).