РОУТИНГ LLM

Финальный usage-frame: проверка контракта потокового ответа

В этом материале
ГЛАВНОЕ ЗА МИНУТУ

Финальный usage — accounting-событие, а не новая генерация. Его форму и порядок нужно проверять по конкретному endpoint, сохраняя различия реализаций.

Счётчики запроса часто приходят тогда, когда текст уже закончился. Это создаёт ловушку для тестов: обработчик закрывает результат по finish_reason и больше не читает поток либо принимает следующий frame за второй ответ. Проверка финального usage должна связывать текст, завершение choice и accounting в одну операцию, сохраняя их как разные события. Сравнивать нужно конкретные интерфейсы, а не абстрактную совместимость с семейством API.

Зафиксируйте сравниваемые стороны

Документация streaming OpenRouter описывает текущую форму внешнего Chat Completions. Финальный usage-frame идёт перед DONE и содержит непустой choices с content-free delta и повторённой причиной завершения. На той же странице это явно отмечено как отличие от формы, ожидаемой в исходной спецификации OpenAI.

Нельзя на этом основании объявлять такую форму обязательной для upstream-провайдера. В адаптере нужно отдельно описать входной контракт движка и выходной контракт сервиса. Если между ними есть преобразование, его проверяют отдельным fixture, а не меняют ожидания молча при чтении ответа.

Не считайте frame новой работой

Обзор схемы OpenRouter связывает usage с запросом и описывает поля prompt_tokens, completion_tokens и total_tokens. Получение accounting-frame не должно увеличивать число успешно завершённых генераций. Счётчик результата меняет состояние один раз, а accounting дополняет уже известную операцию.

Для проверки сохраните request ID и список наблюдаемых событий. Если одна операция имеет два terminal-looking frames, посмотрите на их роль и форму. Простое правило «любое повторение finish_reason — дубль запроса» даст ложный сигнал на допустимом потоке, описанном документацией.

Определите структурные инварианты

У fixture должны быть ожидаемые свойства: usage связан с правильным запросом, получен в допустимой позиции, не потерян из-за раннего выхода из цикла, не суммируется с собственным повтором. Для полей задайте единицы и правила отсутствия. Нулевое значение отличается от неизвестного счётчика.

Не требуйте положительного completion_tokens для каждого корректного исхода. Запрос мог закончиться иначе, а visible content не всегда описывает весь сгенерированный объём. Проверка структуры отделяется от проверки арифметики и от коммерческого расчёта, которому могут требоваться дополнительные подтверждения.

Испытайте нормальную и неполную доставку

Создайте маленький synthetic stream: content, terminal reason, usage, DONE. Затем удалите accounting-frame и отдельно оборвите поток перед DONE. Ожидаемая классификация должна показать, что именно отсутствует, а не превращать любой последний полученный объект в полный успех.

Добавьте вариант, где клиент отменяет чтение после текста. Он показывает предел наблюдения: отсутствие usage у клиента ещё не доказывает, что движок не посчитал работу. Для разбора нужна связь с серверной записью, если она предусмотрена выбранной реализацией. Такой тест не требует реального платного потока.

Сохраните контракт по endpoint

Responses и другие интерфейсы могут сообщать usage другими terminal events. Поэтому общий parser должен выбирать проверку по endpoint, а не искать одно магическое поле в любом JSON. Версия fixture хранится рядом с версией адаптера, чтобы изменение схемы было видно при релизе.

Смежные материалы — сверка usage и инкрементальное чтение stream. Здесь итогом служит проверка порядка и полноты accounting, без утверждений о действующем подключении или универсальном требовании сторонней платформы.

← Все материалы