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

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 (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, 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, 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 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):

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):

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 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) 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, where every LLM interaction is normalized to standard Message types.
  • The streaming abstraction layer in 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 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. 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 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. 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →