Skip to main content

Custom Providers

Extensions can register custom model providers via pi.registerProvider(). This enables:
  • Proxies - Route requests through corporate proxies or API gateways
  • Custom endpoints - Use self-hosted or private model deployments
  • OAuth/SSO - Add authentication flows for enterprise providers
  • Custom APIs - Implement streaming for non-standard LLM APIs

Example Extensions

See these complete provider examples:

Table of Contents

Quick Reference

The extension factory can also be async. For dynamic model discovery, fetch and register models in the factory instead of session_start. Atomic waits for the factory before startup continues, so the provider is available during interactive startup and to atomic --list-models.

Override Existing Provider

The simplest use case: redirect an existing provider through a proxy.
When only baseUrl and/or headers are provided (no models), all existing models for that provider are preserved with the new endpoint.

Register New Provider

To add a completely new provider, specify models along with the required configuration. If the model list comes from a remote endpoint, use an async extension factory:
This registers the fetched models before startup finishes.
When models is provided, it replaces all existing models for that provider.

Unregister Provider

Use pi.unregisterProvider(name) to remove a provider that was previously registered via pi.registerProvider(name, ...):
Unregistering removes that provider’s dynamic models, API key fallback, OAuth provider registration, and custom stream handler registrations. Any built-in models or provider behavior that were overridden are restored. Calls made after the initial extension load phase are applied immediately, so no /reload is required.

API Types

The api field determines which streaming implementation is used: Most OpenAI-compatible providers work with openai-completions. Use model-level thinkingLevelMap for model-specific thinking levels, and compat for provider quirks:
Use openrouter for OpenRouter-style reasoning: { effort } controls. Use together for Together-style reasoning: { enabled } controls; with supportsReasoningEffort, it also sends reasoning_effort. Use qwen-chat-template for local Qwen-compatible servers that read chat_template_kwargs.enable_thinking and need preserve_thinking. Use cacheControlFormat: "anthropic" for OpenAI-compatible providers that expose Anthropic-style prompt caching via cache_control on the system prompt, last tool definition, and last user/assistant text content. Use mistral-conversations for native Mistral models. If you intentionally route a Mistral-compatible or custom endpoint through openai-completions, set the required compat flags explicitly.

Auth Header

If your provider expects Authorization: Bearer <key> but doesn’t use a standard API, set authHeader: true:

OAuth Support

Add OAuth/SSO authentication that integrates with /login:
After registration, users can authenticate via /login corporate-ai. Existing extension OAuth definitions keep their login, refreshToken, getApiKey, and optional modifyModels methods. OAuth refresh is serialized so concurrent requests do not overwrite each other’s credentials.

Dynamic model catalog refresh

Providers whose catalogs change at runtime can add refreshModels. Atomic calls it during the asynchronous model refresh used by the model picker and authentication flows:
The current catalog stays readable while refresh is pending. Successful provider results are applied independently; a provider that fails, times out, or observes an aborted signal retains its previous list. Use the provider-scoped store only when the catalog should persist across sessions.

OAuthLoginCallbacks

The callbacks object provides three ways to authenticate:

OAuthCredentials

Credentials are persisted in ~/.atomic/agent/auth.json (legacy ~/.pi/agent/auth.json may be read for compatibility):

Custom Streaming API

For providers with non-standard APIs, implement streamSimple. Study the existing provider implementations before writing your own: Reference implementations: Atomic uses provider implementations from its installed @earendil-works/pi-ai dependency. Inspect the compiled declarations and JavaScript under node_modules/@earendil-works/pi-ai/dist/providers/, including:
  • anthropic.d.ts / anthropic.js - Anthropic Messages API
  • mistral.d.ts / mistral.js - Mistral Conversations API
  • openai-completions.d.ts / openai-completions.js - OpenAI Chat Completions
  • openai-responses.d.ts / openai-responses.js - OpenAI Responses API
  • google.d.ts / google.js - Google Generative AI
  • amazon-bedrock.d.ts / amazon-bedrock.js - AWS Bedrock

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.

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:

Testing Your Implementation

Test your provider against focused tests that mirror Atomic’s provider contract. If you are working from the source checkout, note that provider internals come from @earendil-works/pi-ai; this monorepo does not contain a packages/ai/test directory to copy from directly: Run tests with your provider/model pairs to verify compatibility.

Config Reference

Model Definition Reference

The cost shape is equivalent to Model<Api>["cost"]. Base rates and every tier are complete rate sets. When multiple thresholds match, calculateCost() uses the highest threshold and applies that tier to all four cost buckets for the request. openrouter sends reasoning: { effort }. deepseek sends thinking: { type: "enabled" | "disabled" } and reasoning_effort when enabled. together sends reasoning: { enabled } and also reasoning_effort when supportsReasoningEffort is enabled. qwen is for DashScope-style top-level enable_thinking. Use qwen-chat-template for local Qwen-compatible servers that read chat_template_kwargs.enable_thinking and need preserve_thinking. Use chat-template for configurable chat_template_kwargs, for example DeepSeek V3.x behind vLLM with chatTemplateKwargs: { "thinking": { "$var": "thinking.enabled" } }. cacheControlFormat: "anthropic" applies Anthropic-style cache_control markers to the system prompt, last tool definition, and last user/assistant text content. For openai-responses providers, set compat.sessionAffinityFormat to "openai" for session_id plus x-client-request-id, "openai-nosession" to omit session_id while retaining x-client-request-id, or "openrouter" for x-session-id. Responses-compatible providers may also set supportsToolSearch when they support deferred tool loading.