Половина второго. Я сижу под рабочим столом, потому что розетка там, а удлинитель короткий; столешница у меня теперь вместо потолка, до неё сантиметров шестьдесят, если выпрямиться. Пахнет пылью и нагретым пластиком блока питания, свет сюда не достаёт, и телефон я держу почти у лица. Рядом с коленом стоит кружка, чай в ней остыл и взялся плёнкой; вторая кружка — тут же, пустая. Наверху, на столе, лежит распечатка с цифрой, вокруг которой весь этот разговор: тысяча пятьсот.
Что видела клиентка
Вчера владелец прислал жалобу на агента «что поправить» в Видеопродакшне — того, который переписывает промт по короткой просьбе. На китайском промте для Seedance агент вёл себя двумя способами: не дописывал фразу до конца или отвечал «Агент вернул пустой ответ. Попробуйте сформулировать иначе.»
Второе сообщение — дословно строка из кода, двести сотая в основной ветке. Оно и звучит как отписка, потому что это она и есть: человеку предлагают сформулировать иначе то, что сломалось не в формулировке.
Потолок, который делят
Причина стоит сорок пятой строкой того же файла: MAX_OUTPUT_TOKENS = 1_500. Потолок ответа — полторы тысячи токенов на всё. Модель по умолчанию — с рассуждением, а у таких моделей число общее: размышление и видимый текст вычитаются из одного лимита.
Справка OpenRouter говорит это прямее любого отчёта: на большинстве провайдеров потолок ответа ограничивает рассуждение и видимый вывод вместе, а если лимит мал настолько, что модель тратит его целиком на размышление, ответ приходит со статусом incomplete и без сообщения — и токены рассуждения всё равно списываются. Дальше арифметика без мистики: на переписать промт остаётся то, что не съело рассуждение.
Отсюда два исхода, и оба видел клиент. Пустой ответ — когда размышление заняло весь потолок. Обрывок — когда на видимый текст осталось полстроки. Код читал содержимое и не читал поле, в котором написано, почему модель остановилась; обрывок уходил в ответ как готовый промт. На китайском на знак уходит больше токенов, чем на латинице, поэтому потолок кончался раньше — и жалоба пришла именно с китайского промта.
Пустой ответ возвращал кредиты, обрывок — нет
В том же файле пустой ответ считается ошибкой: под него заведён отдельный класс исключения, и комментарий рядом объясняет, что для платного хода ядро вернёт кредиты. Обрывок ошибкой не считался: это непустой текст, то есть успешный ответ. Ход засчитывался, клиент платил за попытку и получал половину промта, которую нёс в генерацию — а генерация стоит своих кредитов и бесплатно не повторяется.
Один и тот же отказ — модель не договорила — оплачивался по-разному в зависимости от того, успела ли она сказать хоть слово. Формально система наказывала пустоту и поощряла обрывок; по смыслу — ровно наоборот, потому что пустота честнее. Это, кажется, главное в истории, и это не про токены: чтобы отличить «не смог» от «сделал половину», нужен один читатель причины остановки, а его не было.
Записка лежала в шапке соседнего файла
Самое неприятное здесь — не сам баг. В шапке клиента, которого этот агент зовёт, на строках с девяностой по сто первую в основной ветке записан почти тот же отказ, только про другую модель: Sonnet 5 в цикле Нодсмита заканчивает ход с пустым содержимым, в прод-логе причина остановки «stop», длина текста ноль, выходных токенов 6 572 — шесть с половиной тысяч ушли в размышление, а видимого ответа нет вовсе.
Там же сказано, что это воспроизведено в четырёх конфигурациях, что увеличение потолка до 16 000 не лечит и что это поведение модели, а не наша настройка. То есть знание лежало в файле, который агент импортирует — в трёхстах строках от места, где ставится потолок в 1 500.
Между комментарием и потолком нет связи: комментарий ничего не проверяет и ни на что не жалуется. Каждая следующая нода узнаёт про общий потолок тем же способом — от клиента. Вывод скучный и потому не усваивается: место, где читают причину остановки, должно быть одно и общее, иначе записка в шапке остаётся литературой.
Что в правке
Правка 2195 меняет именно это. Потолок — 8 000 токенов и низкая степень рассуждения: переписать промт не задача на долгий вывод. Если модель всё равно оборвалась по длине, идёт один повтор той же моделью с потолком 16 000 — в пределах общего срока хода в пятьдесят секунд, под который настроен прокси. Оборвалась и после повтора — ответ 502 «Ответ агента оборвался на середине, промт не изменён» вместо половины промта. Потолок одного вызова поднят с 18 до 35 секунд, в журнал хода уходят причина остановки, число выходных токенов, длина текста и модель.
Отдельно про деньги, потому что правка трогает платный ход: повтор идёт внутри одного прогона ядра — один резерв, одно списание, при неудаче возврат. Цена хода для пользователя не меняется; себестоимость бесплатного хода для платформы может вырасти, потому что потолок больше. В диффе двенадцать файлов, 287 строк добавлено и 36 удалено; пять из них — тесты: три набора про обрыв по длине, платный ход и срок, плюс проверки клиента и учёта стоимости хода. Числа прогона — сорок два файла и 434 теста зелёными — это отчёт автора правки; сама я тесты не запускала и в контейнере ничего не гоняла.

Чего в правке нет
Правка открыта с 2 ноября, тринадцать шестнадцать по Москве, и ревью у неё пока нет ни одного. Она трогает денежный путь — платный ход агента и повтор внутри ядра, — а по правилам репозитория такой мёрж требует перекрёстного ревью. Поэтому 2195 не влита, и в основной ветке я сегодня прочитала ту же строку: MAX_OUTPUT_TOKENS = 1_500, а строкой выше — потолок одного вызова 18 секунд.
Значит, у клиента всё то же, что вчера: либо пустота, либо половина. Сколько таких ходов прошло с утра, я не знаю и не буду оценивать — счётчик ходов я не читала. Могу сказать только то, что видно в файле: потолок стоит, причина остановки не читается, а починка лежит открытой и ждёт второго человека.
