В этом материале
Завершение choice, итог usage и закрытие HTTP-потока — отдельные события. Автомат должен понимать их связь и не считать повторный accounting-frame новым ответом.
Значение finish_reason удобно читать как подпись к последнему chunk, но для проверки провайдера этого недостаточно. Одна генерация проходит несколько состояний: запрос принят, поток открыт, появились данные, завершён конкретный choice, получены итоговые счётчики, закрыт транспорт. Если смешать эти события, тест может принять обрыв за успех или посчитать одно завершение дважды. Явная модель состояний делает такие ошибки наблюдаемыми.
Определите единицу состояния
Состояние запроса и состояние отдельного choice не всегда совпадают. Внутри запроса может существовать несколько независимых результатов, если соответствующая функция поддержана интерфейсом. Поэтому запись результата должна иметь область: request ID и choice index. Не делайте вывод о завершении всей операции только из первого встретившегося ненулевого finish_reason.
OpenRouter описывает нормализованные причины, включая stop, length, tool_calls, content_filter и error, и отдельно возвращает native_finish_reason. Нормализованное значение помогает клиентскому коду, а исходное сохраняет сведения реализации. Их нельзя произвольно подменять друг другом при анализе расхождений.
Задайте допустимые переходы
Минимальный автомат отличает отсутствие завершения от успешного terminal outcome и от отказа. Получение data-event ещё не означает успех. Полезно отдельно хранить причину окончания генерации и полноту доставки её envelope. Тогда length может быть корректным окончанием по лимиту, а оборванная доставка после content останется неполным результатом.
Таблица переходов должна содержать входное событие, предыдущее состояние, новое состояние и действие наблюдателя. Неизвестное значение reason сохраняют для разбора. Молчаливое превращение его в stop скрывает изменение контракта и делает регрессионный тест слишком снисходительным.
Учтите accounting-frame
Текущая документация streaming OpenRouter описывает финальный chunk с usage перед DONE. В Chat Completions он содержит непустой choices и повторяет terminal reason, чтобы поддержать клиентов, ожидающих элемент массива. Это особенность внешнего интерфейса платформы, а не универсальное требование к любому upstream-серверу.
Следовательно, правило «два terminal reason всегда ошибка» слишком грубо. Тест должен распознавать accounting-frame и не создавать второй завершённый ответ. При этом новый content после окончательного accounting-события требует отдельного рассмотрения, а не автоматического дописывания к уже закрытому результату.
Постройте минимальные трассы
Подготовьте короткие синтетические последовательности: обычное stop, length без видимого текста, tool_calls и неполный поток без terminal marker. Для каждой сохраните ожидаемую классификацию и число завершённых результатов. Fixtures должны быть малыми, чтобы ошибочный переход легко находился по одному событию.
Если сервер принимает запрос, но отклоняет неподдерживаемую возможность до открытия потока, это другая ветвь. Не смешивайте её с terminal reason внутри генерации. Полный отчёт показывает HTTP-исход, прикладной исход и наличие итогового usage независимо друг от друга.
Ограничьте вывод проверенным контрактом
Автомат подтверждает последовательность событий выбранной версии, но не качество текста и не доступность будущих запросов. При смене API нужно пересмотреть переходы: другой endpoint может использовать иные terminal events. Положительный тест Chat Completions нельзя молча перенести на Responses.
Смежные темы — ошибки внутри потока и сверка usage. Их полезно читать раздельно: первая объясняет отказ, вторая счётчики, а автомат связывает оба наблюдения без выдуманного универсального протокола.