Почему после остановки генерации остаются списания: отмена API, фоновые задачи и счёт
Разделите остановку отображения, отмену соединения, завершение фоновой задачи и расчёт. Проверьте передачу сигнала, очередь повторов и поздние результаты с помощью локального примера.
Вы нажали «Остановить генерацию», текст больше не появляется, но в кабинете позднее возникли расход токенов или списание. Это ещё не доказывает неисправность кнопки. Остановка отображения, разрыв текущего соединения, отмена серверной задачи и окончательный расчёт — разные события.
Ниже разбирается проверка добровольной отмены: что должно остановиться, какие результаты могут прийти позже и как избежать повторного запроса или замены нового ответа старым. Статья не обещает бесплатную отмену у какого-либо провайдера. Пример работает только локально, без обращений к платному API. Документация проверена 26 октября 2026 года.
1. Определите, на каком уровне произошла остановка
| Уровень | Наблюдаемое подтверждение | Что оно ещё не доказывает |
|---|---|---|
| Интерфейс | Текст больше не дописывается, кнопки доступны | Сетевой запрос отменён |
| Соединение клиента | Текущему запросу передан сигнал отмены, чтение завершилось или вернуло ошибку отмены | Соединение шлюза с upstream тоже завершилось |
| Серверная задача | Свой backend прекратил планирование; upstream вернул документированное состояние отмены | Уже созданный текст и выполненные действия отменены задним числом |
| Расчёт | Финальное использование, состояние расчёта и применимые правила для запроса | После нажатия кнопки никаких расходов быть не может |
По MDN, AbortController.abort() может прервать fetch, потребление тела ответа или поток. В Node.js этот класс описан как механизм уведомления об отмене для определённых Promise API.[1][2] Речь об операциях, участвующих в механизме отмены, а не об универсальной команде отката удалённой задачи и биллинга.
Если приложение лишь перестало выводить символы, оно может продолжать получать данные. Возможен и обратный случай: весь ответ уже загружен, но интерфейс показывает его медленно. Остановка анимации не отменит ранее выполненную генерацию. Сначала различите «модель генерирует» и «интерфейс отображает».
2. Проследите передачу отмены для одного запроса
Назначьте пользовательской задаче бизнес-ID, каждой фактической попытке — отдельный номер и свяжите их с request ID провайдера. ID фронтенда не обязательно совпадает с ID платформы. Сначала изучите имеющиеся записи; для разработки используйте локальный имитатор upstream, чтобы не отправлять платные запросы только ради проверки отмены.
Последовательно проверьте:
- Кнопку остановки. Вызывает ли она контроллер именно текущего запроса или лишь меняет loading? При быстром запуске второй задачи нельзя случайно отменить её вместо первой.
- Чтение ответа. Охватывает ли отмена чтение тела после получения заголовков? Возвращённый fetch объект Response не означает, что тело уже прочитано; последующая отмена может завершить чтение ошибкой.[3]
- Собственный backend. Браузер–backend и backend–upstream — два соединения. Распознаёт ли backend отмену с учётом жизненного цикла своего фреймворка и передаёт ли сигнал исходящему SDK или запросу, поддерживающему этот параметр? События close и finish имеют разные значения в разных системах; нельзя произвольно выбрать одно из них как доказательство отмены клиентом.
- Очередь и фоновую задачу. Если запрос превратился в сохраняемую задачу, закрытия страницы обычно недостаточно. Только при наличии документированного интерфейса отмены используйте нужный task ID, права и правила состояния. Без такого интерфейса отметьте «отмена не подтверждена», а не придумывайте путь DELETE.
- Повторы и последующие действия. Не остались ли после остановки таймер повтора, переключение модели, вызов инструмента или следующая порция запросов? Проверяйте отмену перед каждым новым запуском.
Запись, платёж или отправка сообщения, уже начатые во внешней системе, не откатываются автоматически из-за локального исключения. Проверьте журнал действия, затем применяйте поддерживаемый этой системой откат или компенсацию. Границы автоматического переключения разобраны в проверке fallback и цепочки попыток.
3. Локальный пример: сигнал отправлен, но задача может его игнорировать
Это не имитация HTTP-сервера и не реализация отмены конкретной платформы. Две управляемые задачи показывают только договорённость об обработке сигнала. Сохраните код как cancel-demo.mjs и запустите актуальной версией Node.js с AbortController. В нём нет ключей, сети, ожидания по таймеру или платных операций. finish() вручную завершает вымышленную задачу.
export function mockTask(signal, cooperative) {
if (!(signal instanceof AbortSignal) || typeof cooperative !== 'boolean') {
throw new TypeError('Expected AbortSignal and boolean');
}
let settled = false;
let resolveResult, rejectResult;
const result = new Promise((resolve, reject) => {
resolveResult = resolve;
rejectResult = reject;
});
const settle = (success, value) => {
if (settled) return;
settled = true;
signal.removeEventListener('abort', onAbort);
if (success) resolveResult(value);
else rejectResult(value);
};
const onAbort = () => settle(false, signal.reason);
if (cooperative) {
if (signal.aborted) onAbort();
else signal.addEventListener('abort', onAbort, { once: true });
}
return { result, finish: () => settle(true, 'completed') };
}
for (const cooperative of [true, false]) {
const controller = new AbortController();
const task = mockTask(controller.signal, cooperative);
const observed = task.result.then(
value => value,
error => error?.name ?? 'aborted'
);
controller.abort();
task.finish();
console.log(cooperative, await observed);
}
Результат: true AbortError и false completed. Первая задача проверяет уже отменённый сигнал и слушает последующую отмену. Вторая намеренно игнорирует сигнал и поэтому завершается. Повторный finish() или повторная отмена не меняют результат уже завершённого Promise. Слушатель снимается и при обычном завершении: одного once недостаточно для пути, на котором отмена так и не произошла.[2][3]
Пример не доказывает, что реальный сервер продолжит генерацию, и не вычисляет стоимость. Он объясняет, почему создание контроллера и вызов abort() без подключения signal к самой операции могут оставить работу в фоне. Это демонстрация состояний в одном цикле событий JavaScript; она не обеспечивает транзакции базы данных, отмену между процессами или распределённое устранение дублей.
Для нового запроса создавайте новый контроллер. Отменённый AbortSignal не восстанавливается: fetch с таким сигналом сразу отклоняется.[3] Повтор с уже отменённым сигналом не является проверкой доступности провайдера.
4. Как не позволить позднему результату заменить новую задачу
Предположим: задача A запущена, пользователь отменяет A и запускает B, после чего выполняется старый callback A. Даже при корректной сетевой отмене перед показом или сохранением нужно проверить владельца результата и право этой задачи на запись.
На фронтенде как минимум сравните task ID результата с текущим task ID и проверьте, не отменена ли задача пользователем. finally от A не должен выключать индикатор B, а обработчик ошибки A — помечать B как неудачную. Частичный текст можно оставить с пометкой «прервано, результат неполный», но нельзя автоматически записывать его как законченный ответ в рабочие данные.
На backend нужна атомарная проверка состояния: сохранять результат может только задача в допустимом состоянии и с ожидаемой версией. Между отдельным чтением статуса и безусловной записью может произойти отмена. Такой порядок оставляет окно гонки. Условное обновление, транзакция или механизм очереди выбираются по устройству вашего хранилища.
У ещё не отправленных задач проверяйте отмену при извлечении из очереди и непосредственно перед отправкой. Для выполняющихся — запрашивайте отмену и записывайте её подтверждение. Для завершённых сохраняйте факт выполнения: «пользователю больше не нужно» не означает «не выполнялось». Добровольная отмена обычно не должна попадать в общий обработчик сетевых ошибок с автоматическим повтором.
5. После остановки вырос счёт: восстановите временную линию
Различайте четыре времени: отправка запроса, нажатие остановки, завершение задачи платформой, если доступно, и отражение операции в счёте. Поздняя запись в счёте не означает, что всё использование произошло после остановки. Возможны также предварительное удержание и последующий расчёт. Отсутствие строки в текущий момент не доказывает, что upstream ничего не выполнил.
| Наблюдение | Следующая проверка |
|---|---|
| Для того же request ID позднее появилась итоговая сумма | Время завершения, задержка расчёта, входное и выходное использование, условия оплаты отмены |
| После остановки появились новые request ID | Автоповтор, fallback, продолжение очереди или другое устройство; сами разные номера не являются достаточным доказательством |
| На экране часть текста, а выходных токенов в счёте больше | Буфер отображения, место остановки соединения, состояние upstream; видимые символы не равны тарифицируемым токенам |
| Отмена принята, но задача не достигла конечного состояния | Проверка финального состояния по документации; «принято» не обязательно значит «остановлено» |
| Новых запросов нет, а баланс меняется | Поздний расчёт, подписка, автопополнение, другие задачи; одного общего баланса недостаточно |
Если поток отменён до финального события использования, локально может не оказаться полного usage. Запишите «неизвестно, требуется сверка с платформой», а не ноль. Число полученных символов не позволяет точно восстановить весь счёт. Проблемы передачи разбираются в диагностике потока и чтения SSE, но оплату отмены определяют текущие правила платформы и записи конкретного запроса.
Поддержке передайте четыре времени с часовым поясом, клиент и версию, путь запроса, связь task ID и номера попытки, факт добровольной отмены, наличие повторов, request ID платформы и финальное состояние счёта. Не прикладывайте полный Key, Cookie, исходный prompt или необезличенный сетевой дамп. Подойдёт шаблон минимального воспроизведения и обращения без секретов.
6. Приёмка кнопки остановки перед запуском
На локальных фиктивных задачах проверьте шесть порядков: отмена до старта, во время работы, после завершения, повторная отмена, игнорирование сигнала и запуск B после отмены A. Затем сверяйтесь с документацией реального клиента и провайдера. Если очередь продолжает отправлять задания, добровольная отмена запускает повтор, старый результат заменяет новый или неизвестный исход upstream помечается успехом, сначала исправьте это и лишь затем разрешайте массовую обработку.
После локальной проверки необходимость реального теста с небольшим бюджетом определяет владелец процесса; в рамках этой статьи такой тест не выполнялся. Если его проводить, нужны явный бюджет, предел вывода, одна попытка и несекретный ввод. Отдельно проверьте отмену клиента, состояние сервера и окончательную стоимость. Ненаблюдаемые этапы upstream оставляйте неизвестными.
Для пользователя готового клиента практический порядок таков: после остановки не нажимайте многократно «Сгенерировать заново», сохраните время и request ID, проверьте очередь и повторы, затем уточните у автора клиента или провайдера границы остановки. Отзыв ключа может ограничить дальнейшее использование учётных данных, но нельзя считать, что он автоматически завершает все уже начатые задачи или возвращает понесённые расходы.
Источники
- [1] MDN: AbortController.abort(): отмена запроса, чтения тела и потока; AbortError как причина по умолчанию.
- [2] Node.js: AbortController и AbortSignal: уведомление об отмене, однократное событие и очистка слушателей.
- [3] MDN: AbortSignal: одноразовый сигнал, отмена после получения заголовков, собственный отменяемый API и очистка при обычном завершении.