Биллинг LLM при стриминге: когда отмена правда экономит
Разбираем биллинг LLM при стриминге: что оплачивается при ранней остановке, отмене запроса и обрыве соединения, и где экономии не будет.

Почему отмена не всегда снижает счёт
Когда ответ в чате обрывается раньше, кажется, что модель сгенерировала меньше текста и счёт тоже должен стать меньше. Для интерфейса это звучит логично. Для биллинга - не всегда.
Во время стриминга идут два процесса сразу. Модель генерирует токены у провайдера, а сеть и клиент доставляют их в приложение небольшими частями. Эти процессы связаны, но не совпадают по времени. Текст может перестать идти на экран, хотя модель ещё немного работает, а часть ответа уже попала в учёт.
Есть три похожих с виду, но разных сценария остановки. Отмена в UI прекращает чтение потока клиентом и иногда отправляет сигнал отмены дальше по цепочке. Stop sequence просит модель завершить генерацию, когда она встречает нужную последовательность. Обрыв сети просто ломает доставку и часто не останавливает саму генерацию сразу.
Для денег разница заметная. Stop sequence чаще всего режет именно генерацию, поэтому реально уменьшает число выходных токенов. Кнопка Cancel тоже может сработать вовремя, но может и опоздать: модель уже успела досчитать заметную часть ответа. Обрыв соединения обычно помогает хуже всего, потому что он часто прерывает только показ текста.
Если запрос идёт через шлюз вроде RU LLM, между вашим приложением и провайдером есть как минимум два участка. Закрытие соединения на первом участке не означает мгновенную остановку на втором. Поэтому фраза "пользователь не дочитал ответ" ещё не равна фразе "провайдер не посчитал токены".
Есть и ещё одна причина путаницы. Входные токены обычно уже обработаны к моменту, когда начинается генерация ответа. Их отмена почти никогда не возвращает. Спор идёт в основном о выходных токенах, и то только о той части, которую модель не успела сгенерировать.
Простой пример. Оператор увидел первую полезную фразу и нажал Stop. На экране осталось около 25 токенов, но модель к этому моменту могла уже сгенерировать 150. Для счёта это ближе к 150, чем к 25. Если бы вы заранее ограничили формат ответа и поставили stop sequence, остановка случилась бы раньше, и экономия была бы настоящей.
Отмена сама по себе не режет счёт. Экономию даёт только та остановка, которая успела прервать саму генерацию, а не просто оборвала показ текста пользователю.
Что происходит во время стриминга
Стриминг не меняет саму логику расчёта цены. Он меняет только способ доставки ответа. Вместо одной готовой строки в конце вы получаете маленькие части по мере генерации.
Обычно путь запроса выглядит так: сервис принимает prompt, системные инструкции и параметры вроде max_tokens, разбивает вход на токены и отправляет его модели или провайдеру. Модель читает весь входной контекст. За входные токены счёт уже начинает формироваться, даже если пользователь ещё не увидел ни одного символа. Потом модель начинает генерировать ответ токен за токеном. Как только появляются первые готовые токены, API отправляет их чанками.
Здесь и возникает важный временной сдвиг. Между моментом "первый токен появился в интерфейсе" и моментом "модель уже посчитала часть продолжения" нет полного совпадения. Модель, сеть, прокси и клиент работают в разном темпе. Пока UI показывает 20 токенов, у провайдера их может быть уже 30 или 40 в очереди на отправку.
Поэтому стриминг сам по себе не делает ответ дешевле. Если без стриминга модель сгенерировала бы 300 токенов, то при стриминге она обычно посчитает те же 300, если её не остановить раньше. Меняется только способ, которым ответ доезжает до клиента.
Это хорошо видно на OpenAI-совместимых шлюзах вроде RU LLM. Вы получаете чанки через привычный формат API, но тарификация всё равно привязана к тому, сколько входных и выходных токенов реально прошло через модель и провайдера, а не к тому, сколько текста успел отрисовать фронтенд.
Момент показа ответа и момент начисления стоимости - разные вещи. Если путать эти этапы, легко решить, что отмена уже сэкономила деньги, хотя на самом деле она просто остановила вывод в интерфейсе.
За что списываются деньги по факту
Счёт обычно состоит из двух частей: prompt_tokens и completion_tokens. Первые - это всё, что вы отправили модели: системный промпт, историю диалога, инструкции, данные пользователя. Вторые - то, что модель сгенерировала в ответ.
Если упростить, вход оплачивает чтение контекста, выход - генерацию ответа. Поэтому отмена почти никогда не возвращает деньги за уже принятый входной текст. Если модель успела начать ответ, часть суммы за выход тоже уже набежала.
При стриминге провайдер редко смотрит на то, сколько символов дошло до интерфейса. Его интересует другое: сколько токенов модель реально обработала и сгенерировала у себя. Это не одно и то же.
Часто процесс выглядит так. Провайдер принимает запрос и считает входные токены. Затем модель начинает генерацию и накапливает выходные токены. В конце провайдер фиксирует usage в метаданных ответа или во внутренних логах, а шлюз и клиент получают уже готовую цифру для списания.
Из-за этого и возникает частая путаница. Пользователь мог увидеть только половину ответа, а счёт пришёл почти за полный. Такое бывает, если клиент оборвал соединение поздно, сеть потеряла часть чанков или приложение перестало показывать текст, но модель на стороне провайдера ещё несколько секунд продолжала генерацию.
Пример простой: модель сгенерировала 400 токенов, а браузер успел показать только 260. Для пользователя ответ как будто оборвался. Для биллинга провайдер всё равно может посчитать все 400, если модель уже успела их создать до остановки.
Момент фиксации usage зависит от провайдера. Одни считают completion по факту завершения генерации. Другие обновляют счётчик по мере стриминга. Но в обоих случаях в расчёт обычно попадает то, что модель уже успела создать, даже если клиент это не получил.
Есть и ещё один нюанс - минимальная стоимость запроса. Некоторые провайдеры используют нижний порог списания, округление до небольшого блока токенов или отдельную плату за обращение к модели. Тогда совсем короткий запрос, отменённый почти сразу, всё равно не станет бесплатным.
Если вы работаете через RU LLM, полезно сверять не только то, что увидел клиент, но и usage, который вернулся через шлюз. Для денег это почти всегда главная цифра.
Когда ранняя остановка реально экономит
Экономия появляется только в одном случае: модель перестаёт генерировать новые токены раньше. Для счёта это важнее, чем сам факт нажатия на кнопку "Стоп".
Если вы задали stop sequence или получили нужный ответ очень рано, расход часто падает заметно. Допустим, модели хватило 60 токенов вместо 300. Тогда вы платите за короткий completion, а не за полный развёрнутый ответ.
Совсем другая картина при поздней отмене. Пользователь читает поток, а потом останавливает ответ на 280-м токене из возможных 320. Формально отмена есть, но экономия маленькая: большая часть текста уже сгенерирована. Иногда разницы в счёте почти не видно.
На практике ранняя остановка помогает в двух сценариях. Первый - модель сама завершает ответ по stop sequence, потому что дошла до нужной границы. Второй - клиент прерывает запрос достаточно рано, и провайдер действительно прекращает генерацию, а не просто перестаёт отправлять уже готовые чанки.
Если остановка случилась только в интерфейсе, этого мало. Бэкенд мог уже держать в буфере ещё несколько чанков, а провайдер - продолжать расчёт ответа. Пользователь видит короткий текст, но биллинг идёт почти как за длинный.
Поэтому обычно полезно сравнивать два сценария на одних и тех же запросах. В первом модель останавливается на 50-80 токенах из-за stop sequence или жёсткого формата ответа. Во втором отмена приходит, когда модель уже дошла до 80-90% текста. Во втором случае экономия обычно слабая. В первом она повторяется от запроса к запросу.
Ещё один понятный инструмент - max_tokens. Он не делает ответ дешёвым сам по себе, но ставит жёсткий потолок на размер completion. Для операторских чатов, саппорта и внутренних ассистентов это часто самый предсказуемый способ держать расход под контролем.
Если вы работаете через OpenAI-совместимый шлюз вроде RU LLM, смотрите на фактический учёт completion_tokens по каждому запросу, а не на то, сколько текста дошло до экрана. Именно там видно, была ли ранняя остановка реальной экономией или просто обрывом вывода.
Почему обрыв соединения часто не помогает
Потеря соединения на клиенте часто выглядит как экономия. Пользователь закрыл вкладку, пропал мобильный интернет, приложение поймало timeout. Для счёта это нередко ничего не меняет.
Типичный сценарий выглядит так: веб-чат отправил запрос через OpenAI-совместимый шлюз, через несколько секунд браузер оборвал SSE-поток, и на экране ответ оборвался на середине фразы. Но апстрим-провайдер уже получил полный промпт и продолжил генерацию. Если отмена не дошла до него отдельным сигналом, выходные токены могут начислиться почти полностью.
Такое случается чаще, чем кажется. Стриминг не означает, что модель генерирует строго "по одному токену под ваш экран". Между клиентом, прокси и провайдером есть буферы, очереди и разная скорость сети. Пока браузер медленно читает поток, сервер уже мог принять почти весь ответ от провайдера. Для пользователя это выглядит как обрыв. Для биллинга - как почти завершённый запрос.
Клиентский timeout и настоящая отмена запроса - разные вещи. Timeout на фронтенде или в SDK обычно закрывает соединение только со стороны клиента. Провайдер о таком событии может узнать поздно или не узнать вовсе. Реальная отмена работает иначе: прокси или сервер отправляет явный сигнал остановки, и только после этого генерация может действительно прекратиться.
Проверять здесь нужно не один факт, а сразу несколько. Где именно сработал timeout - в браузере, в бэкенде или у прокси. Передаёт ли ваш стек сигнал cancel дальше по цепочке. Прекращает ли провайдер генерацию сразу или только после ближайшего внутреннего шага. И что именно попадает в usage и логи: выданные токены, сгенерированные токены или уже принятые сервером чанки.
Поведение различается. Один провайдер почти сразу перестаёт считать выход после cancel. Другой включает в счёт всё, что модель успела сгенерировать к моменту отмены. Прокси тоже ведут себя по-разному: одни быстро рвут апстрим, другие замечают обрыв клиента с задержкой и ещё несколько секунд держат запрос живым.
Поэтому сам факт разрыва соединения почти ничего не говорит о расходах. Смотрите на usage, серверные логи и на то, дошёл ли cancel до апстрима. Только это показывает, спас ли обрыв бюджет или нет.
Как проверить списание по шагам
Спорить об экономии по ощущениям бессмысленно. Нужны одинаковые тесты и логи, где видно, сколько токенов модель успела сгенерировать до остановки.
Возьмите один и тот же промпт и одну модель. Один запрос дайте закончить полностью. Второй оборвите в заранее выбранный момент - например, через 2-3 секунды стрима или после 150-200 выходных токенов.
- Сохраните
usage,finish_reason, время до первого токена и полную длительность ответа. Если API отдаётusageтолько в конце, отметьте это отдельно. - Сравните успешный и прерванный запрос по входным и выходным токенам. Смотрите не только на общую сумму, но и на разницу именно в
completion_tokens. - Проверьте, кто закрыл соединение: клиент, ваш прокси, балансировщик или сам провайдер по timeout.
- Поднимите логи по
request_idи посмотрите фактический статус стрима. Важен не только HTTP-статус, но и запись о том, дошёл ли запрос до нормального завершения. - Повторите тест 5-10 раз. Один прогон легко искажает картину из-за сети, ретраев или очереди у провайдера.
На практике самая частая ошибка очень простая: команда видит, что пользователь закрыл вкладку, и считает, что расход тоже остановился. Это не всегда так. Если провайдер уже сгенерировал большую часть ответа, деньги обычно спишутся за уже обработанные токены, даже если клиент ничего не дочитал.
Полезно записывать три времени: момент отправки запроса, момент первого чанка и момент разрыва. Тогда видно, отменили вы запрос до генерации, в самом начале или почти в конце. Для биллинга это разница между заметной экономией и почти нулевым эффектом.
Если у вас есть прокси-слой, ищите расхождение между логом клиента и логом провайдера. В RU LLM это удобно проверять по request_id и аудит-трейлу запроса: так проще понять, где именно оборвался стрим и дошёл ли запрос до провайдера.
Пример с чат-оператором
Представьте поддержку интернет-магазина или банка. Оператор открывает диалог, а помощник на LLM получает длинный промпт: правила ответа, выдержки из базы знаний, статус заказа, историю переписки и служебные инструкции. Такой запрос легко тянет на 1500-2000 входных токенов ещё до первого слова ответа.
Дальше клиент спрашивает: "Где мой заказ?" Модель начинает стримить подсказку для оператора на 500-700 выходных токенов: сначала короткий ответ, потом пояснение, затем шаблон на случай конфликта. Но оператору хватает первых двух строк. Он уже понял, что делать, и закрывает окно.
Внешне похожих сценария здесь три, а счёт у них разный. В первом случае система отдала полный ответ: 1800 входных токенов и, допустим, 600 выходных. В счёт попадают и вход, и все 600 токенов ответа.
Во втором случае приложение отправило явный stop или cancel после первых 80 токенов, и провайдер действительно остановил генерацию. Тогда в счёте обычно остаются те же 1800 входных и примерно 80 выходных токенов, иногда чуть больше из-за буфера.
В третьем случае оператор просто закрыл вкладку или потерял сеть. Текст на экране пропал, но генерация могла продолжиться. Если сервер не передал отмену провайдеру, модель спокойно допишет почти весь ответ, и счёт окажется близок к сценарию с полным выводом.
Команды часто путают второй и третий случай. Для бюджета важен не факт, что окно закрылось, а факт, что ваш бэкенд действительно оборвал запрос к модели. Если вы работаете через шлюз вроде RU LLM, это удобно сверять по логам и аудит-трейлу: клиент ушёл раньше времени или генерация правда остановилась.
Здесь правило очень простое: деньги обычно экономит только управляемая ранняя остановка, когда приложение явно шлёт cancel, а верхний провайдер прекращает выпуск токенов. Обрыв сети сам по себе расход не режет.
Где команды ошибаются
Самая частая ошибка звучит так: пользователь закрыл вкладку, значит генерация сразу остановилась и деньги больше не тратятся. На практике браузер мог разорвать соединение с интерфейсом, а модель ещё несколько секунд продолжала дописывать ответ у провайдера.
Из-за этого многие смотрят только на время ответа. Если стрим прервался на четвёртой секунде, кажется, что система сэкономила. Но счёт считают не по ощущению скорости, а по токенам. Если вы не проверяете usage, вы видите только половину картины.
Отдельно мешает привычка ставить высокий max_tokens "про запас". Сам по себе такой лимит не означает перерасход, но он даёт модели длинный коридор. Если сигнал остановки не дошёл или пришёл поздно, лишние токены успевают сгенерироваться и попасть в биллинг.
На практике ошибки повторяются одни и те же. Фронтенд пишет "пользователь отменил ответ", а серверный лог показывает, что запрос жил дольше. Команда меряет только latency и число ошибок, но не сравнивает completion_tokens по отменённым запросам. Или один удачный тест на знакомой модели превращают в правило для всех моделей и провайдеров.
Ещё одна путаница возникает между клиентскими логами и данными провайдера. Клиент видит обрыв стрима и считает запрос незавершённым. Провайдер может считать иначе: часть ответа уже создана, токены уже учтены. Если не сводить эти источники по одному request_id, разговор о расходах быстро превращается в гадание.
Это особенно заметно там, где запросы идут через единый шлюз к разным моделям. Поведение при отмене и при разрыве соединения у провайдеров различается. В RU LLM для этого полезно сверять клиентские события с аудит-трейлами и учётом по каждому запросу, а не доверять только тому, что увидел браузер.
Самая дорогая ошибка - проверить одну модель, увидеть экономию на ранней остановке и перенести этот вывод на весь стек. На одной модели cancel сработает почти сразу, на другой списание почти не изменится. Пока команда не сравнила usage, момент отмены и итоговую стоимость на каждой рабочей модели, у неё нет правила. Есть только догадка.
Что проверить перед релизом
Перед запуском полезен не только нагрузочный тест, но и короткий прогон именно на биллинг. Он быстро ловит дорогие мелочи: слишком высокий max_tokens, неверную обработку отмены и путаницу между тем, что увидел клиент, и тем, что успел сгенерировать сервер.
Один и тот же промпт может дать разный счёт не из-за качества ответа, а из-за того, как ваш код завершает запрос. Поэтому проверяйте не только текст, но и метаданные.
Для каждого сценария задайте свой потолок max_tokens. Поиск по базе, короткий ответ оператору и развёрнутое резюме не должны жить с одним лимитом. Сохраняйте finish_reason рядом со стоимостью и числом токенов, чтобы быстро видеть, где модель дошла до stop, а где упёрлась в length или завершилась иначе. Разведите в логах клиентский timeout и серверную отмену: если браузер закрыл соединение, это ещё не значит, что провайдер перестал генерировать токены. И прогоните тесты по всем моделям в маршруте, если у вас есть фолбэк или маршрутизация через единый endpoint.
Хороший минимальный тест выглядит скучно, и это нормально. Возьмите 10-20 типовых запросов, прогоните их в обычном режиме и в стриминге, затем повторите часть с ранней остановкой на 30%, 60% и 90% ответа. После этого сравните токены и finish_reason. Если стоимость почти не меняется, значит отмена у вас декоративная.
Отдельно проверьте сценарий с timeout на клиенте. Частая проблема выглядит так: фронтенд ждёт 15 секунд и закрывает сокет, а сервер или провайдер ещё несколько секунд дописывает ответ. Пользователь видит обрыв, команда думает, что сэкономила, а списание идёт почти как за полный ответ.
Если вы работаете через маршрутизацию нескольких провайдеров, как в RU LLM, не ограничивайтесь одной моделью. На одном маршруте ранняя остановка может сразу уменьшать расход, на другом почти не даст разницы. Это лучше проверить до релиза и зафиксировать в логах.
Что делать дальше
После такого разбора лучше перейти от споров к замерам. В теме отмен и обрывов почти все ошибки появляются из-за одной причины: команда смотрит на текст в интерфейсе, а считать нужно по usage, finish_reason и событию отмены на каждом уровне.
Начните с одной общей таблицы для всех моделей и сценариев. Обычно в неё достаточно добавить модель, провайдера, тип запроса, finish_reason, фактический usage, цену и то, кто именно прервал запрос - фронтенд, бэкенд или пользователь. Такая таблица быстро показывает, где ранняя остановка правда снижает расход, а где вы просто раньше закрыли соединение.
Дальше нужен простой рабочий порядок. Команда должна договориться об одном правиле отмены для SDK, бэкенда и фронтенда. Разработчикам стоит логировать прерванные запросы отдельно от обычных завершений. Аналитике полезно считать пустые ответы как отдельный класс, а не смешивать их с якобы бесплатными вызовами. Тестировщикам - гонять один и тот же сценарий на нескольких моделях и сравнивать usage с итоговым списанием.
Это не бюрократия, а способ перестать терять деньги в тихих местах. Если браузер оборвал стрим, а сервер не отправил явную отмену дальше по цепочке, провайдер может спокойно догенерировать ответ и посчитать токены полностью. Пользователь увидит пустой экран, а счёт пустым не будет.
Отчёт по прерванным запросам и пустым ответам тоже быстро окупается. Часто команда видит рост расходов и ищет проблему в тарифе, хотя причина проще: мобильное приложение теряет соединение, чат закрывается раньше времени или ретраи плодят лишние вызовы. Без отдельного отчёта это выглядит как случайный шум.
Если вы уже используете RU LLM, удобно сверять usage и аудит-трейлы через один OpenAI-совместимый эндпоинт. Это упрощает проверку разных провайдеров и помогает быстро увидеть, одинаково ли они считают стрим, раннюю остановку и обрыв соединения.
Через несколько дней таких замеров у вас появится не теория, а собственная карта правил: где отмена режет расход, где почти не меняет счёт и какие сценарии нужно исправлять в коде в первую очередь.
Часто задаваемые вопросы
Почему после Cancel счёт почти не меняется?
Потому что Cancel часто останавливает только вывод текста в интерфейсе. Провайдер к этому моменту уже мог сгенерировать заметную часть ответа и включить эти completion_tokens в счёт.
Проверяйте не экран, а usage и момент отмены. Если сигнал дошёл поздно, экономия будет маленькой или её не будет вовсе.
Что дешевле: Cancel, stop sequence или обрыв сети?
Обычно лучше всего экономит stop sequence, потому что он завершает саму генерацию в нужной точке. Cancel тоже может помочь, но только если ваш сервер быстро передал отмену дальше и провайдер сразу остановил модель.
Обрыв сети почти всегда хуже. Он часто рвёт доставку, а не генерацию.
Можно ли вернуть деньги за prompt_tokens после отмены?
Почти никогда. Модель читает входной контекст в самом начале, и провайдер обычно считает эти токены сразу.
Спор обычно идёт только о выходе. Если модель не успела сгенерировать часть ответа, вы не платите за эту часть.
Стриминг сам по себе делает ответ дешевле?
Нет. Стриминг меняет только способ доставки ответа: вы получаете текст частями, а не одной строкой в конце.
Если модель без остановки сгенерировала бы 300 токенов, при стриминге счёт обычно останется тем же.
Как понять, что отмена дошла до провайдера?
Сравните клиентский лог, серверный лог и usage по одному request_id. Если клиент закрыл поток, а completion_tokens почти такие же, как у полного ответа, ваш стек не остановил апстрим вовремя.
В шлюзе вроде RU LLM это удобно смотреть по request_id и аудит-трейлу.
Зачем ставить max_tokens, если есть кнопка Stop?
max_tokens ставит жёсткий потолок на длину ответа. Cancel зависит от времени, сети и того, как ваш код передаёт сигнал дальше.
Если вам нужен предсказуемый расход, сначала задайте разумный max_tokens, а уже потом добавляйте отмену и stop sequence.
Почему пользователь увидел мало текста, а completion_tokens оказались больше?
Потому что экран и биллинг живут в разном темпе. Пока UI показал 20 токенов, провайдер уже мог сгенерировать 40 или 60 и держать их в буфере.
Для денег смотрите на completion_tokens, а не на то, сколько символов увидел пользователь.
Что нужно логировать, чтобы не гадать о списаниях?
Сохраняйте usage, finish_reason, время до первого токена, полную длительность запроса и причину разрыва соединения. Ещё полезно видеть, кто именно закрыл поток: фронтенд, бэкенд, прокси или провайдер.
Без этих полей команда обычно спорит по ощущениям и промахивается с выводами.
Как быстро проверить поведение биллинга перед релизом?
Возьмите 10–20 типовых запросов и прогоните их в одном режиме до конца, а в другом останавливайте на 30%, 60% и 90% ответа. Потом сравните completion_tokens, цену и finish_reason.
Отдельно проверьте клиентский timeout. Он часто создаёт видимость экономии, хотя провайдер дописывает ответ дальше.
Одинаково ли ведут себя все модели и провайдеры при отмене?
Да, и разница бывает заметной. Одна модель почти сразу прекращает генерацию после cancel, другая успевает досчитать ещё кусок ответа и включает его в счёт.
Не переносите вывод с одного маршрута на весь стек. Проверьте каждую рабочую модель и каждого провайдера отдельно.