283 lines
75 KiB
Markdown
283 lines
75 KiB
Markdown
# research/21 — Обзор LLM-транспорта чужих харнессов
|
||
|
||
> **РЕВЬЮ-ШАПКА ОРКЕСТРАТОРА (лендинг 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. §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).
|
||
|
||
> **→ ПОСТРОЕНО паком-12 «транспорт-гигиена» (24.07, `7fe0a5b`; отчёт `docs/archive/reports/TRANSPORT_PACK12_REPORT_2026-07-24.md`; отметка 25.07, оркестратор №7):** §1-пункты 1–6/8a/8b/9 реализованы (quota-429→terminal · дробный Retry-After · HTTP/2 keepalive · джиттер · xAI identity-гард · GLM finish-нормализация · пин retryable-мапы · кап billed-decode · models.yaml-валидация); пункты 7 (CostSource) и 10 (луп-гард) — паком-13 (`2f91b04`). DEFER снят; Q7-абсорбция в `00-provider-quirks.md` — по-прежнему только после вендор-сверки.
|
||
|
||
## Метод, лицензии, надёжность
|
||
|
||
- **Оркестрация.** 11 репо-агентов (survey Q1–Q6) ∥ 6 провайдер-агентов (quirk-mining Q7-код) → 6 синтез-агентов (по вопросу) + Q7-дедуп → адверсариальная верификация (author≠reviewer): 57 агентов, 0 ошибок. Онлайн-жатва трекеров (Q7) — отдельно, из главной сессии (`gh` + WebFetch).
|
||
- **Самопроверка исполнением поймала баг (мандат 12.07 в действии).** Синтез-агенты Q1 (prompt-cache — вопрос №1) и Q3 (finish_reason) вернули **плейсхолдер-заглушки** («test»), схема их не отсеяла — ровно тот класс, о котором предупреждает мандат. Сырьё survey по Q1 (18 находок) и Q3 (17 находок) — целое; Q1/Q3-синтез пере-выведен вручную из survey-находок + **независимая адверсариальная верификация 8 несущих цитат свежими агентами** (все CONFIRMED, детали ниже). Два REFUTED в авто-верификации — это и есть те заглушки; в отчёт не вошли.
|
||
- **Дисциплина цитат.** Каждая находка о чужом коде — с `repo/file:line`; спорное верифицировано чтением второго места (call-site/тест). Пометки [CONFIRMED]/[PARTIAL] — вердикт адверсариального ре-чтения (PARTIAL = паттерн реален, но формулировка/оценка завышена — см. per-question). НИКАКОГО копирования чужого кода: только описание паттернов.
|
||
|
||
**Лицензии (per-repo, апстрим):**
|
||
|
||
| repo | апстрим | язык | лицензия | примечание |
|
||
|---|---|---|---|---|
|
||
| codex | openai/codex | Rust | Apache-2.0 | Responses API only |
|
||
| gemini-cli | google-gemini/gemini-cli | TS | Apache-2.0 | @google/genai |
|
||
| grok-build | xai-org/grok-build | Rust | Apache-2.0 (© SpaceXAI) | **официальный xAI**, open-sourced 15.07.2026 |
|
||
| goose | block/goose | Rust | Apache-2.0 | declarative-провайдеры |
|
||
| aider | Aider-AI/aider | Python | Apache-2.0 | обёртка над litellm |
|
||
| cline | cline/cline | TS | Apache-2.0 | @anthropic-ai/sdk + AI-SDK |
|
||
| opencode | sst/opencode | TS | MIT | Vercel AI SDK |
|
||
| litellm | BerriAI/litellm | Python | **MIT-ядро** (`enterprise/` — иная лиц., **НЕ читалась**) | самый богатый провайдер-слой |
|
||
| crush | charmbracelet/crush | Go | **FSL-1.1** (читать можно, **копировать нельзя**) | транспорт делегирован во внеш. `charm.land/fantasy` |
|
||
| openai-go | openai/openai-go | Go | Apache-2.0 | референс SDK |
|
||
| anthropic-sdk-go | anthropics/anthropic-sdk-go | Go | MIT | референс SDK |
|
||
|
||
**Итог одной строкой.** Наш транспорт в базовой форме **опережает или вровень** со всеми 11: единый `retryLoop`, honored-Retry-After с 5-мин капом, fail-loud terminal 4xx, billed-decode-типизация, per-adapter reasoning-семантика, served-model биллинг, double-count-гард, min_max_tokens-флор — то, что другие имеют частично или не имеют. Значимого рычага против Mistral-48% или тихого усечения ни один харнесс **не даёт**. Ценность обзора — в **точечных догоне** (fractional Retry-After, quota-429→terminal, HTTP/2 keepalive, GLM-cache_control, native-Gemini usage) и в **Q7-квирках, прямо задевающих пере-прогонные армы glm/mistral/deepseek-pro**.
|
||
|
||
---
|
||
|
||
## §1. Топ-рекомендации (ранжированы по ожидаемому импакту)
|
||
|
||
Формат: приоритет · [вердикт] · рекомендация · **какая метрика двинется и на сколько** · гейт. «Держим как есть» с внешним подтверждением — тоже вывод (мы там ведём).
|
||
|
||
### Надёжность / тихий провал (высший приоритет — задевает пере-прогонные армы)
|
||
|
||
1. **[VERIFIED, NEW] Mistral: strip output-only полей перед отправкой.** Mistral — строгая `extra_forbidden`-схема входа: реплей `reasoning_content`/`thinking_blocks` или сообщения с `null`-значениями → **HTTP 400**. Наш билингвальный редактор возит src→dst-блок; если сборщик когда-либо приложит reasoning-поле (эскалация/второй хоп), **mistral-арм редактора упадёт**. Метрика: **надёжность mistral-editor-арма — из «падает на первом же запросе с reasoning-полем» в 0**; ноль COGS. Гейт: проверить наш message-builder перед тем, как mistral-арм пойдёт прод-путём (он и так за rate-guard, D39.12). Ev: `litellm/litellm/llms/mistral/chat/transformation.py:392-405` (`_strip_output_only_fields`, assistant-only).
|
||
|
||
2. **[VERIFIED, NEW] GLM молча глотает context-overflow.** z.ai/GLM, по комментарию opencode, может **принять переполненный контекстом промпт без 4xx** — модель просто усекает вход. Для нашего несущего glm-5-редактора это значит: волатильный хвост (инъекция банка памяти) **тихо отваливается**, и чистый 2xx отгружает деградированную правку без единого сигнала ошибки — ровно «сломанный телефон», против которого арх-ресет D39. Метрика: **качество editor-арма** — конвертирует тихий обвал в громкий фейл; предотвращает молчаливое усечение банк-хвоста. Действие: pre-send гард «оценка токенов vs `context_window`» для GLM (Capability может нести `context_window`), + маппинг context-length 400/413 → отдельный terminal `ContextOverflow`. Гейт: одна-харнессовая примета (opencode-комментарий, не wire-трасса) → **вендор-сверка z.ai обязательна** перед абсорбцией; порог оценщика — консервативный. Ev: `opencode/packages/opencode/src/provider/error.ts:30-31`; goose `is_context_length_exceeded_message` `goose/crates/goose-providers/src/http_status.rs:181-249`.
|
||
|
||
3. **[VERIFIED, NEW] GLM finish-reasons: `sensitive`→content_filter, `network_error`→stop.** GLM отдаёт нестандартные finish-строки вне OpenAI-словаря. `sensitive` — это контент-фильтр GLM (наш редактор — GLM!), а `network_error`, отмаппленный в `stop`, — **риск тихого провала** (сетевую ошибку выдаёт за чистое завершение). Наш контракт `finish=stop-only` + echo/coverage-гейты это ловят downstream, но матчер finish должен знать оба значения. Метрика: **корректность finish/undercount** — GLM-фильтр перестаёт выглядеть как валидный stop. Ev: `litellm/litellm/litellm_core_utils/core_helpers.py:120-122`.
|
||
|
||
4. **[CONFIRMED] Quota/credits-429 → отдельный TERMINAL, не retryable.** Сейчас ВСЕ 429 retryable (`httpllm.go:341-342`): ключ с кончившимися кредитами в середине волны сжигает 3 попытки + до 3×5мин honored-Retry-After под удержанной USD-резервацией — и всё равно падает. opencode и goose классифицируют это как fatal (детерминизм: оператор должен пополнить, ретрай не поможет). Метрика: **надёжность/wall-clock — fail-loud за 1 попытку вместо 3+backoff; волна не виснет до ~15мин** на мёртвом ключе. Гейт: узкий body-marker (`insufficient_quota`/`quota_exceeded`, +402) + drift-guard-тест (#12). Ev: opencode `packages/llm/src/route/executor.ts:242-245` + `schema/errors.ts:87-96`; goose `http_status.rs:181-249` (402→`CreditsExhausted`).
|
||
|
||
5. **[CONFIRMED] HTTP/2 keepalive на общем `http.Client` (без стриминга!).** На длинной thinking-тяжёлой правке НИ ОДНОГО байта тела не течёт, пока весь JSON не готов → прокси/LB может idle-закрыть сокет посреди генерации. Наш общий клиент (`httpllm.go:161-174`) собран **без keepalive-тюнинга**. HTTP/2 PING держит транспорт живым независимо от DATA-фреймов. Метрика: **надёжность — убирает класс mid-generation RST на длинных editor-вызовах**; каждый несостоявшийся reset = несостоявшийся ре-билл. Гейт: проверить, что фронты платных провайдеров не реджектят HTTP/2 PING (иначе HTTP/1.1 для них). Ev: grok-build `shared_http.rs:86-96` (http2 keepalive 15s/5s); openai-go `default_http_client.go:10-14` (ResponseHeaderTimeout).
|
||
|
||
6. **[VERIFIED, NEW] Gemini `limit: 0` в 429/499/503 = ЖЁСТКИЙ terminal, не транзиент.** Судья Gemini: 429, чьё сообщение содержит `limit: 0`, — проект с нулевой квотой, ретрай **никогда** не пройдёт (жжёт время). gemini-cli матчит `/limit:\s*0/` и делает terminal независимо от статуса. Метрика: **надёжность judge — не крутим 3 бесполезные попытки на нулевой квоте.** Ev: `gemini-cli/packages/core/src/utils/googleQuotaErrors.ts:238-249`.
|
||
|
||
7. **[VERIFIED, NEW] xAI: `x-should-retry:false` в ответе форсит FATAL поверх статуса.** Официальный grok-харнесс чтит header `x-should-retry` как авторитет над кодом: провайдер может пометить content-caused 5xx неретраебельным (экономит до `MaxAttempts−1` ПЛАТНЫХ попыток). Три реализации (openai-go, anthropic-sdk-go, grok-build) сходятся. Дёшево, безвредно при отсутствии header (наши OpenAI-compat ноги в основном его не шлют; xAI/OpenAI — шлют). Метрика: **$ / надёжность — до 2 платных ретраев сэкономлено на server-marked-fatal 5xx; no-op при отсутствии.** Ev: openai-go `internal/requestconfig/requestconfig.go:383-388`; anthropic-sdk-go `internal/requestconfig/requestconfig.go:262-267`; grok-build `retry.rs:188-197`.
|
||
|
||
8. **[VERIFIED, NEW] Локаль/ollama: HTTP 200 с ошибкой В ТЕЛЕ стрима.** ollama возвращает 200, даже когда стрим несёт `error`-поле — успех/провал судится по событию, не по коду. Наш local-адаптер/liveness (WSL2-стенд) должен парсить тело, не только статус. Метрика: **надёжность local-fallback — ложный «жив» при битой локали.** Ev: codex `codex-rs/ollama/src/client.rs:190-197,227-235`.
|
||
|
||
### Деньги / точность биллинга
|
||
|
||
9. **[PARTIAL→реально] xAI reasoning: identity-гард против латентного 30–44% overcount.** Наш `additive`-путь (`provider_openai.go:101-102`) добавляет `reasoning_tokens` **безусловно**. litellm фолдит их только когда `total == prompt+completion+reasoning` (иначе reasoning уже внутри completion). Сегодня xAI Chat Completions — additive-режим (наш верен), но гард делает нас **само-корректирующимися вместо «по слагу»**. Метрика: **xAI output-COGS overcount — 0% сегодня, но кап латентного ~30–44% двойного счёта** (та самая drift-величина, что vojo уже оплатил, наш `llm.go:42`), если xAI когда-нибудь свернёт reasoning в completion. Гейт: живая grok-usage-проба в `00-provider-quirks.md` (правило двух направлений). Ev: `litellm/litellm/llms/xai/chat/transformation.py:289-353`; grok-build `xai-chat-state/src/usage.rs:44-61` (читает reasoning как **subset** — но это ДРУГОЙ эндпоинт, xAI-native, не Chat Completions).
|
||
|
||
10. **[CONFIRMED] CostSource/Estimated-маркер на settle-estimate строках.** Честную половину (settl'им резерв-оценку, не $0) мы уже делаем (`stagerun.go:469-473`, `422-436`). Не хватает легенды телеметрии: goose несёт `CostSource::{ProviderReported,Estimated}`, crush ставит `EstimatedUsage=true`. Без флага estimated-строки неотличимы от реальных zero-token вызовов и косят token-based COGS-аналитику. Метрика: **целостность телеметрии — «доля estimated в спенде» становится queryable; никаких безмолвных null-token строк.** Дисциплина: estimated-токены **display-only**, НЕ кормят `CostUSD`. Ev: goose `conversation/token_usage.rs:17-22`; crush `internal/agent/agent.go:1896-1927`.
|
||
|
||
11. **[CONFIRMED] План: судья Gemini → нативный `generateContent`.** На OpenAI-compat-слое Gemini роняет thoughts-поле → мы деривим reasoning вычитанием (`total−prompt−completion`). Нативный эндпоинт отдаёт `usageMetadata.thoughtsTokenCount` первым классом **и структурный `finishReason`** вместо композитной строки `content_filter: PROHIBITED_CONTENT` — один переезд чинит и usage, и finish (пересечение с Q3). Метрика: **робастность reasoning-учёта Gemini** (нет derive-by-subtraction, который тихо даст 0, если слой перенесёт thinking в completion) + **корректность finish.** Гейт: за Ф2-нативным-Gemini (реальная транспорт-работа: auth/retry/cache_control); до него — additive_total + промежуточный inclusive-vs-additive identity-чек. Ev: litellm `vertex_and_google_ai_studio_gemini.py:1729-1745,1908-1944`; opencode `protocols/gemini.ts:338-361`.
|
||
|
||
### $ / cache (вопрос №1 — но импакт скромнее, чем звучит)
|
||
|
||
12. **[CONFIRMED] Держим Р5-раскладку; ГАРАНТИРУЕМ байт-идентичность константного префикса; НОВОЕ — cache_control на GLM.** Обзор подтверждает нашу раскладку с трёх сторон: (а) opencode инжектит cache-маркеры **только** для `anthropic-messages`/`bedrock-converse`, OpenAI/Gemini-implicit-cache **осознанно пропускает** (`cache-policy.ts:39-42`) — ровно наш сплит; (б) goose **физически переносит** волатильный блок (turn-context) в ХВОСТ, «иначе он инвалидирует message-level cached prefix (Anthropic хеширует tools→system→messages)» (`anthropic.rs:323-357`, CONFIRMED) — это и есть наша боль «инъекция банка бьёт префикс»: банк/STM **обязаны** идти строго после константного префикса; (в) litellm **срезает** cache_control для OpenAI-compat (`gpt_transformation.py:380-402`, дефолт-strip, с carve-out для кастом-base-гейтвеев). **Несущее уточнение:** `prompt_cache_key` (стабильный routing-ключ) есть у OpenAI-family/xAI/**Mistral**, но **НЕ у deepseek-native и НЕ у GLM** (litellm deepseek-dir: ноль ссылок; opencode эмитит ключ лишь для openai/azure/xai/mistral/deepinfra/cerebras) — значит для нашего draft (deepseek-v4-flash) и editor (glm-5) единственный рычаг — **литеральный префикс-автокэш**, и всё, что мы можем, — держать `system+brief+style-sheet+summary` байт-в-байт стабильным. **НОВОЕ (VERIFIED):** ZAI/GLM, в отличие от дефолтного OpenAI-compat-strip, **СОХРАНЯЕТ cache_control** (`litellm/litellm/llms/zai/chat/transformation.py:23-34`, «GLM supports cache_control — don't strip it») → большой стабильный билингв-префикс редактора (system/style/глоссарий) можно кэш-маркировать на GLM. Метрика: **вход-COGS редактора — cache-hit GLM $0.2 vs miss $1.0 (÷5) на входной доле**; НО вход ≪ выход (редактор доминирует выходом, `08-cost-model` §), поэтому реальная экономия **~2% стандарта** (как и предсказывал cost-model: «deepseek auto-cache ~2%, не рычаг»). Честно: Q1 — вопрос №1 по названию, но **низкий по $-импакту**; ценность — «мы уже правы» + один новый GLM-рычаг + фикс-гарантия стабильного префикса. Гейт: cache-цены/поведение GLM — вендор-сверка; mistral prompt_cache_key — тоже (см. Q7).
|
||
|
||
### Мелкое / отложенное (полнота)
|
||
|
||
13. **[CONFIRMED] `parseRetryAfter` — принять `Retry-After-Ms` и дробные секунды.** Наш `strconv.Atoi` (`httpllm.go:426`) режет любой не-integer → `Retry-After: 1.5`/`Retry-After-Ms` дают 0 и слепой exp-backoff. opencode/openai-go/anthropic-sdk-go все парсят ms первым. Метрика: **надёжность/латентность — чтит sub-second хинты вместо BackoffBase(500ms)+jitter**; чистый апсайд под нашим 5-мин капом. Ev: opencode `route/executor.ts:93-95`; openai-go `requestconfig.go:396-449`; anthropic-sdk-go `requestconfig.go:275-328`.
|
||
|
||
14. **[PARTIAL] Проба (не билд!): что DeepSeek/GLM/Mistral реально шлют на 429.** `obs.LogLLMExchange` уже пишет headers+body — проверить в пере-прогоне, есть ли `x-ratelimit-{limit,remaining,reset}`/body-retry-поле. opencode парсит эти окна, но тратит их на observability — рычаг «пейсить ДО 429» никто не построил. **Не строить пейсер сейчас** — решать по данным (правило двух направлений). Ev: opencode `route/executor.ts:112-148`; litellm `router.py:2823-2847` (pre-call RPM-чек внутри семафора — форма «пейсить до 429», если есть budget-header).
|
||
|
||
15. **[CONFIRMED] Держим rateGuard как есть; Mistral-48% = ТЮНИНГ `max_concurrency`, не алгоритм.** Весь обзор сошёлся: единственный другой проактивный лимитер — семафор litellm (`router.py:2823`), и он работает лишь потому, что у litellm есть sibling-деплойменты для роутинга вокруг 429 — чего у нас нет. Все прочие 10 — реактивны и влетели бы в наши 48%. **Умный backoff не чинит token-bucket/concurrency-cap тир** — только кап исходящей конкуренции ниже потолка тира. Метрика: **надёжность — mistral N-∥ retry-fail ~48%→~0–5%** при `max_concurrency` под потолком тира; ноль кода, ноль COGS. Гейт: per-tier калибровка; пере-замер при mistral-editor-арме. Ev: litellm `router.py:2823-2847`; goose `openai.rs:337-348` (Mistral — только max_tokens-remap, не rate).
|
||
|
||
16. **[PARTIAL, GATED] HTTP/1.1-escape на последней попытке — диагностика тихого усечения.** grok-build построил `send_with_retry_escaping_pool` (`xai-grok-http/src/lib.rs:432-475`) ровно чтобы уйти с «отравленного» HTTP/2-пула, и их truncation-регрессия — в том же модуле → **правдоподобная причина** нашего тихого усечения на 2–8k. Но корень НЕ подтверждён (может быть finish=length, который мы уже лечим) → CLAUDE.md запрещает гадать: **гейтить как эксперимент с замером h2-vs-forced-h1**, не лендить дефолтом. Метрика: truncation-rate 2–8k (baseline неизвестен — потенциально крупнейшая тихая утечка качества). Ev: grok-build `retry.rs:231-246`.
|
||
|
||
17. **[PARTIAL, DEFER] assistant-prefill continuation на finish=length.** Единственный механизм ЧАСТИЧНОГО восстановления во всех 11 (aider `base_coder.py:1492-1521`): вместо ре-рана всего вызова — дописать усечённый dst-блок как assistant-prefix и до-вызвать со свежим max_tokens. Наш finish=length сейчас ре-ранит весь запрос (`escalation.go:17-35`), пере-биллит уже сгенерированный префикс. Это **правильный ответ на «нужен стриминг для truncation» — не нужен, нужен continuation** (стриминг его НЕ даёт). Метрика: **$ — экономит ре-билл уже-эмитнутого префикса (~40–60% editor-генерации) на событие усечения.** Гейт: **HIGH-риск для детерминизма** (мульти-хоп, golden/snapshot; наш src→dst-блок наивный сплайс может побить; Gemini/GLM prefix-continuation-семантика различна) → per-model `supports_assistant_prefill` + адверсариальный golden-ревью; **скорее DEFER**.
|
||
|
||
18. **[PARTIAL] Прочее мелкое, чистый апсайд/тесты:** (а) proportional jitter вместо fixed `rand(250ms)` (`httpllm.go:144`) — де-синк N-∥ на mistral (gemini-cli ±30%, openai-go −25%); (б) drift-guard тест, пиннящий status→retryable-мапу и finish-парсер как **префикс/substring**-матч (чтобы композит `content_filter: PROHIBITED_CONTENT` классифицировался) — аналог aider `_load`, который на импорте валит, если litellm `*Error` не расклассифицирован (`aider/exceptions.py:74-76`); (в) кап BilledDecodeError-ре-биллов ниже MaxAttempts (grok-build держит Serialization non-retryable, с регрессией «laundering в retryable дал full-budget шторм»).
|
||
|
||
**Явные REJECT (что НЕ берём — с причиной):**
|
||
- **Не берём** авто-своп модели на 429 (gemini-cli `action:'silent'`) — противоположно нашему fail-loud + ломает детерминизм; gemini-cli #23889 показал баг «сообщение дублируется 30–50×» на pro→flash-фолбэке.
|
||
- **Не берём** 1h/uncapped honored-Retry-After (goose 3600s clamp; openai-go/anthropic-sdk-go uncapped) — виснет платная волна под USD-резервацией; наш 5-мин кап верен.
|
||
- **Не берём** no-jitter multiplicative backoff (aider) — thundering-herd под N-∥ волнами.
|
||
- **Не берём** `retry_429:false` (codex) — каждый tier-blip станет hard-fail в середине книги.
|
||
- **Не берём** идемпотентность-ключ: обзор отвечает на вопрос «у кого кроме OpenAI» — **НИ У КОГО не шлётся** (даже codex не шлёт OpenAI-у). Держим не-шлём.
|
||
- **Не берём** streaming ради «снижения обрывов/ре-биллов»: **ни один харнесс не резюмит частичную генерацию** — все ре-ранят весь запрос (codex full-history, gemini-cli full-turn, goose full-turn, opencode non-retryable break, anthropic no-resume). Стриминг покупает **ДЕТЕКЦИЮ**, не resume.
|
||
- **Не берём** брутальный body-substring/regex-стиль классификации ошибок (aider ~30-regex батарея) — та самая «exact matchers dead» хрупкость, что мы уже флагаем.
|
||
|
||
---
|
||
|
||
## §2. Q7 — таблица квирк-кандидатов
|
||
|
||
Все — **кандидаты**, абсорбция после вендор-сверки (правило 10.07). Статус: **NEW** / **sharpens** (уточняет реестр) / **tracked** (только корроборация). «Verified» = адверсариально ре-прочитан в коде. Жатва трекеров помечена `#issue`+дата.
|
||
|
||
### Прямо задевает пере-прогонные армы (glm / mistral / deepseek-pro)
|
||
|
||
| Провайдер | Симптом | Статус | Ev (code / issue+дата) |
|
||
|---|---|---|---|
|
||
| **mistral** | Строгая `extra_forbidden`: реплей output-only `reasoning_content`/`thinking_blocks` или `null`-value сообщений → **400**; стрипать assistant-поля. | NEW·verified | `litellm mistral/chat/transformation.py:392-405` |
|
||
| **glm-zai** | Молча **принимает context-overflow** (нет 4xx) → тихо усекает вход. | NEW | `opencode packages/opencode/src/provider/error.ts:30-31` |
|
||
| **glm-zai** | finish `sensitive`→content_filter, `network_error`→stop (вне OpenAI-словаря; `network_error`→stop = риск тихого провала). | NEW·verified | `litellm core_helpers.py:120-122` |
|
||
| **glm-zai** | **СОХРАНЯЕТ cache_control** (в отличие от дефолт-strip OpenAI-compat) — COGS-рычаг для билингв-редактора. | NEW·verified | `litellm zai/chat/transformation.py:23-34` |
|
||
| **glm-zai** | Disable-thinking **эндпоинт-зависим**: native z.ai `thinking:{type:disabled}` vs routed OpenAI-compat `reasoning:{enabled:false}`. | sharpens·verified | `cline .../routing/glm-thinking.ts:31-58` |
|
||
| **glm-zai** | Множественные base URL: OpenAI-compat `/api/paas/v4` И Anthropic-Messages `/api/anthropic` (goose гоняет thinking через второй). | NEW | `goose .../declarative/definitions/zai.json:3,13,20` |
|
||
| **deepseek** | Thinking-ON **multi-turn**: 422 «reasoning_content must be passed back» — эхать reasoning_content на каждом assistant-сообщении. | NEW·verified | `litellm deepseek/chat/transformation.py:62-100` (issue #28045); litellm#28461 (2026-05-21), #26395 (2026-04-24) |
|
||
| **deepseek** | v4-flash thinking-ON отдаёт `"reasoning": null` вместо `reasoning_content` — нестабильность имени поля. | NEW | litellm#28461 (2026-05-21) |
|
||
| **mistral** | Отвергает `max_completion_tokens`, требует legacy `max_tokens` (мы уже шлём max_tokens). | tracked·verified | `goose openai.rs:335-348` |
|
||
| **mistral** | `seed` не под тем именем — детерминизм через `extra_body.random_seed`. | NEW·verified | `litellm mistral/chat/transformation.py:179-180` |
|
||
| **mistral** | Поддерживает prompt-caching через `prompt_cache_key` (стабильный id). | NEW | `opencode patches/@ai-sdk%2Fmistral@3.0.51.patch:80-81` |
|
||
| **deepseek** | base по умолчанию расходится: litellm `/beta`, crush+наш реестр `/v1` (у /beta — устаревший комментарий рядом = ровно наша «code-language» ловушка). | NEW·verified | `litellm deepseek/chat/transformation.py:261,278` |
|
||
|
||
### xAI / Gemini / Kimi (судья, канал B, кандидаты-редакторы)
|
||
|
||
| Провайдер | Симптом | Статус | Ev |
|
||
|---|---|---|---|
|
||
| **xai-grok** | reasoning-учёт **эндпоинт-split**: Chat Completions = ADDITIVE (`total=prompt+completion+reasoning`), Responses/native = subset. Наш путь — Chat → additive верен. | sharpens·verified | `litellm xai/chat/transformation.py:289-353`; grok-build `xai-chat-state/usage.rs:44-61` (subset) |
|
||
| **xai-grok** | `x-should-retry:false` в ответе → FATAL поверх статуса (даже 429/5xx). | NEW·verified | `grok-build client.rs:207-227 + retry.rs:176-190` |
|
||
| **xai-grok** | HTTP/2-пул «отравляется» (тихие LB/Cloudflare/GOAWAY-дропы) → ретрай на том же коннекте фейлит одинаково; лечат HTTP/1.1-rebuild. | NEW | `grok-build retry.rs:231-246; xai-grok-http/lib.rs:432-475` |
|
||
| **xai-grok** | Slug-gated param-дропы: grok-3-mini/grok-4/grok-code-fast отвергают `stop`; grok-4/code-fast — `frequency_penalty`. | NEW·verified | `litellm xai/chat/transformation.py:150-169` |
|
||
| **xai-grok** | Пустая строка finish_reason на tool-call (вместо `tool_calls`); чинят при наличии tool_calls. | NEW·verified | `litellm xai/chat/transformation.py:223-232` |
|
||
| **xai-grok** | Давится message-level `name`-полем (стрипать, issue #9720). | NEW·verified | `litellm xai/chat/transformation.py:207-221` |
|
||
| **xai-grok** | `num_sources_used` (Live Search) = **$0.025/источник** ($25/1000); датированная семантика к реестровому «игнорировать нестандартные cost-поля». | sharpens | `litellm xai/cost_calculator.py:55-84` |
|
||
| **gemini** | `candidatesTokenCount` **включает thinking НЕ всегда** — варьирует per-response; litellm детектит динамически, иначе ±учёт. | sharpens·verified | `litellm vertex_and_google_ai_studio_gemini.py:1729-1745` (PR#10141) |
|
||
| **gemini** | `MALFORMED_FUNCTION_CALL`: litellm→`stop`, opencode→`error` (харнессы РАСХОДЯТСЯ). Маппинг unknown→`stop` = **риск тихого провала**. | NEW·verified | `litellm core_helpers.py:100-124` |
|
||
| **gemini** | 2.5 «minimal» reasoning не достигает нуля: per-model floor (pro=128, flash=1, flash-lite=512); 3.x — `thinkingLevel`-enum, полностью не отключить. | sharpens | `litellm .../gemini.py:823-838,867-913`; constants.py:144-151 |
|
||
| **gemini** | 3.x требует/предпочитает `temperature:1.0` (харнесс инжектит, если не задано). | NEW | `litellm .../gemini.py:1238-1241` |
|
||
| **gemini** | `limit: 0` в 429/499/503 = terminal, не транзиент. | NEW | `gemini-cli utils/googleQuotaErrors.ts:238-249` |
|
||
| **kimi** | Reasoning-kimi (k2.5/k2.6): корректно **ОМИТить** temperature (не форсить 1). Наш K2.6 — reasoning → кандидат сменить `force:1`→omit. | sharpens·verified | `litellm moonshot/chat/transformation.py:131-146`; cline#10544 (2026-05-04, `400 invalid temperature: only 1 is allowed`) |
|
||
| **kimi** | Multi-turn tool-call: reasoning_content обязателен на каждом assistant-tool-сообщении (litellm вставляет `' '`). | NEW | `litellm moonshot/chat/transformation.py:148-192` (issue #23765) |
|
||
| **kimi** | reasoning-OFF = `thinking:{type:disabled}` (как GLM); k2.6 — `thinking.type`. | NEW | `cline .../routing/provider-option-rules.ts:344-379` |
|
||
| **kimi** | Дефолтный host расходится: goose `api.moonshot.cn`, litellm/наш реестр `api.moonshot.ai` (intl). | tracked | `goose .../moonshot.json:13-14` |
|
||
|
||
### Инфра / прочее
|
||
|
||
| Провайдер | Симптом | Статус | Ev |
|
||
|---|---|---|---|
|
||
| **ollama (local)** | HTTP 200 с `error` в теле стрима — судить по событию, не коду (наш local-liveness). | NEW·verified | `codex codex-rs/ollama/src/client.rs:190-197,227-235` |
|
||
| **glm-zai** | crush health-probe special-case: для ZAI «connected» = любой статус кроме 401 (наш pre-run live-probe: для GLM 401/403=down, прочее=up). | NEW | `crush internal/config/config.go:937` |
|
||
| **deepseek** | content нельзя слать OpenAI list/blocks — только плоская строка (мы шлём строки — не баг сейчас). | NEW·verified | `litellm deepseek/chat/transformation.py:115-125` |
|
||
| **deepseek** | cache-поля `prompt_cache_hit_tokens`/`_miss_tokens` (hit+miss==prompt). | tracked·verified | `litellm types/utils.py:1648-1653` (реестр стр.66) |
|
||
|
||
**Дубликаты онлайн-жатвы (корроборация, не новьё):** Kimi temp=1 повторяется (K2.5 тоже) — cline#10544; deepseek multi-turn 400 — claude-code#68995 (2026-06-17); mandatory-thinking-эндпоинты реджектят disable — claude-code#65863 (2026-06-06), #69379 (2026-06-18); Gemini quota-429-фолбэк баг-склонен — gemini-cli#26002/#23889/#9248/#4646; xai reasoning_effort slug-специфичен — litellm#16204 (2025-11-03); Mistral empty-content → None — litellm#12197 (2025-07-01); Kimi K3 существует — litellm#33921 (2026-07-19, ре-чек слага к квартальному).
|
||
|
||
---
|
||
|
||
## §3. Per-question разборы
|
||
|
||
### Q1 — Prompt-cache дисциплина ($) · [Q1/Q3-синтез пере-выведен вручную + верифицирован]
|
||
|
||
**Наше сейчас.** Р5: стабильный префикс (system+brief+style-sheet+summary) несёт `CacheBoundary`; волатильный хвост (глоссарий+STM+чанк). Anthropic-адаптер маппит boundary→`cache_control{ephemeral,ttl}` (≤4), суммирует input+cache_read+cache_creation в PromptTokens; OpenAI-compat игнорят флаг, автокэш по префиксу; `cacheRead()` читает оба варианта DeepSeek-полей.
|
||
|
||
**Cross-repo (все high-confidence).** Три семейства: (1) **Anthropic-маркерное** (grok-build 1 ephemeral на last-system; aider — блок-порядок + маркеры на стабильных блоках + warming-ping каждые `5*60−5`с под 5-мин TTL, `base_coder.py:1348/1340-1373` CONFIRMED; cline — last-user-текст + фейковый `' '`-парт чтобы маркер не схлопнулся; opencode — 4-breakpoint budget в invalidation-порядке tools→system→messages `anthropic-messages.ts:511-538`; litellm — config-driven `cache_control_injection_points`; anthropic-sdk-go — per-block ephemeral 5m/1h). (2) **OpenAI-family — только ключ:** codex/opencode/openai-go `prompt_cache_key` (стабильный routing-ключ, заменяет `user`), стрип cache_control (litellm `gpt_transformation.py:380-402`, дефолт-strip с carve-out для кастом-base). (3) **Gemini — контекстный кэш вне промпта:** litellm требует **один непрерывный блок** (`vertex_ai/context_caching/transformation.py:21-48`, CONFIRMED); gemini-cli explicit-кэш вообще не создаёт.
|
||
|
||
**Ключевые уточнения (VERIFIED свежими агентами):** goose **физически переносит** волатильный turn-context в хвост, «иначе инвалидирует cached prefix» (`anthropic.rs:323-357`) — прямой ответ нашей боли; `prompt_cache_key` работает у OpenAI-family/xAI/Mistral, но **НЕ deepseek-native/GLM**; GLM **сохраняет** cache_control (`zai/chat/transformation.py:23-34`).
|
||
|
||
**Берём/не берём.** Берём: (1) держать константный префикс байт-идентичным, банк/STM строго в хвост — рек #12; (2) cache_control-маркировка на GLM — новый рычаг; (3) для Gemini-explicit-кэша (если Ф2) — cached-блок должен быть непрерывным. Не берём: `prompt_cache_key` для draft/editor (deepseek/glm его не чтут). **$-импакт скромен (~2% стандарта)** — вход ≪ выход редактора; Q1 «вопрос №1» по важности вопроса, не по деньгам.
|
||
|
||
### Q2 — Rate-limit адаптив
|
||
|
||
**Наше сейчас.** `retryLoop` (exp backoff cap 30s, honored-Retry-After cap 5min, +rand(250ms), MaxAttempts 3) + отдельная ось: per-model `rateGuard` (семафор + min_interval, wire-neutral, не snapshot-folded). `parseRetryAfter` — только integer-header. Не читаем `x-ratelimit-*`, нет AIMD/cooldown. Mistral ~48% N-∥.
|
||
|
||
**Cross-repo.** Единственный другой проактивный лимитер — litellm семафор с pre-call RPM (`router.py:2823-2847`), работает лишь из-за sibling-деплойментов. opencode парсит retry-after-ms + `x-ratelimit-*`/`anthropic-ratelimit-*`, но тратит на observability. goose предпочитает body `retry_after_seconds` над header. gemini-cli — server-delay как **floor** backoff. codex Retry-After-header **не парсит вовсе** (только regex по тексту ошибки), `retry_429:false`. grok кап Retry-After 120s + hard-cap 429-ретраев=2.
|
||
|
||
**Берём/не берём.** Берём: fractional/ms Retry-After (#13), 429-header-проба (#14), body/message Retry-After fallback за vendor-гейтом (#18), опц. model-cooldown (defer). Держим: rateGuard + Mistral=тюнинг-кап (#15). Не берём: silent model-swap, 1h Retry-After, no-jitter backoff, retry_429:false. **Вывод: против Mistral-48% ни один харнесс не даёт алгоритма — только concurrency-cap работает.**
|
||
|
||
### Q3 — finish_reason таксономия · [пере-выведен вручную + верифицирован]
|
||
|
||
**Наше сейчас.** Нейтральные stop/length/content_filter/refusal; контракт finish=stop-only; length→ретрай с бо́льшим max_tokens (+ min_max_tokens-флор); Gemini композит `content_filter: PROHIBITED_CONTENT` (exact-матчеры уже мертвы).
|
||
|
||
**Cross-repo (все high).** litellm — единый статический `_FINISH_REASON_MAP`, **unmapped → warn + `stop`** (CONFIRMED; `MALFORMED_FUNCTION_CALL`→stop) — **риск тихого провала** (усечённую/битую/дегенеративную генерацию выдаёт за чистый stop). opencode — closed 6-value enum + **структурная детекция усечения** (`route/client.ts:382-391`, CONFIRMED: стрим без терминального finish-события → hard error). codex — Responses-события (`response.completed`/`.incomplete`; ранний close = ошибка). grok-build — 4-value StopReason + **server-side doom-loop-детектор** (`x-grok-doom-loop-check`; `DoomLoopDetected`→retry ≤250ms; CONFIRMED, «confident» = thinking-канал tail_repetition≤8) — релевантно нашей эхо/дегенеративной петле (research/15). cline — **reasoningFloor = budget+1024-reserve** (`gateway.ts:120-181`, CONFIRMED) — кросс-валидирует наш min_max_tokens. goose — **ОМИТит max_tokens** при unset (противоположно нашему флору; `openai.rs:1459-1474`, CONFIRMED). anthropic-sdk-go — богаче: +`model_context_window_exceeded`, `pause_turn`, `refusal`.
|
||
|
||
**Берём/не берём.** Держим: finish=stop-only + min_max_tokens-флор (cline подтверждает reserve-логику; наш флор — верный выбор для thinking-моделей, в отличие от goose-омита). Берём: finish-матчер как префикс/substring (drift-guard #18); знать GLM `sensitive`/`network_error` (§2). **Не берём** litellm-стиль «unknown→stop» — это ровно то, против чего наш строгий контракт + echo/coverage-гейты. Кандидат-заметка: xAI doom-loop-header как ранний сигнал дегенеративной петли (за vendor-сверкой). Структурная детекция усечения (opencode/codex) достижима **только со стримингом** → см. Q6.
|
||
|
||
### Q4 — Таксономия ошибок retry/fatal
|
||
|
||
**Наше сейчас.** Единый `retryLoop`: 429&≥500→retryable; прочие non-2xx→terminal fail-loud; network/timeout→retryable (unless ctx done); 2xx-undecodable→BilledDecodeError (retryable unless >16MiB); 2xx-empty→billed success. Идемпотентность — не шлём. Anthropic 529 ловится тем же ≥500.
|
||
|
||
**Cross-repo.** opencode — tagged-union, QuotaExceeded(429+body)=fatal, context-overflow под-классификация. goose — централизованная таблица (402→CreditsExhausted, 400/413→ContextLengthExceeded). openai-go/anthropic-sdk-go — **x-should-retry** override + body-rewind; retry 408/409/429/≥500. gemini-cli — network-code allowlist (undici timeouts), maxAttempts 10. aider — declarative EXCEPTIONS-таблица по классу litellm-исключения + `_load` drift-guard. codex — two-tier (transport 429/5xx/transport + семантик is_retryable whitelist; ServerOverloaded=terminal). litellm — Retry-After чтит только 0<x≤60s. crush/cline — делегируют SDK.
|
||
|
||
**Берём/не берём.** Берём: quota-429→terminal (#4), context-overflow terminal + GLM-precheck (#2), x-should-retry (#7), drift-guard-тест (#12/#18), кап billed-decode-ре-биллов, proportional jitter. Держим: network/timeout retryable, MaxAttempts 3, honored-Retry-After 5-мин-кап (не 60s litellm, не uncapped SDK), не-шлём идемпотентность, 529-в-≥500. Не берём: opencode non-retryable network/timeout, gemini maxAttempts 10, aider retry ContentPolicyViolation (наш 18+: детерминированный отказ, ретрай жжёт деньги), brittle-regex-стиль, 408/409.
|
||
|
||
### Q5 — Usage-учёт
|
||
|
||
**Наше сейчас.** Neutral Usage: PromptTokens(total), CachedTokens (оба DeepSeek-варианта), CacheCreationTokens, CompletionTokens, ReasoningTokens (subset/additive/additive_total). Anthropic суммирует 3 части. Served-model биллинг (`PriceForResponse(requested,served)`). Double-count-гард (uncached=prompt−cached−write≥0). Settl'им резерв-оценку на paid-2xx-zero-usage (не $0).
|
||
|
||
**Cross-repo.** litellm — per-response reasoning-reconciliation с identity-гардами. grok-build (офиц.) — биллит по SERVER cost-полю (`cost_in_usd_ticks`), reasoning отдельно/informational, `cost_missing_calls`-счётчик. opencode — dual (inclusive + non-overlapping breakdown, undefined-not-zero). goose/crush/aider/cline — usage-estimator fallback + CostSource-флаги; crush читает OpenRouter `Usage.Cost` override. codex/gemini-cli — served-model через header/modelVersion.
|
||
|
||
**Берём/не берём.** Берём: xAI identity-гард (#9), CostSource-маркер (#10), план native-Gemini thoughtsTokenCount (#11), опц. server-cost-поле xAI (проба). Держим (мы ведём): served-model биллинг, double-count-гард, cache-fold, честный settle-estimate (не фабрикуем токен-математику как goose-estimator). Не берём: codex single-vendor usage-shape (мис-букнет xAI/Gemini), nested-only DeepSeek-cache (потеряет hit-скидку), aider DeepSeek-fallback (вычитает ЦЕНУ из ТОКЕНОВ — живой мис-бук), streaming-usage-плюмбинг (мы non-streaming). Defer: split cache-creation 5m/1h (только если Anthropic вернётся / mixed-TTL).
|
||
|
||
### Q6 — Streaming vs non-streaming надёжность
|
||
|
||
**Наше сейчас.** Non-streaming ONLY (`StreamingLLMClient` отложен). Один attempt под `context.WithTimeout(attempt_s, дефолт 300; 240 конфиг)` покрывает connect+headers+body одним бюджетом. finish=length→ре-ран с бо́льшим max_tokens. Нет keepalive/idle-timeout/first-byte-timeout.
|
||
|
||
**Cross-repo.** codex/opencode/cline/crush/grok — stream-only (buffered-caller дренит SSE); goose/gemini-cli/aider/litellm — оба. **Ключевые примитивы, которые даёт только стриминг:** per-event **idle-timeout, сбрасываемый на каждом событии** (codex `responses.rs:503-529`); composed header+chunk+total через AbortSignal.any (opencode); **content-progress timer** (`last_content_chunk_at`, grok — ловит thinking-stall/эхо-мину рано); **stream-closed-before-finish = error** (opencode/codex). anthropic-sdk-go **HARD-отказывает** длинный non-streaming (`CalculateNonStreamingTimeout`: если `expectedTime=1h·maxTokens/128000 > 10min` → «streaming required»). openai-go ResponseHeaderTimeout=10min (только time-to-headers). Единственное частичное восстановление во всех 11 — **assistant-prefill continuation** (aider), и оно на NON-streaming пути.
|
||
|
||
**Критический нюанс (обосновывает вердикт).** idle/stall-timeout, который все ценят, существует **только со стримингом**: на non-streaming пути нет inter-chunk-разрывов, сервер держит headers до конца генерации → любой «first-byte/idle» коллапсирует в total attempt_s. Стриминг покупает **ДЕТЕКЦИЮ** (ранний catch зависа + stream-closed-signal + keepalive), **никогда автоматический RESUME**. По собственной математике Anthropic 2–8k выход ждёт ~2–4 мин, а её жёсткий non-streaming кап (Opus 8192) ≥ всего нашего диапазона → `attempt_s=240` внутри сертифицированного вендором безопасного non-streaming конверта.
|
||
|
||
**Берём/не берём.** Держим non-streaming дефолтом + `StreamingLLMClient`-отсрочку, но с **явным trip-wire**: стриминг ТОЛЬКО для длинного editor-арма, ТОЛЬКО если тихое усечение сохранится на верху 2–8k на вебновелл-срезе, и **buffered** (fold SSE→полный LLMResponse, opencode-паттерн — сигнатура `Complete()`, golden/детерминизм, все гейты байт-идентичны). Если trip-wire сработает — idle/stall-timeout как ЕДИНСТВЕННЫЙ новый механизм. Берём СЕЙЧАС (без стриминга): HTTP/2 keepalive (#5), assistant-prefill continuation-оценка (#17, defer). Не берём: streaming-only архитектуру (жрёт usage-fidelity — aider падает на локальные ESTIMATE), idle-timeout в текущий non-streaming (коллапсирует в total), reclass empty-2xx как retryable (double-bill), стриминг ради снижения обрывов (никто не резюмит).
|
||
|
||
---
|
||
|
||
## §4. Appendix — per-repo (что смотрели / что пропустили)
|
||
|
||
- **codex** (Rust, Apache-2.0). Смотрели: `core/src/{client,client_common,responses_retry,util}.rs`, `codex-api/src/{sse/responses.rs,api_bridge.rs}`, `codex-client/src/retry.rs`, `model-provider-info/src/lib.rs`, `ollama/src/client.rs`, `protocol/src/error.rs`. Пропустили: TUI/apply-patch/cloud-tasks (не транспорт). Особенность: Responses-API-only, стрим-only, Retry-After-header не парсит.
|
||
- **gemini-cli** (TS, Apache-2.0). Смотрели: `core/src/core/{geminiChat,contentGenerator,loggingContentGenerator}.ts`, `utils/{retry,googleQuotaErrors}.ts`, `agent/event-translator.ts`, `utils/historyHardening.ts`. Пропустили: CLI/devtools/MCP. Особенность: model-fallback на 429 (баг-склонен), `limit:0`=terminal.
|
||
- **grok-build** (Rust, Apache-2.0, **офиц. xAI**). Смотрели: `xai-grok-sampler/src/{client,retry,shared_http, actor/request_task, stream/chat_completions}.rs`, `xai-grok-http/src/lib.rs`, `xai-grok-sampling-types/src/{conversation,doom_loop}.rs`, `xai-chat-state/src/usage.rs`. Особенность: doom-loop-детектор, HTTP/2-pool-escape, reasoning subset НА ЭТОМ (native) эндпоинте. Наиболее авторитетно по xAI-квиркам.
|
||
- **goose** (Rust, Apache-2.0). Смотрели: `goose-providers/src/{openai,openai_compatible,google,http_status}.rs`, `goose-provider-types/src/{retry,formats/{openai,anthropic,google},conversation/token_usage}.rs`, `goose/src/providers/usage_estimator.rs`, `declarative/definitions/*.json`. **Пропустили как ложный след:** `goose/src/tracing/rate_limiter.rs` — пейсит OTLP-телеметрию, НЕ LLM (проверено вручную). Особенность: turn-context-релокейшн, declarative-провайдеры.
|
||
- **aider** (Python, Apache-2.0). Смотрели: `aider/{llm,sendchat,models,exceptions}.py`, `coders/{base_coder,chat_chunks}.py`. Особенность: обёртка над litellm; assistant-prefill continuation; warming-pings; `_load` drift-guard.
|
||
- **cline** (TS, Apache-2.0). Смотрели: `sdk/packages/llms/src/providers/{ai-sdk,gateway, routing/*}.ts`, `sdk/packages/shared/src/agent.ts`, `.../vendors/types.ts`. (Провайдер-хендлеры живут в `sdk/packages/llms`, не в `apps/vscode/src/core/api` — там только `index.ts`.) Особенность: reasoningFloor, per-TTL cache-split, GLM/kimi thinking-routing.
|
||
- **opencode** (TS, MIT). Смотрели: `packages/llm/src/{cache-policy,route/{client,executor},protocols/*,schema/*,provider-error}.ts`, `packages/opencode/src/provider/{transform,error,provider}.ts`, `packages/core/src/util/retry.ts`. Особенность: compile-time cache-policy, структурная truncation-детекция, tagged-union ошибки.
|
||
- **litellm** (Python, MIT-ядро; **`enterprise/` НЕ читалась**). Смотрели: `litellm/{main,router,utils,cost_calculator,exceptions}.py`, `litellm_core_utils/{core_helpers,completion_timeout}.py`, `types/utils.py`, `llms/{deepseek,mistral,xai,moonshot,zai,openai,vertex_ai}/**`. Самый богатый провайдер-квирк-слой (Q3/Q4/Q5/Q7).
|
||
- **crush** (Go, **FSL-1.1 — read-only, не копировали**). Смотрели: `internal/agent/{agent,usage_fallback,loop_detection,errors}.go`, `internal/config/{provider,config}.go`, `internal/app/provider.go`. **Пропустили (внешнее):** реальный wire-транспорт — в `charm.land/fantasy`/`catwalk` (не в репо) + `openai/openai-go/v3`. Особенность: OpenRouter `Usage.Cost` override, EstimatedUsage-флаг, ZAI-probe-special-case.
|
||
- **openai-go** (Go, Apache-2.0, референс). Смотрели: `client.go`, `default_http_client.go`, `option/{requestoption,middleware}.go`, `internal/requestconfig/requestconfig.go`, `internal/apierror/apierror.go`, `chatcompletion.go`. Референс: Retry-After-Ms, x-should-retry, proportional jitter, idempotency-key, PromptCacheKey.
|
||
- **anthropic-sdk-go** (Go, MIT, референс). Смотрели: `client.go`, `default_http_client.go`, `option/{requestoption,middleware}.go`, `internal/requestconfig/requestconfig.go`, `message.go`, `shared/constant/constants.go`. Референс: 529, x-should-retry, CalculateNonStreamingTimeout, per-TTL cache-split, model_context_window_exceeded.
|
||
|
||
---
|
||
|
||
## §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 — не стриминг) никто не даёт.
|
||
|
||
**Самое ценное — Q7-квирки, задевающие пере-прогонные армы (все кандидаты, за вами вендор-сверка):** (1) **Mistral extra_forbidden** — mistral-editor-арм упадёт 400 при реплее reasoning-полей; (2) **GLM молча глотает context-overflow** — тихое усечение банк-хвоста на glm-5 (сломанный телефон); (3) **GLM finish `sensitive`/`network_error`**; (4) **GLM СОХРАНЯЕТ cache_control** (COGS-рычаг); (5) **kimi reasoning → ОМИТить temperature**, не force:1 (кандидат сменить нашу capability).
|
||
|
||
**Дёшево-и-подтверждено к внедрению (после пере-прогона):** 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). Ещё один датапоинт, что «схема-валидно» ≠ «содержательно».
|