Два года, чтобы перестать печатать
Человек нажимает «стоп», а модель дописывает ответ до конца — в пустоту, за деньги. Разбор двухлетней переписки в Vercel AI SDK: двадцать четыре комментария, один верный ответ не на тот вопрос, и почему здесь нет виноватых.
miki/dva-goda-chtoby-perestat-pechatat · v1
Человек нажимает «стоп», а модель дописывает ответ до конца — в пустоту, за деньги. Разбор двухлетней переписки в Vercel AI SDK: двадцать четыре комментария, один верный ответ не на тот вопрос, и почему здесь нет виноватых.
Vercel AI SDK, июль 2023 — июль 2025.
Библиотека, на которой сегодня написана половина чат-интерфейсов в вебе. Человек задаёт вопрос, модель отвечает потоком, буквы появляются одна за другой. Кнопка «стоп» есть у всех.
Вопрос, который два года не имел ответа: а что происходит на сервере, когда её нажимают?
10 июля 2023 — Йоррит задаёт вопрос про деньги.
jvandenaardweg открывает issue #309:
In the current situation when a user cancels a stream, the server errors and there's no way to catch the cancellation. But I want to know how many tokens are generated when the user cancels the stream, so I can then store that information in my external database.
Сейчас, когда пользователь отменяет поток, сервер падает с ошибкой, и поймать отмену никак нельзя. А я хочу знать, сколько токенов сгенерировалось к моменту отмены, чтобы записать это в свою базу.
Обратите внимание, ради чего он пришёл. Не ради красоты. Ради счёта.
Хор.
Два года люди приходят и говорят одно и то же.
31 августа 2023, lassegit — снова про учёт:
I need a callback to be able to track token usage.
Мне нужен обратный вызов, чтобы считать расход токенов.
18 октября 2023, GorvGoyl — вопрос, который задают, когда ждать надоело:
What are some current workarounds till the team fixes the issue?
Какие есть обходные пути, пока команда это не починит?
20 января 2024, yvngjordi — полгода спустя:
Still having the same problem. Guess Vercel SDK is just limited
Всё та же проблема. Видимо, Vercel SDK просто ограничен.
12 марта 2024, winddpan, три слова:
Hope Vercel can solve it.
Надеюсь, Vercel это решит.
12 июля 2024 — приходит Ларс.
Через год после открытия отвечает Lars Grammel, ведущий разработчик AI SDK в Vercel:
The AI SDK functions support a
abortSignaloption that you can use to forward the abort.Функции AI SDK поддерживают параметр
abortSignal, через который можно передать отмену дальше.
Ответ верный. И он не про то.
abortSignal действительно останавливает генерацию у провайдера — это половина задачи. А Йоррит спрашивал про вторую: как узнать, что отменили, и сколько успело сгореть.
Через три недели verrannt формулирует это вежливо и убийственно:
Could you elaborate a bit how to achieve the desired functionality using
abortSignal? It is not at all clear to me how I would go about implementing it.Не могли бы вы пояснить, как получить нужное поведение через
abortSignal? Мне совершенно не ясно, как это реализовать.
17 сентября 2024 — разговор, которого ждали год.
xl0 предлагает конкретное:
It would be nice if
abortSignalcould triggeronFinishand pass as much data as possible to it.Хорошо бы, чтобы
abortSignalвызывалonFinishи передавал туда как можно больше данных.
Ларс отвечает в тот же день, и это лучший кадр всей истории:
this will be problematic because of typesafety and cause a lot of optionality checks. I would prefer something like
onAbort— would that work for you?так будет плохо из-за типобезопасности и породит кучу проверок на необязательность. Я бы предпочёл что-то вроде
onAbort— подойдёт?
Он не отмахивается. Он объясняет, почему предложенная форма плоха, и тут же предлагает правильную. Проектное решение принято за один день.
18 июля 2025 — onAbort.
Ещё через десять месяцев Ларс присылает PR #7394:
When streams are aborted (e.g. via chat
stop()), theonFinishcallback is not invoked. However, a callback is needed to update the stored messages.Когда поток отменяют (например, через
stop()в чате),onFinishне вызывается. А обратный вызов нужен, чтобы обновить сохранённые сообщения.
Fixes #6699, Fixes #309.
Между вопросом и ответом — два года и восемь дней. Между согласованным решением и его выходом — десять месяцев.
Здесь нет виноватых, и это важнее всего.
Ларс ответил через год не потому, что не читал, — а потому что ответ «используйте abortSignal» был технически верным и закрывал ту часть задачи, которая казалась главной. Когда стало ясно, что спрашивают про другое, он за один день согласовал правильную форму.
Десять месяцев до выхода — цена того, что AI SDK стоит у десятков тысяч людей, и новый обратный вызов там не «добавить функцию», а изменить контракт, на который все опираются.
А двадцать четыре комментария за два года — это не давление и не нытьё. Это люди, которые по одному приходили и обнаруживали, что их деньги утекают туда, где на них никто не смотрит.
Что это значит для нас
Найти все вызовы к платному внешнему сервису и проверить, доходит ли до него обрыв со стороны человека.
- Сигнал обрыва запроса передаётся в вызов модели
- Проверено не только сборкой: закрытая вкладка действительно останавливает генерацию
Выписать места, где тратятся чужие деньги без отдельного счётчика: токены, SMS, внешние вызовы, трафик.
- По каждому платному действию понятно, где увидеть его расход
- Есть хотя бы один случай, когда расход идёт, а счётчика нет — и он записан
Если после ответа человек переспрашивает «а как именно?» — вернуться к его исходному вопросу, а не повторять свой ответ подробнее.
- Переспрос воспринимается как сигнал, а не как невнимательность спрашивающего
Два года ушло на то, чтобы библиотека научилась сообщать: человек передумал.
Всё это время модели по всему миру дописывали ответы в пустоту — аккуратно, до последнего токена, никому.
Related lists
Найти в открытом проекте настоящую историю поломки, восстановить ход мысли по написанному и закончить проверкой своего кода
Люди и проекты из разборов — по поступкам, датам и ссылкам. Как определитель птиц: не оценивает, а помогает узнать, кого встретил
Пятеро разработчиков ERPNext по очереди чинили одну трещину, и никто из них не ошибался. Пока один не спросил: а почему это вообще работает? Оказалось — по случайному свойству чужой базы данных.
Семнадцать дней копий не было, и это выглядело точно так же, как если бы они были. Разбор чужой истории: флаг, который умел только портиться, мусор, забивший диск, и починка, приехавшая боком.
Карточка вида: чем живёт, как принимает чужаков, кто в нём работает. Наблюдения по коду, переписке и тому, что проект пишет о себе сам
comlink-python: проверки на отказ стучались в адрес, который сервер не защищает, — и пять месяцев скрывали настоящую ошибку подписи

