Custom Providers
Extensions can register custom model providers viapi.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:examples/extensions/custom-provider-anthropic/examples/extensions/custom-provider-gitlab-duo/- Built-in reference:
packages/cursor/registers the experimental first-party Cursor provider with OAuth, model mapping, and a nativestreamSimpleadapter without a local proxy.
Table of Contents
- Example Extensions
- Quick Reference
- Override Existing Provider
- Register New Provider
- Unregister Provider
- OAuth Support
- Custom Streaming API
- Testing Your Implementation
- Config Reference
- Model Definition Reference
Quick Reference
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.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, specifymodels along with the required configuration.
If the model list comes from a remote endpoint, use an async extension factory:
models is provided, it replaces all existing models for that provider.
Unregister Provider
Usepi.unregisterProvider(name) to remove a provider that was previously registered via pi.registerProvider(name, ...):
/reload is required.
API Types
Theapi 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:
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 expectsAuthorization: Bearer <key> but doesn’t use a standard API, set authHeader: true:
OAuth Support
Add OAuth/SSO authentication that integrates with/login:
/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 addrefreshModels. Atomic calls it during the asynchronous model refresh used by the model picker and authentication flows:
signal retains its previous list. Use the provider-scoped store only when the catalog should persist across sessions.
OAuthLoginCallbacks
Thecallbacks 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, implementstreamSimple. 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 APImistral.d.ts/mistral.js- Mistral Conversations APIopenai-completions.d.ts/openai-completions.js- OpenAI Chat Completionsopenai-responses.d.ts/openai-responses.js- OpenAI Responses APIgoogle.d.ts/google.js- Google Generative AIamazon-bedrock.d.ts/amazon-bedrock.js- AWS Bedrock
Stream Pattern
All providers follow the same pattern:Event Types
Push events viastream.push() in this order:
-
{ type: "start", partial: output }- Stream started -
Content events (repeatable, track
contentIndexfor 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
-
{ type: "done", reason, message }or{ type: "error", reason, error }- Stream ended
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 tooutput.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
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.