Cancel mid-stream

A long agent turn with several tool calls can run for ten or more seconds. Users who realize they asked the wrong question should be able to stop it. useChat exposes a stop function that aborts the HTTP stream. The AI SDK wires that abort signal into streamText so in-flight tool calls terminate too.

components/chat/chat-panel.tsx
tsx
const { messages, sendMessage, status, stop } = useChat({
  api: '/api/chat',
});

const isLoading = status === 'submitted' || status === 'streaming';

return (
  <div className="flex gap-2">
    <input
      value={input}
      onChange={(e) => setInput(e.target.value)}
      disabled={isLoading}
    />
    {isLoading ? (
      <button type="button" onClick={() => stop()}>
        Stop
      </button>
    ) : (
      <button type="submit" disabled={!input.trim()}>
        Send
      </button>
    )}
  </div>
);

While streaming, show Stop. When idle, show Send. stop() aborts the request, and the SDK surfaces the cancellation to the route handler through the underlying AbortSignal.

lib/tools/fetch-url.ts
typescript
execute: async ({ url }, { abortSignal }) => {
  const res = await fetch(url, {
    signal: abortSignal ?? AbortSignal.timeout(8_000),
  });
  if (!res.ok) {
    return { error: `Fetch failed with status ${res.status}`, url };
  }
  const body = await res.text();
  return { url, body: body.slice(0, MAX_BYTES) };
}

Every execute function receives an abortSignal. Pass it through to fetch so cancelling the chat also cancels any in-flight tool work. No orphaned requests.

Tokens streamed before the abort are billed as usual. Tokens after the abort are not generated at all because the provider sees the stream close. Cancelling saves money only if you stop early. For expensive tools, pair the stop button with a timeout on the tool itself so a frozen provider cannot run up the bill in the background.