@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
@@ -0,0 +1,131 @@
1
+ ---
2
+ title: Provider OAuth
3
+ description: OAuth login callbacks, credential storage, and dynamic catalog refresh.
4
+ ---
5
+
6
+ # Provider OAuth
7
+
8
+ ## OAuth Support
9
+
10
+ Add OAuth/SSO authentication that integrates with `/login`:
11
+
12
+ ```typescript
13
+ import type { OAuthCredentials, OAuthLoginCallbacks } from "@bastani/pi-ai";
14
+
15
+ pi.registerProvider("corporate-ai", {
16
+ baseUrl: "https://ai.corp.com/v1",
17
+ api: "openai-responses",
18
+ models: [...],
19
+ oauth: {
20
+ name: "Corporate AI (SSO)",
21
+
22
+ async login(callbacks: OAuthLoginCallbacks, signal: AbortSignal): Promise<OAuthCredentials> {
23
+ // Option 1: Browser-based OAuth
24
+ callbacks.onAuth({ url: "https://sso.corp.com/authorize?..." });
25
+
26
+ // Option 2: Device code flow
27
+ callbacks.onDeviceCode({
28
+ userCode: "ABCD-1234",
29
+ verificationUri: "https://sso.corp.com/device"
30
+ });
31
+
32
+ // Option 3: Prompt for token/code
33
+ const code = await callbacks.onPrompt({ message: "Enter SSO code:" });
34
+
35
+ // Exchange for tokens (your implementation). Forward `signal` so
36
+ // cancelling /login aborts the in-flight network request.
37
+ const tokens = await exchangeCodeForTokens(code, { signal });
38
+
39
+ return {
40
+ refresh: tokens.refreshToken,
41
+ access: tokens.accessToken,
42
+ expires: Date.now() + tokens.expiresIn * 1000
43
+ };
44
+ },
45
+
46
+ async refreshToken(
47
+ credentials: OAuthCredentials,
48
+ signal: AbortSignal | undefined
49
+ ): Promise<OAuthCredentials> {
50
+ const tokens = await refreshAccessToken(credentials.refresh, { signal });
51
+ return {
52
+ refresh: tokens.refreshToken ?? credentials.refresh,
53
+ access: tokens.accessToken,
54
+ expires: Date.now() + tokens.expiresIn * 1000
55
+ };
56
+ },
57
+
58
+ getApiKey(credentials: OAuthCredentials): string {
59
+ return credentials.access;
60
+ },
61
+
62
+ // Optional: modify models based on user's subscription
63
+ modifyModels(models, credentials) {
64
+ const region = decodeRegionFromToken(credentials.access);
65
+ return models.map(m => ({
66
+ ...m,
67
+ baseUrl: `https://${region}.ai.corp.com/v1`
68
+ }));
69
+ }
70
+ }
71
+ });
72
+ ```
73
+
74
+ After registration, users can authenticate via `/login corporate-ai`.
75
+
76
+ 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.
77
+
78
+ 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.
79
+
80
+ 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.
81
+
82
+ 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.
83
+
84
+ ## Dynamic model catalog refresh
85
+
86
+ Providers whose catalogs change at runtime can add `refreshModels`. Atomic calls it during the model picker's bounded asynchronous refresh, independently of authentication completion:
87
+
88
+ ```typescript
89
+ pi.registerProvider("corporate-ai", {
90
+ baseUrl: "https://ai.corp.com/v1",
91
+ api: "openai-responses",
92
+ apiKey: "$CORPORATE_AI_KEY",
93
+ models: cachedModels,
94
+ async refreshModels({ signal, force, credential, store }) {
95
+ const models = await fetchCorporateModels({ signal, force, credential });
96
+ await store.write({ models, checkedAt: Date.now() });
97
+ return models;
98
+ }
99
+ });
100
+ ```
101
+
102
+ 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.
103
+
104
+ ### OAuthLoginCallbacks
105
+
106
+ The `callbacks` object provides three ways to authenticate:
107
+
108
+ ```typescript
109
+ interface OAuthLoginCallbacks {
110
+ // Open URL in browser (for OAuth redirects)
111
+ onAuth(params: { url: string }): void;
112
+
113
+ // Show device code (for device authorization flow)
114
+ onDeviceCode(params: { userCode: string; verificationUri: string }): void;
115
+
116
+ // Prompt user for input (for manual token entry)
117
+ onPrompt(params: { message: string }): Promise<string>;
118
+ }
119
+ ```
120
+
121
+ ### OAuthCredentials
122
+
123
+ Credentials are persisted in `~/.atomic/agent/auth.json` (legacy `~/.pi/agent/auth.json` may be read for compatibility):
124
+
125
+ ```typescript
126
+ interface OAuthCredentials {
127
+ refresh: string; // Refresh token (for refreshToken())
128
+ access: string; // Access token (returned by getApiKey())
129
+ expires: number; // Expiration timestamp in milliseconds
130
+ }
131
+ ```
@@ -0,0 +1,34 @@
1
+ ---
2
+ title: Override an existing provider
3
+ description: Change the behavior of a provider Atomic already ships.
4
+ ---
5
+
6
+ # Override an existing provider
7
+
8
+ ## Override Existing Provider
9
+
10
+ The simplest use case: redirect an existing provider through a proxy.
11
+
12
+ ```typescript
13
+ // All Anthropic requests now go through your proxy
14
+ pi.registerProvider("anthropic", {
15
+ baseUrl: "https://proxy.example.com"
16
+ });
17
+
18
+ // Add custom headers to OpenAI requests
19
+ pi.registerProvider("openai", {
20
+ headers: {
21
+ "X-Custom-Header": "value"
22
+ }
23
+ });
24
+
25
+ // Both baseUrl and headers
26
+ pi.registerProvider("google", {
27
+ baseUrl: "https://ai-gateway.corp.com/google",
28
+ headers: {
29
+ "X-Corp-Auth": "$CORP_AUTH_TOKEN" // resolves from env; omit $ for a literal
30
+ }
31
+ });
32
+ ```
33
+
34
+ When only `baseUrl` and/or `headers` are provided (no `models`), all existing models for that provider are preserved with the new endpoint.
@@ -0,0 +1,163 @@
1
+ ---
2
+ title: Register a provider
3
+ description: Register and unregister a provider, and the API types a provider implements.
4
+ ---
5
+
6
+ # Register a provider
7
+
8
+ ## Register New Provider
9
+
10
+ To add a completely new provider, specify `models` along with the required configuration.
11
+
12
+ If the model list comes from a remote endpoint, use an async extension factory:
13
+
14
+ ```typescript
15
+ import type { ExtensionAPI } from "@bastani/atomic";
16
+
17
+ export default async function (pi: ExtensionAPI) {
18
+ const response = await fetch("http://localhost:1234/v1/models");
19
+ const payload = (await response.json()) as {
20
+ data: Array<{
21
+ id: string;
22
+ name?: string;
23
+ context_window?: number;
24
+ max_tokens?: number;
25
+ }>;
26
+ };
27
+
28
+ pi.registerProvider("local-openai", {
29
+ baseUrl: "http://localhost:1234/v1",
30
+ apiKey: "$LOCAL_OPENAI_API_KEY",
31
+ api: "openai-completions",
32
+ models: payload.data.map((model) => ({
33
+ id: model.id,
34
+ name: model.name ?? model.id,
35
+ reasoning: false,
36
+ input: ["text"],
37
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
38
+ contextWindow: model.context_window ?? 128000,
39
+ maxTokens: model.max_tokens ?? 4096,
40
+ })),
41
+ });
42
+ }
43
+ ```
44
+
45
+ This registers the fetched models before startup finishes.
46
+
47
+ ```typescript
48
+ pi.registerProvider("my-llm", {
49
+ baseUrl: "https://api.my-llm.com/v1",
50
+ apiKey: "$MY_LLM_API_KEY", // env var reference; omit $ for a literal value
51
+ api: "openai-completions", // which streaming API to use
52
+ models: [
53
+ {
54
+ id: "my-llm-large",
55
+ name: "My LLM Large",
56
+ reasoning: true, // supports extended thinking
57
+ input: ["text", "image"],
58
+ cost: {
59
+ input: 3.0, // $/million tokens
60
+ output: 15.0,
61
+ cacheRead: 0.3,
62
+ cacheWrite: 3.75
63
+ },
64
+ contextWindow: 200000,
65
+ maxTokens: 16384
66
+ }
67
+ ]
68
+ });
69
+ ```
70
+
71
+ When `models` is provided, it **replaces** all existing models for that provider.
72
+
73
+ ## Unregister Provider
74
+
75
+ Use `pi.unregisterProvider(name)` to remove a provider that was previously registered via `pi.registerProvider(name, ...)`:
76
+
77
+ ```typescript
78
+ // Register
79
+ pi.registerProvider("my-llm", {
80
+ baseUrl: "https://api.my-llm.com/v1",
81
+ apiKey: "$MY_LLM_API_KEY",
82
+ api: "openai-completions",
83
+ models: [
84
+ {
85
+ id: "my-llm-large",
86
+ name: "My LLM Large",
87
+ reasoning: true,
88
+ input: ["text", "image"],
89
+ cost: { input: 3.0, output: 15.0, cacheRead: 0.3, cacheWrite: 3.75 },
90
+ contextWindow: 200000,
91
+ maxTokens: 16384
92
+ }
93
+ ]
94
+ });
95
+
96
+ // Later, remove it
97
+ pi.unregisterProvider("my-llm");
98
+ ```
99
+
100
+ 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.
101
+
102
+ Calls made after the initial extension load phase are applied immediately, so no `/reload` is required.
103
+
104
+ ### API Types
105
+
106
+ The `api` field determines which streaming implementation is used:
107
+
108
+ | API | Use for |
109
+ |-----|---------|
110
+ | `anthropic-messages` | Anthropic Claude API and compatibles |
111
+ | `openai-completions` | OpenAI Chat Completions API and compatibles |
112
+ | `openai-responses` | OpenAI Responses API |
113
+ | `azure-openai-responses` | Azure OpenAI Responses API |
114
+ | `openai-codex-responses` | OpenAI Codex Responses API |
115
+ | `mistral-conversations` | Native Mistral Chat Completions streaming |
116
+ | `google-generative-ai` | Google Generative AI API |
117
+ | `google-vertex` | Google Vertex AI API |
118
+ | `bedrock-converse-stream` | Amazon Bedrock Converse API |
119
+
120
+ Most OpenAI-compatible providers work with `openai-completions`. Use model-level `thinkingLevelMap` for model-specific thinking levels, and `compat` for provider quirks:
121
+
122
+ ```typescript
123
+ models: [{
124
+ id: "custom-model",
125
+ // ...
126
+ reasoning: true,
127
+ thinkingLevelMap: { // map Atomic thinking levels to provider values; null hides unsupported levels
128
+ minimal: null,
129
+ low: null,
130
+ medium: null,
131
+ high: "default",
132
+ xhigh: null,
133
+ max: "max"
134
+ },
135
+ compat: {
136
+ supportsDeveloperRole: false, // use "system" instead of "developer"
137
+ supportsReasoningEffort: true,
138
+ maxTokensField: "max_tokens", // instead of "max_completion_tokens"
139
+ requiresToolResultName: true, // tool results need name field
140
+ thinkingFormat: "qwen", // top-level enable_thinking: true
141
+ cacheControlFormat: "anthropic" // Anthropic-style cache_control markers
142
+ }
143
+ }]
144
+ ```
145
+
146
+ 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`.
147
+ 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.
148
+
149
+ 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.
150
+
151
+ ### Auth Header
152
+
153
+ If your provider expects `Authorization: Bearer <key>` but doesn't use a standard API, set `authHeader: true`:
154
+
155
+ ```typescript
156
+ pi.registerProvider("custom-api", {
157
+ baseUrl: "https://api.example.com",
158
+ apiKey: "$MY_API_KEY",
159
+ authHeader: true, // adds Authorization: Bearer header
160
+ api: "openai-completions",
161
+ models: [...]
162
+ });
163
+ ```
@@ -0,0 +1,214 @@
1
+ ---
2
+ title: Provider streaming API
3
+ description: "Implement a custom streaming API: events, content blocks, tool calls, stop reasons, and usage."
4
+ ---
5
+
6
+ # Provider streaming API
7
+
8
+ ## Custom Streaming API
9
+
10
+ For providers with non-standard APIs, implement `streamSimple`. Study the existing API implementations before writing your own:
11
+
12
+ **Reference implementations:**
13
+
14
+ 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:
15
+ - `anthropic-messages.d.ts` / `anthropic-messages.js` - Anthropic Messages API
16
+ - `mistral-conversations.d.ts` / `mistral-conversations.js` - Mistral Conversations/Chat streaming
17
+ - `openai-completions.d.ts` / `openai-completions.js` - OpenAI Chat Completions
18
+ - `openai-responses.d.ts` / `openai-responses.js` - OpenAI Responses API
19
+ - `google-generative-ai.d.ts` / `google-generative-ai.js` - Google Generative AI
20
+ - `bedrock-converse-stream.d.ts` / `bedrock-converse-stream.js` - Amazon Bedrock Converse API
21
+ 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`.
22
+
23
+ ### Stream Pattern
24
+
25
+ All providers follow the same pattern:
26
+
27
+ ```typescript
28
+ import {
29
+ type AssistantMessage,
30
+ type AssistantMessageEventStream,
31
+ type Context,
32
+ type Model,
33
+ type SimpleStreamOptions,
34
+ calculateCost,
35
+ createAssistantMessageEventStream,
36
+ } from "@bastani/pi-ai";
37
+
38
+ function streamMyProvider(
39
+ model: Model<any>,
40
+ context: Context,
41
+ options?: SimpleStreamOptions
42
+ ): AssistantMessageEventStream {
43
+ const stream = createAssistantMessageEventStream();
44
+
45
+ (async () => {
46
+ // Initialize output message
47
+ const output: AssistantMessage = {
48
+ role: "assistant",
49
+ content: [],
50
+ api: model.api,
51
+ provider: model.provider,
52
+ model: model.id,
53
+ usage: {
54
+ input: 0,
55
+ output: 0,
56
+ cacheRead: 0,
57
+ cacheWrite: 0,
58
+ totalTokens: 0,
59
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
60
+ },
61
+ stopReason: "pending",
62
+ timestamp: Date.now(),
63
+ };
64
+
65
+ try {
66
+ // Push start event
67
+ stream.push({ type: "start", partial: output });
68
+
69
+ // Make API request and process response...
70
+ // Push content events as they arrive, and set output.stopReason from the
71
+ // terminal event. A reason your provider sends that you do not map must
72
+ // become an error, not a silent "stop".
73
+ if (output.stopReason === "pending") {
74
+ throw new Error("Provider stream ended without a stop reason");
75
+ }
76
+ if (output.stopReason === "error" || output.stopReason === "aborted") {
77
+ throw new Error(output.errorMessage || "An unknown error occurred");
78
+ }
79
+
80
+ // Push done event
81
+ stream.push({
82
+ type: "done",
83
+ reason: output.stopReason,
84
+ message: output
85
+ });
86
+ stream.end();
87
+ } catch (error) {
88
+ output.stopReason = options?.signal?.aborted ? "aborted" : "error";
89
+ output.errorMessage = error instanceof Error ? error.message : String(error);
90
+ stream.push({ type: "error", reason: output.stopReason, error: output });
91
+ stream.end();
92
+ }
93
+ })();
94
+
95
+ return stream;
96
+ }
97
+ ```
98
+
99
+ ### Event Types
100
+
101
+ Push events via `stream.push()` in this order:
102
+
103
+ 1. `{ type: "start", partial: output }` - Stream started
104
+
105
+ 2. Content events (repeatable, track `contentIndex` for each block):
106
+ - `{ type: "text_start", contentIndex, partial }` - Text block started
107
+ - `{ type: "text_delta", contentIndex, delta, partial }` - Text chunk
108
+ - `{ type: "text_end", contentIndex, content, partial }` - Text block ended
109
+ - `{ type: "thinking_start", contentIndex, partial }` - Thinking started
110
+ - `{ type: "thinking_delta", contentIndex, delta, partial }` - Thinking chunk
111
+ - `{ type: "thinking_end", contentIndex, content, partial }` - Thinking ended
112
+ - `{ type: "toolcall_start", contentIndex, partial }` - Tool call started
113
+ - `{ type: "toolcall_delta", contentIndex, delta, partial }` - Tool call JSON chunk
114
+ - `{ type: "toolcall_end", contentIndex, toolCall, partial }` - Tool call ended
115
+
116
+ 3. `{ type: "done", reason, message }` or `{ type: "error", reason, error }` - Stream ended
117
+
118
+ The `partial` field in each event contains the current `AssistantMessage` state. Update `output.content` as you receive data, then include `output` as the `partial`.
119
+
120
+ ### Stop Reasons
121
+
122
+ `StopReason` is `"pending" | "stop" | "length" | "toolUse" | "error" | "aborted"`.
123
+
124
+ 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.
125
+
126
+ Two checks belong immediately before `done`:
127
+
128
+ - 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;
129
+ - `"error"` and `"aborted"` are failures, so raise them with `output.errorMessage` and let the `catch` push an `error` event.
130
+
131
+ `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.
132
+
133
+ 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.
134
+
135
+ ### Content Blocks
136
+
137
+ Add content blocks to `output.content` as they arrive:
138
+
139
+ ```typescript
140
+ // Text block
141
+ output.content.push({ type: "text", text: "" });
142
+ stream.push({ type: "text_start", contentIndex: output.content.length - 1, partial: output });
143
+
144
+ // As text arrives
145
+ const block = output.content[contentIndex];
146
+ if (block.type === "text") {
147
+ block.text += delta;
148
+ stream.push({ type: "text_delta", contentIndex, delta, partial: output });
149
+ }
150
+
151
+ // When block completes
152
+ stream.push({ type: "text_end", contentIndex, content: block.text, partial: output });
153
+ ```
154
+
155
+ ### Tool Calls
156
+
157
+ Tool calls require accumulating JSON and parsing:
158
+
159
+ ```typescript
160
+ // Start tool call
161
+ output.content.push({
162
+ type: "toolCall",
163
+ id: toolCallId,
164
+ name: toolName,
165
+ arguments: {}
166
+ });
167
+ stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
168
+
169
+ // Accumulate JSON
170
+ let partialJson = "";
171
+ partialJson += jsonDelta;
172
+ try {
173
+ block.arguments = JSON.parse(partialJson);
174
+ } catch {}
175
+ stream.push({ type: "toolcall_delta", contentIndex, delta: jsonDelta, partial: output });
176
+
177
+ // Complete
178
+ stream.push({
179
+ type: "toolcall_end",
180
+ contentIndex,
181
+ toolCall: { type: "toolCall", id, name, arguments: block.arguments },
182
+ partial: output
183
+ });
184
+ ```
185
+
186
+ ### Usage and Cost
187
+
188
+ Update usage from API response and calculate cost:
189
+
190
+ ```typescript
191
+ output.usage.input = response.usage.input_tokens;
192
+ output.usage.output = response.usage.output_tokens;
193
+ output.usage.cacheRead = response.usage.cache_read_tokens ?? 0;
194
+ output.usage.cacheWrite = response.usage.cache_write_tokens ?? 0;
195
+ output.usage.totalTokens = output.usage.input + output.usage.output +
196
+ output.usage.cacheRead + output.usage.cacheWrite;
197
+ calculateCost(model, output.usage);
198
+ ```
199
+
200
+ `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/reference#request-wide-cost-tiers).
201
+
202
+ ### Registration
203
+
204
+ Register your stream function:
205
+
206
+ ```typescript
207
+ pi.registerProvider("my-provider", {
208
+ baseUrl: "https://api.example.com",
209
+ apiKey: "$MY_API_KEY",
210
+ api: "my-custom-api",
211
+ models: [...],
212
+ streamSimple: streamMyProvider
213
+ });
214
+ ```