textmachine/docs/archive/reports/TRANSPORT_PACK12_REPORT_2026-07-24.md

22 KiB
Raw Blame History

Отчёт бэкенд-сессии: ПАК-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.yamlinternal/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-Mshttpllm.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). Кап латентного 3044% 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-363complete)

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. Реализация: sensitivecontent_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/... -raceOK.
  • Полный 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).