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(sentinelErrStreamTTFBTimeout,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;switchTargetfallback-цепочки больше не ходит по целям на 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останавливает цепочку, текст пользователю (ручки, без джаргона, производный кап).