@bastani/atomic 0.9.19-alpha.6 → 0.9.19-alpha.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +2 -2
  3. package/dist/builtin/intercom/package.json +1 -1
  4. package/dist/builtin/mcp/package.json +1 -1
  5. package/dist/builtin/subagents/package.json +1 -1
  6. package/dist/builtin/web-access/CHANGELOG.md +6 -0
  7. package/dist/builtin/web-access/index.bundle.mjs +22 -3
  8. package/dist/builtin/web-access/package.json +1 -1
  9. package/dist/builtin/workflows/CHANGELOG.md +19 -0
  10. package/dist/builtin/workflows/README.md +1 -1
  11. package/dist/builtin/workflows/builtin/adversarial-verification.js +2 -2
  12. package/dist/builtin/workflows/builtin/{chunk-vhmnzdj9.js → chunk-1jth021m.js} +1 -1
  13. package/dist/builtin/workflows/builtin/{chunk-p8jjwfg6.js → chunk-2377r57g.js} +4 -2
  14. package/dist/builtin/workflows/builtin/{chunk-hbnwv56f.js → chunk-31j2gs7h.js} +2 -2
  15. package/dist/builtin/workflows/builtin/{chunk-n8zysf1t.js → chunk-ngkqkzej.js} +1 -1
  16. package/dist/builtin/workflows/builtin/{chunk-1nbq96fh.js → chunk-nkrafh9s.js} +1 -1
  17. package/dist/builtin/workflows/builtin/{chunk-1kx4aggw.js → chunk-wf741bv3.js} +2 -2
  18. package/dist/builtin/workflows/builtin/{chunk-t6x2d78s.js → chunk-z29qbehr.js} +1 -1
  19. package/dist/builtin/workflows/builtin/generate-and-filter.js +2 -2
  20. package/dist/builtin/workflows/builtin/goal.js +3 -3
  21. package/dist/builtin/workflows/builtin/index.js +7 -7
  22. package/dist/builtin/workflows/builtin/ralph.js +3 -3
  23. package/dist/builtin/workflows/builtin/tournament.js +2 -2
  24. package/dist/builtin/workflows/package.json +1 -1
  25. package/dist/builtin/workflows/src/extension/index.bundle.mjs +590 -256
  26. package/dist/builtin/workflows/src/index.js +67 -19
  27. package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +2 -0
  28. package/dist/modes/interactive/components/task-live-transcript.d.ts +1 -1
  29. package/dist/modes/interactive/components/task-live-transcript.d.ts.map +1 -1
  30. package/dist/modes/interactive/components/task-live-transcript.js +19 -11
  31. package/dist/modes/interactive/components/task-live-transcript.js.map +1 -1
  32. package/docs/background-tasks.md +1 -1
  33. package/docs/build.md +51 -0
  34. package/docs/changelog.mdx +8 -8
  35. package/docs/compaction/reference.md +267 -0
  36. package/docs/compaction.md +36 -219
  37. package/docs/computer-use.md +35 -0
  38. package/docs/containerization.md +2 -0
  39. package/docs/custom-provider/api-reference.md +145 -0
  40. package/docs/custom-provider/oauth.md +131 -0
  41. package/docs/custom-provider/override.md +34 -0
  42. package/docs/custom-provider/registration.md +163 -0
  43. package/docs/custom-provider/streaming.md +214 -0
  44. package/docs/custom-provider.md +43 -607
  45. package/docs/development.md +76 -0
  46. package/docs/docs.json +199 -77
  47. package/docs/environment-variables.md +1 -1
  48. package/docs/extensions/api-reference.md +1001 -0
  49. package/docs/extensions/authoring.md +676 -0
  50. package/docs/extensions/events.md +793 -0
  51. package/docs/extensions/examples.md +94 -0
  52. package/docs/extensions/ui.md +470 -0
  53. package/docs/extensions.md +270 -2768
  54. package/docs/getting-started/authentication.md +55 -0
  55. package/docs/getting-started/first-session.md +172 -0
  56. package/docs/getting-started/installation.md +154 -0
  57. package/docs/getting-started/project-instructions.md +43 -0
  58. package/docs/guides/configuration.md +65 -0
  59. package/docs/guides/intercom.md +24 -0
  60. package/docs/guides/non-interactive.md +33 -0
  61. package/docs/guides/subagents.md +24 -0
  62. package/docs/guides/workflows.md +27 -0
  63. package/docs/guides.md +41 -0
  64. package/docs/index.md +13 -45
  65. package/docs/intercom/operations.md +178 -0
  66. package/docs/intercom/reference.md +146 -0
  67. package/docs/intercom.md +54 -259
  68. package/docs/json.md +17 -0
  69. package/docs/llama-cpp.md +5 -0
  70. package/docs/models/artificial-analysis-index.md +89 -0
  71. package/docs/models/evals.md +2 -2
  72. package/docs/models/model-selection.md +3 -3
  73. package/docs/models/pareto-efficiency.md +2 -2
  74. package/docs/models/reference.md +601 -0
  75. package/docs/models.md +45 -550
  76. package/docs/packages/authoring.md +88 -0
  77. package/docs/packages/reference.md +43 -0
  78. package/docs/packages.md +28 -107
  79. package/docs/programmatic.md +29 -0
  80. package/docs/prompt-templates.md +11 -0
  81. package/docs/providers/reference.md +23 -0
  82. package/docs/providers.md +10 -15
  83. package/docs/quickstart.md +44 -238
  84. package/docs/reference/cli.md +252 -0
  85. package/docs/reference.md +52 -0
  86. package/docs/rpc/examples.md +64 -0
  87. package/docs/rpc/extension-ui.md +198 -0
  88. package/docs/rpc/protocol.md +1341 -0
  89. package/docs/rpc.md +109 -1346
  90. package/docs/sdk/reference.md +1090 -0
  91. package/docs/sdk.md +56 -1002
  92. package/docs/session-format.md +2 -0
  93. package/docs/sessions.md +6 -10
  94. package/docs/settings.md +6 -45
  95. package/docs/skills/authoring.md +95 -0
  96. package/docs/skills/reference.md +60 -0
  97. package/docs/skills.md +20 -122
  98. package/docs/subagents/authoring.md +37 -0
  99. package/docs/subagents/reference.md +60 -0
  100. package/docs/subagents.md +39 -95
  101. package/docs/terminal-setup.md +1 -1
  102. package/docs/themes/reference.md +176 -0
  103. package/docs/themes.md +21 -135
  104. package/docs/tmux.md +5 -0
  105. package/docs/tools/edit.md +5 -0
  106. package/docs/tools.md +5 -0
  107. package/docs/tui/reference.md +343 -0
  108. package/docs/tui.md +23 -286
  109. package/docs/usage.md +26 -209
  110. package/docs/web-access.md +2 -0
  111. package/docs/windows.md +2 -1
  112. package/docs/workflows/api-reference.md +11 -4
  113. package/docs/workflows/authoring.md +4 -2
  114. package/docs/workflows/builtins.md +2 -2
  115. package/docs/workflows/operations.md +15 -10
  116. package/docs/workflows/reliable-design.md +21 -3
  117. package/docs/workflows/verification.md +8 -0
  118. package/npm-shrinkwrap.json +32 -32
  119. package/package.json +3 -3
@@ -1,3 +1,8 @@
1
+ ---
2
+ title: "Custom providers"
3
+ description: "Implement a provider API or OAuth flow Atomic does not ship."
4
+ ---
5
+
1
6
  # Custom Providers
2
7
 
3
8
  Extensions can register custom model providers via `pi.registerProvider()`. This enables:
@@ -7,6 +12,18 @@ Extensions can register custom model providers via `pi.registerProvider()`. This
7
12
  - **OAuth/SSO** - Add authentication flows for enterprise providers
8
13
  - **Custom APIs** - Implement streaming for non-standard LLM APIs
9
14
 
15
+ ## Where to go next
16
+
17
+ This page gets you to a first working provider extension. Each part of the job has its own page:
18
+
19
+ - [Override an existing provider](/custom-provider/override) — change a provider Atomic already ships.
20
+ - [Register a provider](/custom-provider/registration) — register and unregister a provider, and the API types it implements.
21
+ - [Provider OAuth](/custom-provider/oauth) — login callbacks, credential storage, and dynamic catalog refresh.
22
+ - [Provider streaming API](/custom-provider/streaming) — events, content blocks, tool calls, stop reasons, and usage.
23
+ - [Provider API reference](/custom-provider/api-reference) — provider config and model definition contracts.
24
+
25
+ If you only need to add a model for an API Atomic already speaks, use [Custom models](/models) instead.
26
+
10
27
  ## Example Extensions
11
28
 
12
29
  See these complete provider examples:
@@ -18,14 +35,14 @@ See these complete provider examples:
18
35
 
19
36
  - [Example Extensions](#example-extensions)
20
37
  - [Quick Reference](#quick-reference)
21
- - [Override Existing Provider](#override-existing-provider)
22
- - [Register New Provider](#register-new-provider)
23
- - [Unregister Provider](#unregister-provider)
24
- - [OAuth Support](#oauth-support)
25
- - [Custom Streaming API](#custom-streaming-api)
38
+ - [Override Existing Provider](/custom-provider/override#override-existing-provider)
39
+ - [Register New Provider](/custom-provider/registration#register-new-provider)
40
+ - [Unregister Provider](/custom-provider/registration#unregister-provider)
41
+ - [OAuth Support](/custom-provider/oauth#oauth-support)
42
+ - [Custom Streaming API](/custom-provider/streaming#custom-streaming-api)
26
43
  - [Testing Your Implementation](#testing-your-implementation)
27
- - [Config Reference](#config-reference)
28
- - [Model Definition Reference](#model-definition-reference)
44
+ - [Config Reference](/custom-provider/api-reference#config-reference)
45
+ - [Model Definition Reference](/custom-provider/api-reference#model-definition-reference)
29
46
 
30
47
  ## Quick Reference
31
48
 
@@ -63,521 +80,71 @@ The extension factory can also be `async`. For dynamic model discovery, fetch an
63
80
 
64
81
  ## Override Existing Provider
65
82
 
66
- The simplest use case: redirect an existing provider through a proxy.
67
-
68
- ```typescript
69
- // All Anthropic requests now go through your proxy
70
- pi.registerProvider("anthropic", {
71
- baseUrl: "https://proxy.example.com"
72
- });
73
-
74
- // Add custom headers to OpenAI requests
75
- pi.registerProvider("openai", {
76
- headers: {
77
- "X-Custom-Header": "value"
78
- }
79
- });
80
-
81
- // Both baseUrl and headers
82
- pi.registerProvider("google", {
83
- baseUrl: "https://ai-gateway.corp.com/google",
84
- headers: {
85
- "X-Corp-Auth": "$CORP_AUTH_TOKEN" // resolves from env; omit $ for a literal
86
- }
87
- });
88
- ```
89
-
90
- When only `baseUrl` and/or `headers` are provided (no `models`), all existing models for that provider are preserved with the new endpoint.
83
+ Moved to [Override an existing provider](/custom-provider/override#override-existing-provider).
91
84
 
92
85
  ## Register New Provider
93
86
 
94
- To add a completely new provider, specify `models` along with the required configuration.
95
-
96
- If the model list comes from a remote endpoint, use an async extension factory:
97
-
98
- ```typescript
99
- import type { ExtensionAPI } from "@bastani/atomic";
100
-
101
- export default async function (pi: ExtensionAPI) {
102
- const response = await fetch("http://localhost:1234/v1/models");
103
- const payload = (await response.json()) as {
104
- data: Array<{
105
- id: string;
106
- name?: string;
107
- context_window?: number;
108
- max_tokens?: number;
109
- }>;
110
- };
111
-
112
- pi.registerProvider("local-openai", {
113
- baseUrl: "http://localhost:1234/v1",
114
- apiKey: "$LOCAL_OPENAI_API_KEY",
115
- api: "openai-completions",
116
- models: payload.data.map((model) => ({
117
- id: model.id,
118
- name: model.name ?? model.id,
119
- reasoning: false,
120
- input: ["text"],
121
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
122
- contextWindow: model.context_window ?? 128000,
123
- maxTokens: model.max_tokens ?? 4096,
124
- })),
125
- });
126
- }
127
- ```
128
-
129
- This registers the fetched models before startup finishes.
130
-
131
- ```typescript
132
- pi.registerProvider("my-llm", {
133
- baseUrl: "https://api.my-llm.com/v1",
134
- apiKey: "$MY_LLM_API_KEY", // env var reference; omit $ for a literal value
135
- api: "openai-completions", // which streaming API to use
136
- models: [
137
- {
138
- id: "my-llm-large",
139
- name: "My LLM Large",
140
- reasoning: true, // supports extended thinking
141
- input: ["text", "image"],
142
- cost: {
143
- input: 3.0, // $/million tokens
144
- output: 15.0,
145
- cacheRead: 0.3,
146
- cacheWrite: 3.75
147
- },
148
- contextWindow: 200000,
149
- maxTokens: 16384
150
- }
151
- ]
152
- });
153
- ```
154
-
155
- When `models` is provided, it **replaces** all existing models for that provider.
87
+ Moved to [Register a provider](/custom-provider/registration#register-new-provider).
156
88
 
157
89
  ## Unregister Provider
158
90
 
159
- Use `pi.unregisterProvider(name)` to remove a provider that was previously registered via `pi.registerProvider(name, ...)`:
160
-
161
- ```typescript
162
- // Register
163
- pi.registerProvider("my-llm", {
164
- baseUrl: "https://api.my-llm.com/v1",
165
- apiKey: "$MY_LLM_API_KEY",
166
- api: "openai-completions",
167
- models: [
168
- {
169
- id: "my-llm-large",
170
- name: "My LLM Large",
171
- reasoning: true,
172
- input: ["text", "image"],
173
- cost: { input: 3.0, output: 15.0, cacheRead: 0.3, cacheWrite: 3.75 },
174
- contextWindow: 200000,
175
- maxTokens: 16384
176
- }
177
- ]
178
- });
179
-
180
- // Later, remove it
181
- pi.unregisterProvider("my-llm");
182
- ```
183
-
184
- 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.
185
-
186
- Calls made after the initial extension load phase are applied immediately, so no `/reload` is required.
91
+ Moved to [Register a provider](/custom-provider/registration#unregister-provider).
187
92
 
188
93
  ### API Types
189
94
 
190
- The `api` field determines which streaming implementation is used:
191
-
192
- | API | Use for |
193
- |-----|---------|
194
- | `anthropic-messages` | Anthropic Claude API and compatibles |
195
- | `openai-completions` | OpenAI Chat Completions API and compatibles |
196
- | `openai-responses` | OpenAI Responses API |
197
- | `azure-openai-responses` | Azure OpenAI Responses API |
198
- | `openai-codex-responses` | OpenAI Codex Responses API |
199
- | `mistral-conversations` | Native Mistral Chat Completions streaming |
200
- | `google-generative-ai` | Google Generative AI API |
201
- | `google-vertex` | Google Vertex AI API |
202
- | `bedrock-converse-stream` | Amazon Bedrock Converse API |
203
-
204
- Most OpenAI-compatible providers work with `openai-completions`. Use model-level `thinkingLevelMap` for model-specific thinking levels, and `compat` for provider quirks:
205
-
206
- ```typescript
207
- models: [{
208
- id: "custom-model",
209
- // ...
210
- reasoning: true,
211
- thinkingLevelMap: { // map Atomic thinking levels to provider values; null hides unsupported levels
212
- minimal: null,
213
- low: null,
214
- medium: null,
215
- high: "default",
216
- xhigh: null,
217
- max: "max"
218
- },
219
- compat: {
220
- supportsDeveloperRole: false, // use "system" instead of "developer"
221
- supportsReasoningEffort: true,
222
- maxTokensField: "max_tokens", // instead of "max_completion_tokens"
223
- requiresToolResultName: true, // tool results need name field
224
- thinkingFormat: "qwen", // top-level enable_thinking: true
225
- cacheControlFormat: "anthropic" // Anthropic-style cache_control markers
226
- }
227
- }]
228
- ```
229
-
230
- 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`.
231
- 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.
232
-
233
- 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.
95
+ Moved to [Register a provider](/custom-provider/registration#api-types).
234
96
 
235
97
  ### Auth Header
236
98
 
237
- If your provider expects `Authorization: Bearer <key>` but doesn't use a standard API, set `authHeader: true`:
238
-
239
- ```typescript
240
- pi.registerProvider("custom-api", {
241
- baseUrl: "https://api.example.com",
242
- apiKey: "$MY_API_KEY",
243
- authHeader: true, // adds Authorization: Bearer header
244
- api: "openai-completions",
245
- models: [...]
246
- });
247
- ```
99
+ Moved to [Register a provider](/custom-provider/registration#auth-header).
248
100
 
249
101
  ## OAuth Support
250
102
 
251
- Add OAuth/SSO authentication that integrates with `/login`:
252
-
253
- ```typescript
254
- import type { OAuthCredentials, OAuthLoginCallbacks } from "@bastani/pi-ai";
255
-
256
- pi.registerProvider("corporate-ai", {
257
- baseUrl: "https://ai.corp.com/v1",
258
- api: "openai-responses",
259
- models: [...],
260
- oauth: {
261
- name: "Corporate AI (SSO)",
262
-
263
- async login(callbacks: OAuthLoginCallbacks, signal: AbortSignal): Promise<OAuthCredentials> {
264
- // Option 1: Browser-based OAuth
265
- callbacks.onAuth({ url: "https://sso.corp.com/authorize?..." });
266
-
267
- // Option 2: Device code flow
268
- callbacks.onDeviceCode({
269
- userCode: "ABCD-1234",
270
- verificationUri: "https://sso.corp.com/device"
271
- });
272
-
273
- // Option 3: Prompt for token/code
274
- const code = await callbacks.onPrompt({ message: "Enter SSO code:" });
275
-
276
- // Exchange for tokens (your implementation). Forward `signal` so
277
- // cancelling /login aborts the in-flight network request.
278
- const tokens = await exchangeCodeForTokens(code, { signal });
279
-
280
- return {
281
- refresh: tokens.refreshToken,
282
- access: tokens.accessToken,
283
- expires: Date.now() + tokens.expiresIn * 1000
284
- };
285
- },
286
-
287
- async refreshToken(
288
- credentials: OAuthCredentials,
289
- signal: AbortSignal | undefined
290
- ): Promise<OAuthCredentials> {
291
- const tokens = await refreshAccessToken(credentials.refresh, { signal });
292
- return {
293
- refresh: tokens.refreshToken ?? credentials.refresh,
294
- access: tokens.accessToken,
295
- expires: Date.now() + tokens.expiresIn * 1000
296
- };
297
- },
298
-
299
- getApiKey(credentials: OAuthCredentials): string {
300
- return credentials.access;
301
- },
302
-
303
- // Optional: modify models based on user's subscription
304
- modifyModels(models, credentials) {
305
- const region = decodeRegionFromToken(credentials.access);
306
- return models.map(m => ({
307
- ...m,
308
- baseUrl: `https://${region}.ai.corp.com/v1`
309
- }));
310
- }
311
- }
312
- });
313
- ```
314
-
315
- After registration, users can authenticate via `/login corporate-ai`.
316
-
317
- 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.
318
-
319
- In isolated interactive mode, extension code and executable OAuth methods remain in the engine process. Atomic transports only the JSON-safe provider description (`id`, `name`, `loginLabel`, and `usesCallbackServer`) to the terminal process; it never serializes provider functions or acquired credentials and does not load the extension a second time in the frontend. `loginLabel` replaces the login dialog title, while `usesCallbackServer: true` exposes a redirect-URL paste field that races the browser callback. The engine executes the provider's login closure and correlates browser URLs, device codes, progress/info messages, prompts, selections, and manual-code callbacks with the originating login.
320
-
321
- After acquisition, the engine owns serialized credential persistence and logout. It publishes the authenticated provider against its already-loaded snapshot as soon as persistence succeeds; dynamic catalog and ambient-availability refreshes run separately under the model selector's deadline and never extend the login transaction. Logout similarly publishes stored-credential removal without invoking `refreshModels`; Atomic gives the provider's local remaining-auth probe a short deadline so extension code cannot keep the dialog open. The frontend applies the returned snapshot only after the engine transaction succeeds. Escape or Ctrl+C cancels only the matching login and leaves the prior credential/catalog intact. Built-in OAuth and direct, non-isolated extension OAuth use the same persistence and cancellation semantics; later provider registrations continue to override earlier registrations by ID.
322
-
323
- Intentional cancellation is quiet, including native `AbortError`, an aborted signal or its exact reason, nested abort causes, and the legacy exact `Login cancelled` error. Provider denial, timeout, network/protocol errors, malformed responses, token exchange failures, and storage failures remain visible. Catalog-refresh failures are reported by `/model` while cached models remain selectable; they do not turn a persisted login into a failed transaction.
103
+ Moved to [Provider OAuth](/custom-provider/oauth#oauth-support).
324
104
 
325
105
  ## Dynamic model catalog refresh
326
106
 
327
- Providers whose catalogs change at runtime can add `refreshModels`. Atomic calls it during the model picker's bounded asynchronous refresh, independently of authentication completion:
328
-
329
- ```typescript
330
- pi.registerProvider("corporate-ai", {
331
- baseUrl: "https://ai.corp.com/v1",
332
- api: "openai-responses",
333
- apiKey: "$CORPORATE_AI_KEY",
334
- models: cachedModels,
335
- async refreshModels({ signal, force, credential, store }) {
336
- const models = await fetchCorporateModels({ signal, force, credential });
337
- await store.write({ models, checkedAt: Date.now() });
338
- return models;
339
- }
340
- });
341
- ```
342
-
343
- 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.
107
+ Moved to [Provider OAuth](/custom-provider/oauth#dynamic-model-catalog-refresh).
344
108
 
345
109
  ### OAuthLoginCallbacks
346
110
 
347
- The `callbacks` object provides three ways to authenticate:
348
-
349
- ```typescript
350
- interface OAuthLoginCallbacks {
351
- // Open URL in browser (for OAuth redirects)
352
- onAuth(params: { url: string }): void;
353
-
354
- // Show device code (for device authorization flow)
355
- onDeviceCode(params: { userCode: string; verificationUri: string }): void;
356
-
357
- // Prompt user for input (for manual token entry)
358
- onPrompt(params: { message: string }): Promise<string>;
359
- }
360
- ```
111
+ Moved to [Provider OAuth](/custom-provider/oauth#oauthlogincallbacks).
361
112
 
362
113
  ### OAuthCredentials
363
114
 
364
- Credentials are persisted in `~/.atomic/agent/auth.json` (legacy `~/.pi/agent/auth.json` may be read for compatibility):
365
-
366
- ```typescript
367
- interface OAuthCredentials {
368
- refresh: string; // Refresh token (for refreshToken())
369
- access: string; // Access token (returned by getApiKey())
370
- expires: number; // Expiration timestamp in milliseconds
371
- }
372
- ```
115
+ Moved to [Provider OAuth](/custom-provider/oauth#oauthcredentials).
373
116
 
374
117
  ## Custom Streaming API
375
118
 
376
- For providers with non-standard APIs, implement `streamSimple`. Study the existing API implementations before writing your own:
377
-
378
- **Reference implementations:**
379
-
380
- 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:
381
- - `anthropic-messages.d.ts` / `anthropic-messages.js` - Anthropic Messages API
382
- - `mistral-conversations.d.ts` / `mistral-conversations.js` - Mistral Conversations/Chat streaming
383
- - `openai-completions.d.ts` / `openai-completions.js` - OpenAI Chat Completions
384
- - `openai-responses.d.ts` / `openai-responses.js` - OpenAI Responses API
385
- - `google-generative-ai.d.ts` / `google-generative-ai.js` - Google Generative AI
386
- - `bedrock-converse-stream.d.ts` / `bedrock-converse-stream.js` - Amazon Bedrock Converse API
387
- 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`.
119
+ Moved to [Provider streaming API](/custom-provider/streaming#custom-streaming-api).
388
120
 
389
121
  ### Stream Pattern
390
122
 
391
- All providers follow the same pattern:
392
-
393
- ```typescript
394
- import {
395
- type AssistantMessage,
396
- type AssistantMessageEventStream,
397
- type Context,
398
- type Model,
399
- type SimpleStreamOptions,
400
- calculateCost,
401
- createAssistantMessageEventStream,
402
- } from "@bastani/pi-ai";
403
-
404
- function streamMyProvider(
405
- model: Model<any>,
406
- context: Context,
407
- options?: SimpleStreamOptions
408
- ): AssistantMessageEventStream {
409
- const stream = createAssistantMessageEventStream();
410
-
411
- (async () => {
412
- // Initialize output message
413
- const output: AssistantMessage = {
414
- role: "assistant",
415
- content: [],
416
- api: model.api,
417
- provider: model.provider,
418
- model: model.id,
419
- usage: {
420
- input: 0,
421
- output: 0,
422
- cacheRead: 0,
423
- cacheWrite: 0,
424
- totalTokens: 0,
425
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
426
- },
427
- stopReason: "pending",
428
- timestamp: Date.now(),
429
- };
430
-
431
- try {
432
- // Push start event
433
- stream.push({ type: "start", partial: output });
434
-
435
- // Make API request and process response...
436
- // Push content events as they arrive, and set output.stopReason from the
437
- // terminal event. A reason your provider sends that you do not map must
438
- // become an error, not a silent "stop".
439
- if (output.stopReason === "pending") {
440
- throw new Error("Provider stream ended without a stop reason");
441
- }
442
- if (output.stopReason === "error" || output.stopReason === "aborted") {
443
- throw new Error(output.errorMessage || "An unknown error occurred");
444
- }
445
-
446
- // Push done event
447
- stream.push({
448
- type: "done",
449
- reason: output.stopReason,
450
- message: output
451
- });
452
- stream.end();
453
- } catch (error) {
454
- output.stopReason = options?.signal?.aborted ? "aborted" : "error";
455
- output.errorMessage = error instanceof Error ? error.message : String(error);
456
- stream.push({ type: "error", reason: output.stopReason, error: output });
457
- stream.end();
458
- }
459
- })();
460
-
461
- return stream;
462
- }
463
- ```
123
+ Moved to [Provider streaming API](/custom-provider/streaming#stream-pattern).
464
124
 
465
125
  ### Event Types
466
126
 
467
- Push events via `stream.push()` in this order:
468
-
469
- 1. `{ type: "start", partial: output }` - Stream started
470
-
471
- 2. Content events (repeatable, track `contentIndex` for each block):
472
- - `{ type: "text_start", contentIndex, partial }` - Text block started
473
- - `{ type: "text_delta", contentIndex, delta, partial }` - Text chunk
474
- - `{ type: "text_end", contentIndex, content, partial }` - Text block ended
475
- - `{ type: "thinking_start", contentIndex, partial }` - Thinking started
476
- - `{ type: "thinking_delta", contentIndex, delta, partial }` - Thinking chunk
477
- - `{ type: "thinking_end", contentIndex, content, partial }` - Thinking ended
478
- - `{ type: "toolcall_start", contentIndex, partial }` - Tool call started
479
- - `{ type: "toolcall_delta", contentIndex, delta, partial }` - Tool call JSON chunk
480
- - `{ type: "toolcall_end", contentIndex, toolCall, partial }` - Tool call ended
481
-
482
- 3. `{ type: "done", reason, message }` or `{ type: "error", reason, error }` - Stream ended
483
-
484
- The `partial` field in each event contains the current `AssistantMessage` state. Update `output.content` as you receive data, then include `output` as the `partial`.
127
+ Moved to [Provider streaming API](/custom-provider/streaming#event-types).
485
128
 
486
129
  ### Stop Reasons
487
130
 
488
- `StopReason` is `"pending" | "stop" | "length" | "toolUse" | "error" | "aborted"`.
489
-
490
- 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.
491
-
492
- Two checks belong immediately before `done`:
493
-
494
- - 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;
495
- - `"error"` and `"aborted"` are failures, so raise them with `output.errorMessage` and let the `catch` push an `error` event.
496
-
497
- `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.
498
-
499
- 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.
131
+ Moved to [Provider streaming API](/custom-provider/streaming#stop-reasons).
500
132
 
501
133
  ### Content Blocks
502
134
 
503
- Add content blocks to `output.content` as they arrive:
504
-
505
- ```typescript
506
- // Text block
507
- output.content.push({ type: "text", text: "" });
508
- stream.push({ type: "text_start", contentIndex: output.content.length - 1, partial: output });
509
-
510
- // As text arrives
511
- const block = output.content[contentIndex];
512
- if (block.type === "text") {
513
- block.text += delta;
514
- stream.push({ type: "text_delta", contentIndex, delta, partial: output });
515
- }
516
-
517
- // When block completes
518
- stream.push({ type: "text_end", contentIndex, content: block.text, partial: output });
519
- ```
135
+ Moved to [Provider streaming API](/custom-provider/streaming#content-blocks).
520
136
 
521
137
  ### Tool Calls
522
138
 
523
- Tool calls require accumulating JSON and parsing:
524
-
525
- ```typescript
526
- // Start tool call
527
- output.content.push({
528
- type: "toolCall",
529
- id: toolCallId,
530
- name: toolName,
531
- arguments: {}
532
- });
533
- stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
534
-
535
- // Accumulate JSON
536
- let partialJson = "";
537
- partialJson += jsonDelta;
538
- try {
539
- block.arguments = JSON.parse(partialJson);
540
- } catch {}
541
- stream.push({ type: "toolcall_delta", contentIndex, delta: jsonDelta, partial: output });
542
-
543
- // Complete
544
- stream.push({
545
- type: "toolcall_end",
546
- contentIndex,
547
- toolCall: { type: "toolCall", id, name, arguments: block.arguments },
548
- partial: output
549
- });
550
- ```
139
+ Moved to [Provider streaming API](/custom-provider/streaming#tool-calls).
551
140
 
552
141
  ### Usage and Cost
553
142
 
554
- Update usage from API response and calculate cost:
555
-
556
- ```typescript
557
- output.usage.input = response.usage.input_tokens;
558
- output.usage.output = response.usage.output_tokens;
559
- output.usage.cacheRead = response.usage.cache_read_tokens ?? 0;
560
- output.usage.cacheWrite = response.usage.cache_write_tokens ?? 0;
561
- output.usage.totalTokens = output.usage.input + output.usage.output +
562
- output.usage.cacheRead + output.usage.cacheWrite;
563
- calculateCost(model, output.usage);
564
- ```
565
-
566
- `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](/models#request-wide-cost-tiers).
143
+ Moved to [Provider streaming API](/custom-provider/streaming#usage-and-cost).
567
144
 
568
145
  ### Registration
569
146
 
570
- Register your stream function:
571
-
572
- ```typescript
573
- pi.registerProvider("my-provider", {
574
- baseUrl: "https://api.example.com",
575
- apiKey: "$MY_API_KEY",
576
- api: "my-custom-api",
577
- models: [...],
578
- streamSimple: streamMyProvider
579
- });
580
- ```
147
+ Moved to [Provider streaming API](/custom-provider/streaming#registration).
581
148
 
582
149
  ## Testing Your Implementation
583
150
 
@@ -601,139 +168,8 @@ Run tests with your provider/model pairs to verify compatibility.
601
168
 
602
169
  ## Config Reference
603
170
 
604
- ```typescript
605
- interface ProviderConfig {
606
- /** Display name for the provider in UI such as /login. */
607
- name?: string;
608
-
609
- /** API endpoint URL. Required when defining models. */
610
- baseUrl?: string;
611
-
612
- /** API key literal or config value (for env vars use "$ENV_VAR" or "${ENV_VAR}"). Required when defining models (unless oauth). */
613
- apiKey?: string;
614
-
615
- /** API type for streaming. Required at provider or model level when defining models. */
616
- api?: Api;
617
-
618
- /** Custom streaming implementation for non-standard APIs. */
619
- streamSimple?: (
620
- model: Model<Api>,
621
- context: Context,
622
- options?: SimpleStreamOptions
623
- ) => AssistantMessageEventStream;
624
-
625
- /** Custom headers to include in requests. Values use the same config-value syntax as apiKey. */
626
- headers?: Record<string, string>;
627
-
628
- /** If true, adds Authorization: Bearer header with the resolved API key. */
629
- authHeader?: boolean;
630
-
631
- /** Models to register. If provided, replaces all existing models for this provider. */
632
- models?: ProviderModelConfig[];
633
-
634
- /** OAuth provider for /login support. */
635
- oauth?: {
636
- name: string;
637
- login(callbacks: OAuthLoginCallbacks, signal: AbortSignal): Promise<OAuthCredentials>;
638
- refreshToken(credentials: OAuthCredentials, signal: AbortSignal | undefined): Promise<OAuthCredentials>;
639
- getApiKey(credentials: OAuthCredentials): string;
640
- modifyModels?(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[];
641
- };
642
- }
643
- ```
171
+ Moved to [Provider API reference](/custom-provider/api-reference#config-reference).
644
172
 
645
173
  ## Model Definition Reference
646
174
 
647
- ```typescript
648
- interface ProviderModelConfig {
649
- /** Model ID (e.g., "claude-sonnet-4-5"). */
650
- id: string;
651
-
652
- /** Display name (e.g., "Claude Sonnet 4.5"). */
653
- name: string;
654
-
655
- /** API type override for this specific model. */
656
- api?: Api;
657
-
658
- /** API endpoint URL override for this specific model. */
659
- baseUrl?: string;
660
-
661
- /** Whether the model supports extended thinking. */
662
- reasoning: boolean;
663
-
664
- /** Maps Atomic thinking levels to provider/model-specific values; null marks a level unsupported. */
665
- thinkingLevelMap?: Partial<Record<"off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", string | null>>;
666
-
667
- /** Supported input types. */
668
- input: ("text" | "image")[];
669
-
670
- /** Base cost per million tokens plus optional request-wide long-context tiers. */
671
- cost: {
672
- input: number;
673
- output: number;
674
- cacheRead: number;
675
- cacheWrite: number;
676
- tiers?: Array<{
677
- /** Tier applies only when input + cacheRead + cacheWrite strictly exceeds this value. */
678
- inputTokensAbove: number;
679
- input: number;
680
- output: number;
681
- cacheRead: number;
682
- cacheWrite: number;
683
- }>;
684
- };
685
-
686
- /** Default/effective context window size in tokens. */
687
- contextWindow: number;
688
-
689
- /** Maximum output tokens. */
690
- maxTokens: number;
691
- /** Default sampling parameters merged into OpenAI-compatible request bodies. */
692
- samplingParams?: Record<string, unknown>;
693
-
694
- /** Custom headers for this specific model. */
695
- headers?: Record<string, string>;
696
-
697
- /** API-specific provider compatibility settings. */
698
- compat?: {
699
- supportsStore?: boolean;
700
- supportsDeveloperRole?: boolean;
701
- supportsReasoningEffort?: boolean;
702
- supportsUsageInStreaming?: boolean;
703
- supportsFinishReason?: boolean;
704
- supportsThinkingTokenBudget?: boolean;
705
- supportsStrictMode?: boolean;
706
- supportsOpenAIGrammarTools?: boolean;
707
- /** Atomic alias for supportsOpenAIGrammarTools. */
708
- supportsGrammarTools?: boolean;
709
- maxTokensField?: "max_completion_tokens" | "max_tokens";
710
- requiresToolResultName?: boolean;
711
- requiresAssistantAfterToolResult?: boolean;
712
- requiresThinkingAsText?: boolean;
713
- requiresReasoningContentOnAssistantMessages?: boolean;
714
- thinkingFormat?: "openai" | "openrouter" | "deepseek" | "together" | "baseten" | "zai" | "qwen" | "chat-template" | "qwen-chat-template" | "string-thinking" | "ant-ling";
715
- supportsStrictTools?: boolean;
716
- chatTemplateKwargs?: Record<string, string | number | boolean | null | { "$var": "thinking.enabled" | "thinking.effort" | "thinking.budget"; omitWhenOff?: boolean }>;
717
- chatTemplateArgs?: Record<string, string | number | boolean | null | { "$var": "thinking.enabled" | "thinking.effort" | "thinking.budget"; omitWhenOff?: boolean }>;
718
- thinkingTokenBudgetField?: "thinking_token_budget" | "thinking_budget" | "thinking_budget_tokens";
719
- cacheControlFormat?: "anthropic";
720
- sendSessionAffinityHeaders?: boolean;
721
- sessionAffinityFormat?: "openai" | "openai-nosession" | "openrouter";
722
- supportsLongCacheRetention?: boolean;
723
- vllmPriority?: number;
724
- supportsToolSearch?: boolean;
725
- supportsMaxOutputTokens?: boolean;
726
- };
727
- }
728
- ```
729
-
730
- 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.
731
-
732
- `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" } }`. Use `thinkingFormat: "baseten"` with `chatTemplateArgs` when the provider expects toggle values under `chat_template_args` and optionally supports top-level `reasoning_effort`.
733
- `thinkingTokenBudgetField` sends a clamped per-level thinking budget as a top-level request field (`thinking_token_budget` on vLLM, `thinking_budget` on Qwen/SGLang, `thinking_budget_tokens` on llama.cpp). `supportsThinkingTokenBudget: true` is an alias for the vLLM field name. Do not combine it with `reasoning_effort` on DashScope Qwen models.
734
- `vllmPriority` sends a top-level `priority` request field for `openai-completions` providers. Lower values are scheduled earlier and the vLLM server default is `0`, so it only takes effect when vLLM runs with `--scheduling-policy priority`. Set it on a background or batch model so its long prefills queue behind interactive sessions. Unset by default and never set on the generated catalog.
735
- `cacheControlFormat: "anthropic"` applies Anthropic-style `cache_control` markers to the system prompt, last tool definition, and last user, assistant, or tool-result text content.
736
-
737
- Capability flags are enforcement claims, not preferences. `supportsStrictMode` controls strict JSON-schema tools for OpenAI-compatible APIs; Anthropic/Bedrock use `supportsStrictTools`; `supportsOpenAIGrammarTools` controls OpenAI Lark/regex custom tools. Atomic also accepts `supportsGrammarTools` as a compatibility alias and synchronizes it to the canonical OpenAI name; when both disagree, the canonical field wins. Leave these fields unset/false unless the endpoint and selected model actually preserve and enforce the corresponding request shape. See [Extensions](/extensions#constrained-sampling) for exact `constrainedSampling` modes.
738
-
739
- 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. `supportsMaxOutputTokens` defaults to `true`; set it to `false` for OpenAI Responses-compatible gateways such as Codex-protocol proxies that reject `max_output_tokens` with a 400, and Atomic omits the parameter from those requests.
175
+ Moved to [Provider API reference](/custom-provider/api-reference#model-definition-reference).