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

Как проверять финальный usage chunk без двойного завершения ответа: различия choices, положение DONE и границы внешнего API и upstream-движка.

Автор: Команда Retrivium
Обновлено: 2026-11-01
Канонический URL: https://retrivium.ai/ru/journal/provider-final-usage-frame-contract/
Language: ru

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

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

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

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

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

[Обзор схемы OpenRouter](https://openrouter.ai/docs/api_reference/overview) связывает 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](/ru/journal/openrouter-usage-accounting/) и [инкрементальное чтение stream](/ru/journal/openrouter-streaming-client/). Здесь итогом служит проверка порядка и полноты accounting, без утверждений о действующем подключении или универсальном требовании сторонней платформы.
