# How Cross-Provider Handoffs Work in pi-ai: A Unified Streaming Approach

> Discover how pi-ai achieves seamless cross-provider handoffs. Our unified streaming API normalizes LLM interactions for fluid conversation flow between models.

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

---

**Cross-provider handoffs in pi-ai work by normalizing every LLM interaction through a unified message schema and streaming API, allowing conversations generated by one provider to be seamlessly consumed by another.**

The `pi-ai` package within the `badlogic/pi-mono` repository implements a provider-agnostic abstraction layer that enables sophisticated cross-provider handoffs. By treating every LLM as a generic streaming endpoint, the library allows developers to switch between OpenAI, Anthropic, Google, and other providers mid-conversation without losing context or message integrity.

## The Unified Message Schema That Enables Cross-Provider Handoffs

All provider-specific formats are mapped to a single, strongly-typed message structure defined in [`packages/ai/src/types.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/types.ts) (lines 64-92). This schema includes:

- **`AssistantMessage`** – The standard container for assistant responses, supporting text, tool calls, and thinking blocks
- **`ToolCall`** – Normalized tool invocations with a unified `id` field (plain string) that abstracts away provider-specific formatting like OpenAI's pipe-separated identifiers
- **`ThinkingContent`** – Captures reasoning traces from models like Claude, normalized across providers

Because every provider emits and consumes this identical schema, a `Message[]` array generated by Anthropic can be passed directly to OpenAI without transformation.

## The Streaming Abstraction Layer

The core normalization logic resides in [`packages/ai/src/stream.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/stream.ts), which exposes `stream`, `streamSimple`, `complete`, and `completeSimple` functions. Each provider registers an implementation of `stream`/`streamSimple` through the API registry in [`packages/ai/src/api-registry.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/api-registry.ts), returning a standardized `AssistantMessageEventStream`.

When invoking `completeSimple`, the function:
1. Resolves the provider-specific implementation via the registry
2. Streams the normalized request
3. Aggregates events into a final `AssistantMessage` containing content blocks, tool calls, and metadata

This abstraction ensures that calling code remains unaware of whether it's communicating with OpenAI, Anthropic, or Google Vertex AI.

## Cross-Provider Handoff Implementation

The integration test in [`packages/ai/test/cross-provider-handoff.test.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/test/cross-provider-handoff.test.ts) demonstrates the handoff mechanism by generating conversation fixtures from multiple providers and verifying that any provider can consume the mixed history.

### Generating Provider-Specific Fixtures

The `generateContext` function (lines 55-84) builds a complete conversation cycle for each `(provider, model)` pair:

1. **Initial request** – A user message requesting a tool call
2. **Tool invocation** – Uses `completeSimple` to obtain a real tool-call ID and thinking block from the provider
3. **Tool result** – Simulates the tool execution with a `toolResult` message
4. **Final response** – Obtains the assistant's closing remark

This process ensures that fixtures contain provider-specific artifacts (like Anthropic's thinking blocks or OpenAI's tool-call formats) that must be normalized during handoff.

### Concatenating Mixed-Origin Conversations

When testing a target model, the test gathers all messages from other providers' fixtures (lines 51-55):

```typescript
const otherMessages: Message[] = [];
for (const [label, fixture] of Object.entries(fixtures)) {
  if (label === targetLabel) continue;
  otherMessages.push(...fixture);
}

```

The `allMessages` array (lines 62-70) combines these mixed-origin messages with a final user prompt, creating a conversation history that spans multiple LLM providers.

### Executing the Handoff

The test passes the concatenated array to `completeSimple` for the target provider (lines 90-126):

```typescript
const model = getModel(targetProvider, targetModel)!;
const response = await completeSimple(
  model,
  { messages: allMessages },
  { apiKey: process.env[`${targetProvider.toUpperCase()}_API_KEY`] }
);

```

If the target model successfully parses the mixed message history (including tool-call IDs and thinking blocks from other providers), it returns a valid assistant response. Failures indicate normalization gaps that require fixes in the provider implementations.

## Normalizing Provider Differences

The abstraction layer handles three major provider discrepancies that would otherwise break cross-provider handoffs:

**Tool-Call ID Formats** – While the unified `ToolCall.id` is a plain string, providers like OpenAI use pipe-separated identifiers internally. Each provider's implementation normalizes these to the standard format before emitting events, and denormalizes them when sending requests upstream.

**Thinking Blocks** – Anthropic models emit native `ThinkingContent` blocks, while other providers do not. The `requiresThinkingAsText` flag in provider configurations (referenced in [`types.ts`](https://github.com/badlogic/pi-mono/blob/main/types.ts) lines 41-44) determines whether thinking content should be wrapped in `<thinking>` text tags for providers that cannot natively handle reasoning blocks.

**Metadata and Headers** – The `StreamOptions` interface (lines 58-96 of [`types.ts`](https://github.com/badlogic/pi-mono/blob/main/types.ts)) allows each provider to accept provider-specific parameters (like temperature, top-p, or custom headers) without breaking the shared function signatures. This ensures that `completeSimple` can accept an `apiKey` and other options while remaining agnostic to the underlying provider.

## Summary

- **pi-ai** implements cross-provider handoffs through a **unified message schema** defined in [`packages/ai/src/types.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/types.ts), where every LLM interaction is normalized to standard `Message` types.
- The **streaming abstraction layer** in [`packages/ai/src/stream.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/stream.ts) exposes provider-agnostic functions like `completeSimple` that route requests through the API registry without exposing provider-specific details.
- The **cross-provider handoff test** in [`packages/ai/test/cross-provider-handoff.test.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/test/cross-provider-handoff.test.ts) validates interoperability by generating conversation fixtures from multiple providers, concatenating them, and verifying that any target provider can process the mixed message history.
- **Provider differences** are normalized at the edges: tool-call IDs are converted to plain strings, thinking blocks are wrapped in text when necessary, and provider-specific options are handled through the extensible `StreamOptions` interface.

## Frequently Asked Questions

### What is a cross-provider handoff in pi-ai?

A cross-provider handoff is the ability to take a conversation history generated by one LLM provider (such as Anthropic) and seamlessly continue that conversation using a different provider (such as OpenAI or Google). In pi-ai, this works because all providers implement the same unified message schema and streaming interface, allowing message arrays to be passed between providers without transformation.

### How does pi-ai handle different message formats between OpenAI and Anthropic?

pi-ai normalizes all provider-specific formats into a single message schema defined in [`packages/ai/src/types.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/types.ts). For example, while Anthropic uses native thinking blocks and OpenAI uses specific tool-call ID formats, pi-ai's provider implementations convert these to standard `ThinkingContent` and `ToolCall` types at the boundary. This normalization ensures that a message created by Anthropic's API is structurally identical to one created by OpenAI's API within the pi-ai system.

### Can tool calls from one provider be executed by another provider in pi-ai?

Yes, tool calls generated by one provider can be understood and processed by another provider during a cross-provider handoff. The [`cross-provider-handoff.test.ts`](https://github.com/badlogic/pi-mono/blob/main/cross-provider-handoff.test.ts) file specifically tests this scenario by generating tool-call fixtures from multiple providers, concatenating them into a mixed conversation history, and verifying that the target provider can parse the tool-call IDs and context. The unified `ToolCall` type ensures that tool-call identifiers remain valid across provider boundaries.

### Where is the cross-provider handoff logic tested in the repository?

The cross-provider handoff logic is tested in [`packages/ai/test/cross-provider-handoff.test.ts`](https://github.com/badlogic/pi-mono/blob/main/packages/ai/test/cross-provider-handoff.test.ts). This integration test generates conversation fixtures for every supported provider and model combination using the `generateContext` function, then concatenates messages from different providers into a single array. Finally, it passes this mixed-origin message history to `completeSimple` for each target provider to verify that cross-provider handoffs work correctly. The test validates that tool calls, thinking blocks, and message metadata transfer seamlessly between providers.