From 7863461d374328acada696a617843b72b83442e3 Mon Sep 17 00:00:00 2001 From: "Claude (backend session)" Date: Fri, 24 Jul 2026 01:09:03 +0300 Subject: [PATCH] Land the research/21 section-five architecture addendum from the research session: Go-harness comparison, take-vs-skip table, transport refactor rejected --- docs/research/21-llm-transport-survey.md | 56 +++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/docs/research/21-llm-transport-survey.md b/docs/research/21-llm-transport-survey.md index 349b5631..160e2b80 100644 --- a/docs/research/21-llm-transport-survey.md +++ b/docs/research/21-llm-transport-survey.md @@ -2,7 +2,7 @@ > **РЕВЬЮ-ШАПКА ОРКЕСТРАТОРА (лендинг 23.07.2026).** Спот-чек адверсариальный, author≠reviewer: 7/7 несущих цитат о НАШЕМ коде подтверждены по file:line (`httpllm.go` jitter/клиент/429/parseRetryAfter, `provider_openai.go` additive, `llm.go` vojo) + 5/5 выборочных цитат из клонов байт-в-байт (mistral-strip, opencode z.ai-overflow, GLM finish-словарь, zai cache_control, grok-build shared_http). **Эрратум:** §1.1/§2 пишут «Mistral → 400», улика (докстринг litellm) говорит **422** extra_forbidden — класс верен, код статуса в отчёте неточен. **Поправка применимости (проверено исполнением):** наш wire — строго single-shot `system+system+user` (`render.go:223-227`), assistant-сообщений/reasoning-полей не шлём → квирки «Mistral extra_forbidden» и «deepseek multi-turn 422» для ТЕКУЩЕГО пере-прогона ЛАТЕНТНЫ (станут несущими при continuation #17 / любом multi-turn); GLM-overflow — инпуты юнитов ≪ окна (гард = future-proofing, не сегодняшний риск). Идущий пере-прогон НЕ трогать. **Все рекомендации — DEFER до после пере-прогона; Q7 — кандидаты до вендор-сверки (правило 10.07), в `00-provider-quirks.md` НЕ абсорбированы.** -**Статус: ПРИНЯТ (залендён 23.07); черновик сдан ресёрч-сессией 23.07.** Дата: 2026-07-23. Роль — ресёрч (полигон-профиль). Периметр: `$0`-сессия чтения кода, ни одного вызова моделей, `backend/` read-only. Материал — 11 склонированных харнессов в `/home/ubuntu/projects/tmp/`. Рекомендации применяются ТОЛЬКО после пере-прогона, отдельным решением. Все Q7-кандидаты — именно **кандидаты**: абсорбирует оркестратор ПОСЛЕ вендор-сверки (правило двух направлений, 10.07). +**Статус: ПРИНЯТ (залендён 23.07); черновик сдан ресёрч-сессией 23.07. §5-аддендум (арх-паттерны) дописан сессией и залендён вторым коммитом — спот-чек цитат обеих сторон (`capability.go:12-18` фолд-в-снапшот, `clients.go:15` BuildClient, `requestoption.go:80-85` замыкание) подтверждён; рефактор-вердикт «транспорт не трогаем» ратифицирован (D-лог, PROGRESS 23.07).** Дата: 2026-07-23. Роль — ресёрч (полигон-профиль). Периметр: `$0`-сессия чтения кода, ни одного вызова моделей, `backend/` read-only. Материал — 11 склонированных харнессов в `/home/ubuntu/projects/tmp/`. Рекомендации применяются ТОЛЬКО после пере-прогона, отдельным решением. Все Q7-кандидаты — именно **кандидаты**: абсорбирует оркестратор ПОСЛЕ вендор-сверки (правило двух направлений, 10.07). ## Метод, лицензии, надёжность @@ -216,6 +216,58 @@ --- +## §5. Архитектурные паттерны — брать / не брать + +Срез — **АРХИТЕКТУРА** (слои, абстракции, регистрация провайдеров, data-vs-код), а не механика транспорта §1–§4. Три независимых адверсариальных чтения (author≠reviewer, дозаказаны 23.07 по вопросу владельца): два Go-SDK (openai-go, anthropic-sdk-go), crush (Go), declarative-слой goose (Rust — но паттерн язык-агностичен). Наша опора для сравнения: транспорт-шов `internal/llm`, провайдер-фабрика `BuildClient` (switch-on-kind, `backend/internal/pipeline/clients.go:15-53`), `models.yaml`→`Capability`-резолвер, свёрнутый в job-снапшот (`backend/internal/llm/capability.go:12-18`, D5.2). + +### §5.1 Что вообще на Go и где у них транспорт + +- **crush** (FSL-1.1) — интерактивный кодинг-агент, ~50 internal-пакетов. **Свой wire-транспорт НЕ пишет**: делегирует внешним `charm.land/fantasy` (транспорт + generic agent-loop) + `charm.land/catwalk` (каталог провайдеров как данные, с 3-ярусной свежестью live→disk→embedded-at-release) + `openai/openai-go/v3`. Своё = policy/orchestration/state; квирки впрыскиваются в **композиционном** слое (`crush/internal/agent/coordinator.go:1095` `buildProvider`: Anthropic interleaved-thinking beta-header, ZAI `tool_stream` extra-body), не в транспорте. +- **openai-go / anthropic-sdk-go** — Stainless-**генерированные** SDK, в request-слое почти байт-в-байт совпадают; референс «как устроен generic-SDK», не харнесс. +- **Итог:** хэндкрафтового Go-транспорта под наш профиль среди клонов НЕТ. Ближайшее по духу к нам (тонкий транспорт + богатый policy-слой) — это как раз наш `internal/llm` + `internal/pipeline`; crush своим сплитом «lib владеет wire + generic-loop, app владеет policy/state» **независимо валидирует** наш `llm`/`pipeline`-шов. + +### §5.2 Центральный вердикт (ратифицирован владельцем 23.07) + +SDK-паттерн `RequestOption` = `func(*RequestConfig) error` (`openai-go/option/requestoption.go:80-85`; `openai-go/internal/requestconfig/requestconfig.go:102-104`) — **непрозрачное замыкание**. Наш детерминизм-контракт **сворачивает resolved wire-shape в job-снапшот** (`capability.go:12-18`), так что правка wire-формы **инвалидирует чекпойнты** (`--resnapshot`), а не тихо false-hit'ит. Замыкание **несериализуемо → нехешируемо → несворачиваемо в снапшот** → наша `Capability`-как-**данные** это **не «бедная версия option-паттерна», а правильный дизайн** для детерминизм-критичной системы с фиксированной поверхностью; SDK-паттерн для нас — прямо неверный. + +Подтверждающий сигнал с другой стороны: **goose под давлением ~50 провайдеров сходится к нашей же позе** — data-fицирует только **статический каталог** (endpoint/protocol/model-list/цены), а **все wire-квирки** (param-remap, reasoning-control, finish-reason, usage-shape) держит в **КОДЕ**, кейнутые по 3-значному `engine`-enum и hardcoded-спискам имён (`goose/crates/goose-providers/src/openai.rs:337-421` `PROVIDERS_NEEDING_MAX_TOKENS_REMAP` и др.). Эталон, на который смотрим, приходит **туда же, где мы**: `BuildClient` switch-on-kind + квирки в адаптере. + +Показательный артефакт хрупкости generic-SDK: два SDK **расходятся в HTML-escaping** JSON-фолбэка — openai `SetEscapeHTML(false)` (`openai-go/internal/requestconfig/requestconfig.go:241`) vs anthropic `SetEscapeHTML(true)` (`anthropic-sdk-go/internal/requestconfig/requestconfig.go:147`). Ровно тот тихий per-provider wire-дивергенс, что байт-детерминизм терпеть не может — а мы контролируем тело байт-в-байт через `Capability.applyToBody` (`capability.go:129-172`). + +### §5.3 Таблица «взять / не брать» + +| Паттерн | Источник (repo/file:line) | Вердикт | Почему (для нашего профиля) | +|---|---|---|---| +| Validation-тест каталога провайдеров | goose `declarative.rs:396-467` (`all_bundled_providers_are_valid`) | **ВЗЯТЬ** (высшая ценность/нулевая цена) | `go test` над `models.yaml`: `key_env` известен, `budget_field`/reasoning-control — валидный enum, `min_max_tokens ≤` капов, нет ссылок на необъявленный провайдер. Ловит дрейф ДО платного прогона. | +| loop-detection как stop-условие | crush `internal/agent/loop_detection.go:12-39`, wired `agent.go:1049` | **ВЗЯТЬ** (перетаргетить) | SHA-сигнатура повторяющегося выхода → триггер. Перетаргетить на нормализованную сигнатуру перевода-сегмента; **дополняет** `echoMineViolation`, не заменяет. ~40 строк, не зависимость. | +| «никогда не писать тихо $0 usage; оценить + ФЛАГ estimated» | crush `usage_fallback.go` (`EstimatedUsage`); goose `conversation/token_usage.rs:17-22` (`CostSource`) | **ВЗЯТЬ дисциплину** (= Q5#3) | Честную половину (settle резерв-оценки) делаем; не хватает легенды телеметрии. Эвристику `char/4` (`usage_fallback.go:171`) НЕ брать — CJK-мина; подставить fertility research/20 (`1.20·cjk+0.39·other`). | +| `preserves_thinking` как именованный per-provider флаг | goose `declarative.rs:147,154-156` | **ВЗЯТЬ опц.** | Централизует наш `echoes_when_thinking_off` + ReasoningNone-no-op в один снапшотимый capability-флаг. | +| Retry-After-Ms / `x-should-retry` | openai-go `requestconfig.go:396-449` / `:383-388` | **ВЗЯТЬ** (уже в §1) | Q2#1 / Q4#3. | +| Тонкий `Provider`-trait + отдельные `ProviderMetadata` | goose `base.rs:388-583` / `:20-47` | **посмотреть** | Хорошая сепарация runtime-контракта от каталог-метаданных; у нас уже близко (LLMClient + models.yaml). Подтверждение, не действие. | +| `RequestOption`/middleware-onion/`apijson`/`param.Opt[T]` | openai-go `option/requestoption.go:80-85`; `internal/apijson/*`; `packages/param/option.go:11-88` | **НЕ БРАТЬ** | Замыкание несворачиваемо в снапшот (§5.2); `apijson` = reflection-переписывание `encoding/json` ради 200 эндпоинтов; `Opt[T]` амортизируется на сотнях полей — у нас 3-полевое тело + `map[string]any`. | +| Мульти-схемный `Security`-резолвер | openai-go `requestconfig.go:830-906` | **НЕ БРАТЬ** | Один ключ на провайдера; у нас нет Admin/Bearer/OIDC-схем. | +| Интерактивный turn-loop (queue/cancel/IsBusy/drainQueue) | crush `agent.go:305-508` | **НЕ БРАТЬ** | Впрыскивает недетерминизм; batch-волна упорядочена. | +| Permission-сервис (human-in-the-loop) | crush `internal/permission/permission.go` | **НЕ БРАТЬ** | Нет интерактивного апрува. | +| Авто-суммаризация под context-pressure | crush `agent.go:1027-1048` (`StopWhen`) | **НЕ БРАТЬ** | Ломает детерминизм и верность; наш чанкер владеет бюджетом контекста заранее. | +| Live-фетч каталога на старте / posthog-телеметрия | crush `config/provider.go:139-197`; `event/event.go` | **НЕ БРАТЬ** | Сеть на старте = анти-детерминизм; у нас свой ledger. | +| Нондетерминированные источники каталога | goose `declarative.rs:209-233` (`${VAR}`@load), `model.rs:95-128` (OpenRouter-regenerated canonical), live `/v1/models`, n_ctx-probe | **НЕ БРАТЬ** | Каждый — вектор тихого false-hit, который наш снапшот специально устраняет. | +| Декларативная finish-map / param-remap в ДАННЫЕ | goose (держит в коде: `formats/openai.rs`, `http_status.rs:181-260`) | **НЕ БРАТЬ** | goose сам НЕ data-fицирует квирки; лишняя снапшот-поверхность. `extra_body`-passthrough уже покрывает «добавить странный параметр». | +| Base-URL-guessing / model-name-regex-роутинг | goose `openai.rs:85-450`; `formats/openai.rs:1531-1537` | **НЕ БРАТЬ** | Существует только потому, что goose принимает произвольный user-`base_url`; наш курированный YAML это обходит. Добавление модели у нас = правка данных, строго гигиеничнее. | +| Debounce/coalesce стрим-дельт | crush `message/message.go:21` | **НЕ БРАТЬ** | Оптимизация стрим-TUI; batch пишет дискретные артефакты. | + +### §5.4 Оценка кода (наш vs чужой, без реверанса) + +- **Наш.** Purpose-built и факторизован: транспорт 7 файлов, `pipeline` 37 файлов **файл-на-концерн** (банк-майнер расщеплён `miner_detect/alias/emit/palladius/patterns/substrate`; крупнейший `memory.go` 914 строк). Детерминизм-first (Capability в снапшоте), деньги-first (типизированные `HTTPStatusError`/`BilledDecodeError`, reservation-settle). **В ряде мест чище/корректнее SDK для нашей оси**: явный `(T, retryable bool, err)` (`httpllm.go:97`) вместо SDK-шного `noRetryError`-маркера через `errors.As`; billing-типизированные ошибки; снапшот-сериализуемая wire-форма; байт-контроль тела. +- **SDK (openai-go/anthropic-sdk-go).** Компетентная generic-машинерия. Изящно на швах (option-fold, middleware-onion, `Opt[T]` три-стейт present/null/absent, инъекция `HTTPClient` для тестов), тяжело в глубине (`apijson`). Честно: «идиоматичный **вывод генератора**, не рукотворное суждение». Для нас — оверинжиниринг по каждой оси генеричности. +- **crush.** Качественный, concurrency-честный; **комментарии выше среднего** (кодируют *почему* и контракт гонок: broker-семантика, eventual-consistency сообщений, race-safety permissions); дженерики к месту (`Broker[T]`, `cache[T]`). Смеллы: **два god-файла** (`agent.go` ~2278 строк, `coordinator.go` ~1500; `agent.Run` — одна ~750-строчная функция) — тут **мы факторизованы лучше**; `char/4` CJK-мина; stringly-typed `getProviderOptions` (JSON marshal→merge→unmarshal→`map[string]any`, `coordinator.go:344-390`); квирки размазаны inline-switch'ами. +- **goose.** Идиоматичный, хорошо оттестирован (`test_case`-матрицы), тонкий `Provider`-trait + отдельные метаданные = хорошая сепарация. Смеллы: builder-бойлерплейт (поля по 3 раза, `openai.rs:156-271`), квирки-по-списку-имён, base-URL-guessing «крысиное гнездо». **Главный урок — их cruft:** мёртвый `max_tokens` в 29 JSON (молча съеден serde, нет `deny_unknown_fields`) и инертный `supports_cache_control` показывают: **hand-каталог БЕЗ строгого резолвера копит невалидированные/непотребляемые поля.** Наш «резолвер-который-падает + снапшот-который-форсит-`--resnapshot`» — ровно та дисциплина, которой goose лишён. Берём их **validation-тест**, не их **терпимость к полям**. + +### §5.5 Рефактор-вердикт + +**Транспорт/провайдер-слой — НЕ рефакторить.** Три независимых эталона (generic-SDK, интерактивный агент, 50-провайдерный каталог) валидируют наш тонкий-транспорт + data-каталог шов; их «красоты» написаны под другие задачи, а часть (option-замыкания, live-каталог, авто-суммаризация) **враждебна нашему детерминизму**. Реальный архитектурный долг — **не в транспорте, а в `pipeline`** (сломанный телефон → 7-слойная целевая, D39), и он уже ратифицирован — туда энергия. Из §5 к внедрению — только 4 точечные ~40-строчные добавки из §5.3 (validation-тест, loop-detection, CostSource-флаг, `preserves_thinking`), **все DEFER до после пере-прогона**, ни одна не является рефакторингом. + +--- + ## Сообщение оркестратору (кратко) research/21 готов (черновик, не коммичен — лендите вы). Главное: **наш транспорт в базе опережает/вровень со всеми 11** — единый retryLoop, 5-мин-кап Retry-After, fail-loud terminal, billed-decode-типизация, served-model биллинг, min_max_tokens-флор, double-count-гард. Значимого рычага против **Mistral-48%** (только concurrency-cap) и **тихого усечения 2–8k** (стриминг даёт лишь ДЕТЕКЦИЮ, не resume; continuation — не стриминг) никто не даёт. @@ -224,4 +276,6 @@ research/21 готов (черновик, не коммичен — лендит **Дёшево-и-подтверждено к внедрению (после пере-прогона):** fractional/ms Retry-After, quota-429→terminal, HTTP/2 keepalive, CostSource-маркер, xAI reasoning identity-гард, план native-Gemini-судьи. +**§5 (архитектура, дозаказ владельца 23.07):** транспорт/провайдер-слой **не рефакторить** — три эталона (generic-SDK, интерактивный агент, 50-провайдерный каталог) валидируют наш тонкий-транспорт+data-каталог шов; option-замыкание несворачиваемо в снапшот → `Capability`-как-данные правильна, а не «бедный option-паттерн»; goose под 50 провайдерами сходится к нашей позе (квирки в коде, данные — только статический каталог). К внедрению из §5 — 4 точечные ~40-строчные добавки (validation-тест `models.yaml`, loop-detection-гард, CostSource-флаг, `preserves_thinking`), все DEFER. Реальный арх-долг — в `pipeline`/D39, не в транспорте. + **Процессная заметка (мандат 12.07):** синтез-агенты Q1/Q3 вернули плейсхолдер-заглушки, схема не поймала — самопроверка исполнением поймала, Q1/Q3 пере-выведены из целого survey-сырья + независимая верификация (8/8 CONFIRMED). Ещё один датапоинт, что «схема-валидно» ≠ «содержательно».