22 KiB
Отчёт бэкенд-сессии: ПАК-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:<detail> (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 — эквивалентная альтернатива, но:
- наш
force:1НЕ баг (работает для thinking-пути); - смена 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.
Заметки оркестратору (координация)
- Лендить пак-12 ПЕРВЫМ (по промту), затем пак-13.
- Миграция БД: пак-12 миграцию НЕ добавляет (пункт-7, который её содержал, ушёл паку-13). Если пак-13 добавляет миграцию для CostSource — версия
v10свободна. - Совместный полный
./... -race— прогнать после лендинга ОБОИХ паков (сейчас блокирован in-flightSplitChunksпака-13). - Кандидат-строки для
00-provider-quirks(GLM finish-словарь) — см. секцию вендор-сверки; абсорбировать после решения. - Зависимость
golang.org/x/net@v0.26.0добавлена вgo.mod/go.sum(нужна для http2-keepalive).