1.7.2

Fixed (#400)

  • Stream idle timeout больше не режет легитимно долгий TTFB reasoning-моделей и не ретраит обречённые попытки (#400; прод-кейс 03.09: агент №1, glm-5.3 через litellm, промпт ~32k токенов — TTFB 66–128s, 5 подряд idle-таймаутов по ~90–120s, ~16 минут без результата и без фидбека, write_file так и не вызван). Бюджеты разделены: межчанковый idle — из настройки stream_idle_timeout_seconds; бюджет до первого токена — адаптивный base + 2мс × eval-токены промпта (32k → +64s), потолок 10 минут (= Max настройки). Формула и оценка токенов (EstimatePromptTokens, зеркалит agent.EstimateTokens включая JSON-аргументы tool-calls + константа на tool-def) живут в одном месте internal/providers/stream_budget.go и применяются тремя guard-точками синхронно: провайдер openai-compat (было 90s hardcoded — настройка не работала вовсе), retry-коллектор collectStream (было 120s hardcoded), AgentRunner.runStream (единственный, кто читал настройку). Значение настройки теперь прокидывается в запрос (ChatRequest.StreamIdleTimeout) и клампится общим ResolveStreamIdleTimeout (Max настройки GetDuration не проверяет). До-первого-токена таймаут — типизированная ошибка TTFBTimeoutError (sentinel ErrStreamTTFBTimeout, errors.Is/As): retry-классификация считает её нетранзиентной (сабстрочный матч «timeout» больше не крутит идентичный doomed-запрос бесконечно), fallback-провайдер не гуляет по цепочке моделей (N× капа — до ~30 мин — на том же промпте), а пользователь получает в чат человекочитаемое сообщение с ручками (поднять stream_idle_timeout_seconds / сократить контекст / снизить thinking level) вместо сырой строки. Описание настройки дописано. Межчанковый idle сохраняет прежнюю транзиентную семантику (сеть моргнула посреди генерации — ретрай осмыслен).

Review (#400)

  • ocr review — 5 раундов, 16 → 13 → 3 → 4 → 8 находок, все разобраны. Исправлены: оценка токенов не учитывала JSON-аргументы tool-calls (доминирующая стоимость в ReAct-ходе) + fallback-константа и WARN при не-маршаллируемых аргументах; underflow/overflow negative-токенов в формуле бюджета (таймер с отрицательной длительностью сработал бы мгновенно); верхний кламп ResolveStreamIdleTimeout (раннер и провайдер не могли разойтись при значении > 600s мимо UI-валидатора); связка Max настройки ↔ StreamTTFBMaxBudget прибита тестом TestStreamIdleTimeoutSetting_CapMatchesTTFBBudgetCap; switchTarget fallback-цепочки больше не ходит по целям на TTFB-таймауте; сообщение в чат строится из типизированного бюджета (джаргон/строки разработчика не утекают, «10 минут» — производная от константы, тест прибивает); ctx-guard на отправку error-событий в стрим-горутине; json:"-" на новом поле. Ложные/стилевые отклонены с обоснованием: «data race на ttfbRecorded» — чтения после <-doneCh, go test -race чист; предложение кэшировать оценку токенов — стоимость трёх проходов по рунам ничтожна против сетевого вызова, кэш в value-типе ChatRequest усложнил бы ретраи. Проверки: go test ./... -count=1, go test -race ./internal/providers/..., golangci-lint run (0), go vet, make build-cross (6 платформ) — зелёные; фронт не менялся. Тесты: формула бюджета (таблица, границы 300k/кап/negative/overflow), ResolveStreamIdleTimeout (клампы), TTFBTimeoutError (Is/As/обёртки), EstimatePromptTokens (монотонность, tool-defs, marshalled-аргументы), collectStream TTFB-фаза vs межчанковая (sentinel только до первого события, масштабирование промпта), isTransientGoError (TTFB нетранзиентна, обычные timeout/connection транзиентны), persistent-retry не ретраит TTFB-таймаут (1 вызов), switchTarget останавливает цепочку, текст пользователю (ручки, без джаргона, производный кап).