Skip to main content

Provider streaming API

Custom Streaming API

For providers with non-standard APIs, implement streamSimple. Study the existing API implementations before writing your own: Reference implementations: Atomic uses provider implementations from its installed @bastani/pi-ai dependency. The streaming implementations behind the api field live under node_modules/@bastani/pi-ai/dist/api/, including:
  • anthropic-messages.d.ts / anthropic-messages.js - Anthropic Messages API
  • mistral-conversations.d.ts / mistral-conversations.js - Mistral Conversations/Chat streaming
  • openai-completions.d.ts / openai-completions.js - OpenAI Chat Completions
  • openai-responses.d.ts / openai-responses.js - OpenAI Responses API
  • google-generative-ai.d.ts / google-generative-ai.js - Google Generative AI
  • bedrock-converse-stream.d.ts / bedrock-converse-stream.js - Amazon Bedrock Converse API Per-vendor provider configurations (base URLs, auth, model catalogs) live under dist/providers/, for example anthropic.d.ts / anthropic.js and mistral.d.ts / mistral.js.

Stream Pattern

All providers follow the same pattern:

Event Types

Push events via stream.push() in this order:
  1. { type: "start", partial: output } - Stream started
  2. Content events (repeatable, track contentIndex for each block):
    • { type: "text_start", contentIndex, partial } - Text block started
    • { type: "text_delta", contentIndex, delta, partial } - Text chunk
    • { type: "text_end", contentIndex, content, partial } - Text block ended
    • { type: "thinking_start", contentIndex, partial } - Thinking started
    • { type: "thinking_delta", contentIndex, delta, partial } - Thinking chunk
    • { type: "thinking_end", contentIndex, content, partial } - Thinking ended
    • { type: "toolcall_start", contentIndex, partial } - Tool call started
    • { type: "toolcall_delta", contentIndex, delta, partial } - Tool call JSON chunk
    • { type: "toolcall_end", contentIndex, toolCall, partial } - Tool call ended
  3. { type: "done", reason, message } or { type: "error", reason, error } - Stream ended
The partial field in each event contains the current AssistantMessage state. Update output.content as you receive data, then include output as the partial.

Stop Reasons

StopReason is "pending" | "stop" | "length" | "toolUse" | "error" | "aborted". Start the partial message at "pending". It is the reason every in-flight message carries, and it says the terminal event has not arrived yet — it is not a default standing in for "stop". Set the real reason when the provider says the turn ended, then push done with it. Two checks belong immediately before done:
  • a stream that reached the end while still "pending" never received a terminal event, so raise rather than report a stop that did not happen;
  • "error" and "aborted" are failures, so raise them with output.errorMessage and let the catch push an error event.
done accepts only "stop", "length", and "toolUse", which is exactly what those two checks leave, so the as "stop" | "length" | "toolUse" cast older implementations used is no longer needed. Map each raw reason your provider can send onto one of the five terminal values, and raise on one you do not recognise rather than falling back to "stop". This is what the built-in providers do: an unmapped reason becomes a provider error naming the raw value, so a new truncation or safety signal is visible instead of arriving as a turn that looks like it finished normally. The optional rawStopReason field on AssistantMessage is where the provider’s own string belongs when you want to keep it.

Content Blocks

Add content blocks to output.content as they arrive:

Tool Calls

Tool calls require accumulating JSON and parsing:

Usage and Cost

Update usage from API response and calculate cost:
calculateCost() selects one rate set for the whole request. Aggregate input is usage.input + usage.cacheRead + usage.cacheWrite; a tier applies only when that sum is strictly greater than inputTokensAbove, and the matching tier with the highest threshold wins. Every tier must provide complete input, output, cacheRead, and cacheWrite rates. Extension-registered models preserve these tiers, and matching models.json modelOverrides use the same replacement rules described in Custom Models.

Registration

Register your stream function: