# How to Implement Abort and Continue After Abort in pi-ai

> Learn to implement abort and continue after abort in pi-ai Cancel LLM requests with AbortSignal and resume conversations by appending the aborted message with a fresh AbortController.

- Repository: [Mario Zechner/pi-mono](https://github.com/badlogic/pi-mono)
- Tags: how-to-guide
- Published: 2026-02-16

---

**You can cancel active LLM requests in pi-ai by passing an `AbortSignal` to the stream options, then continue the conversation normally by appending the aborted message (with `stopReason: "aborted"`) to the context and issuing a new request with a fresh `AbortController`.**

The `pi-ai` library from the `badlogic/pi-mono` repository provides a unified streaming interface for multiple LLM providers. When building interactive applications, you need the ability to cancel in-flight generation and resume the conversation seamlessly. This guide explains how to implement abort and continue after abort in pi-ai using the native `AbortController` API.

## How Abort Works in pi-ai

### The AbortSignal Contract

Every provider in pi-ai respects the `signal` property in `ProviderStreamOptions`. When you pass `controller.signal` to the stream options, the provider monitors `options?.signal?.aborted` before each network read.

### Provider-Level Implementation

In [`packages/ai/src/providers/openai-completions.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/providers/openai-completions.ts), the provider checks the signal state:

```typescript
if (options?.signal?.aborted) {
    throw new Error("Request was aborted");
}
// ...
output.stopReason = options?.signal?.aborted ? "aborted" : "error";

```

Other providers—including [`openai-responses.ts`](https://github.com/badlogic/pi-mono/blob/main/openai-responses.ts), [`google-vertex.ts`](https://github.com/badlogic/pi-mono/blob/main/google-vertex.ts), [`google-gemini-cli.ts`](https://github.com/badlogic/pi-mono/blob/main/google-gemini-cli.ts), [`anthropic.ts`](https://github.com/badlogic/pi-mono/blob/main/anthropic.ts), and [`amazon-bedrock.ts`](https://github.com/badlogic/pi-mono/blob/main/amazon-bedrock.ts)—follow the same contract. The `stream()` function in [`packages/ai/src/stream.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/stream.ts) simply forwards the options to the selected provider, ensuring uniform abort handling across all LLM services.

## Implementing Abort in Your Application

To cancel a stream mid-generation, create an `AbortController` and pass its signal to the stream options:

```typescript
import { getModel } from "@mariozechner/pi-ai/src/models.js";
import { stream } from "@mariozechner/pi-ai/src/stream.js";

const model = getModel("openai", "gpt-4o-mini")!;
const controller = new AbortController();

const response = await stream(model, context, { 
  signal: controller.signal 
});

// Read tokens until a condition is met, then abort
let gathered = "";
for await (const ev of response) {
  if (ev.type === "text_delta") gathered += ev.delta;
  if (gathered.length >= 100) {
    controller.abort();          // <-- cancel the request
    break;
  }
}

const abortedMsg = await response.result();
console.log(abortedMsg.stopReason); // "aborted"

```

When the signal triggers, the provider throws an abort error, which the stream wrapper converts into an `AssistantMessage` with `stopReason: "aborted"` and (usually) an empty `content` array.

## Continuing After an Abort

The abort does not corrupt the conversation history. To continue, append the aborted message to your context and send a new request with a fresh `AbortController`:

```typescript
// 1. Preserve the aborted message in context
ctx.messages.push(abortedMsg);

// 2. Add the follow-up user prompt
ctx.messages.push({
  role: "user",
  content: "Please continue from where you left off.",
  timestamp: Date.now(),
});

// 3. Create a new request with a fresh controller
const newController = new AbortController();
const followUp = await stream(model, ctx, { 
  signal: newController.signal 
});

const result = await followUp.result();
console.log(result.stopReason); // "stop" (normal completion)

```

This pattern is validated in [`packages/ai/test/abort.test.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/test/abort.test.ts), which confirms that aborted messages preserve conversation continuity and that subsequent requests work independently of previous aborts.

## Key Implementation Files

| Component | File Path | Role |
|-----------|-----------|------|
| Stream dispatcher | [`packages/ai/src/stream.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/stream.ts) | Forwards options to providers |
| OpenAI provider | [`packages/ai/src/providers/openai-completions.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/providers/openai-completions.ts) | Checks `signal?.aborted` and sets `stopReason` |
| Google Vertex | [`packages/ai/src/providers/google-vertex.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/providers/google-vertex.ts) | Abort contract for Vertex AI |
| Anthropic | [`packages/ai/src/providers/anthropic.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/providers/anthropic.ts) | Abort handling for Claude |
| Amazon Bedrock | [`packages/ai/src/providers/amazon-bedrock.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/providers/amazon-bedrock.ts) | Abort support for Bedrock |
| Test suite | [`packages/ai/test/abort.test.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/test/abort.test.ts) | Validates abort and continue workflows |

## Summary

- Pass an `AbortSignal` via stream options to enable cancellation in `pi-ai`.
- Providers check `options?.signal?.aborted` and convert the abort into a message with `stopReason: "aborted"`.
- Append the aborted message to your conversation context to maintain chronological order.
- Issue subsequent requests with a fresh `AbortController` to continue the conversation normally.
- All built-in providers (OpenAI, Google, Anthropic, Amazon) implement the same abort contract.

## Frequently Asked Questions

### Does aborting a request corrupt the conversation history in pi-ai?

No. The aborted request returns a valid `AssistantMessage` with `stopReason: "aborted"`. When you append this message to your context, the conversation history remains intact. The next request receives the full message sequence including the aborted turn, allowing the model to continue as if the previous turn simply produced no output.

### Can I reuse the same AbortController after calling abort()?

No. Once an `AbortController` is aborted, its signal remains permanently in the aborted state. To continue the conversation after an abort, you must create a new `AbortController` instance for the subsequent request. The test suite in [`packages/ai/test/abort.test.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/test/abort.test.ts) explicitly validates this pattern of using fresh controllers for follow-up requests.

### Which providers support abort and continue in pi-ai?

All built-in providers support abort via `AbortSignal`: OpenAI ([`openai-completions.ts`](https://github.com/badlogic/pi-mono/blob/main/openai-completions.ts), [`openai-responses.ts`](https://github.com/badlogic/pi-mono/blob/main/openai-responses.ts)), Google Vertex ([`google-vertex.ts`](https://github.com/badlogic/pi-mono/blob/main/google-vertex.ts)), Google Gemini CLI ([`google-gemini-cli.ts`](https://github.com/badlogic/pi-mono/blob/main/google-gemini-cli.ts)), Anthropic ([`anthropic.ts`](https://github.com/badlogic/pi-mono/blob/main/anthropic.ts)), and Amazon Bedrock ([`amazon-bedrock.ts`](https://github.com/badlogic/pi-mono/blob/main/amazon-bedrock.ts)). Each provider checks `options?.signal?.aborted` and returns `stopReason: "aborted"` when cancelled.

### How does the abort signal propagate through the stream?

The `stream()` function in [`packages/ai/src/stream.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/stream.ts) forwards the `options` object (including `signal`) to the selected provider. The provider then checks `signal?.aborted` before each network read. If aborted, the provider throws an error that is caught by the stream wrapper, which sets `output.stopReason` to `"aborted"` and closes the stream gracefully with a final error event.