orbitx 0.2.9 → 0.3.0

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 (141) hide show
  1. package/Readme.md +478 -456
  2. package/dist/agents/backend.d.ts +4 -0
  3. package/dist/agents/backend.d.ts.map +1 -0
  4. package/dist/agents/backend.js +54 -0
  5. package/dist/agents/backend.js.map +1 -0
  6. package/dist/agents/frontend.d.ts +4 -0
  7. package/dist/agents/frontend.d.ts.map +1 -0
  8. package/dist/agents/frontend.js +50 -0
  9. package/dist/agents/frontend.js.map +1 -0
  10. package/dist/agents/index.d.ts +10 -0
  11. package/dist/agents/index.d.ts.map +1 -0
  12. package/dist/agents/index.js +22 -0
  13. package/dist/agents/index.js.map +1 -0
  14. package/dist/agents/planner.d.ts +4 -0
  15. package/dist/agents/planner.d.ts.map +1 -0
  16. package/dist/agents/planner.js +86 -0
  17. package/dist/agents/planner.js.map +1 -0
  18. package/dist/agents/protocol.d.ts +5 -0
  19. package/dist/agents/protocol.d.ts.map +1 -0
  20. package/dist/agents/protocol.js +70 -0
  21. package/dist/agents/protocol.js.map +1 -0
  22. package/dist/agents/reasoner.d.ts +5 -0
  23. package/dist/agents/reasoner.d.ts.map +1 -0
  24. package/dist/agents/reasoner.js +96 -0
  25. package/dist/agents/reasoner.js.map +1 -0
  26. package/dist/agents/research.d.ts +4 -0
  27. package/dist/agents/research.d.ts.map +1 -0
  28. package/dist/agents/research.js +28 -0
  29. package/dist/agents/research.js.map +1 -0
  30. package/dist/agents/tester.d.ts +4 -0
  31. package/dist/agents/tester.d.ts.map +1 -0
  32. package/dist/agents/tester.js +70 -0
  33. package/dist/agents/tester.js.map +1 -0
  34. package/dist/core/agent-definition.d.ts +61 -0
  35. package/dist/core/agent-definition.d.ts.map +1 -0
  36. package/dist/core/agent-definition.js +82 -0
  37. package/dist/core/agent-definition.js.map +1 -0
  38. package/dist/core/ai-provider.d.ts +3 -0
  39. package/dist/core/ai-provider.d.ts.map +1 -1
  40. package/dist/core/ai-provider.js.map +1 -1
  41. package/dist/core/anthropic-provider.d.ts +1 -0
  42. package/dist/core/anthropic-provider.d.ts.map +1 -1
  43. package/dist/core/anthropic-provider.js +3 -0
  44. package/dist/core/anthropic-provider.js.map +1 -1
  45. package/dist/core/base-agent.d.ts +51 -28
  46. package/dist/core/base-agent.d.ts.map +1 -1
  47. package/dist/core/base-agent.js +118 -26
  48. package/dist/core/base-agent.js.map +1 -1
  49. package/dist/core/deepseek-provider.d.ts +1 -0
  50. package/dist/core/deepseek-provider.d.ts.map +1 -1
  51. package/dist/core/deepseek-provider.js +3 -0
  52. package/dist/core/deepseek-provider.js.map +1 -1
  53. package/dist/core/mcp-computer.d.ts +14 -1
  54. package/dist/core/mcp-computer.d.ts.map +1 -1
  55. package/dist/core/mcp-computer.js +352 -71
  56. package/dist/core/mcp-computer.js.map +1 -1
  57. package/dist/core/ollama-provider.d.ts +1 -0
  58. package/dist/core/ollama-provider.d.ts.map +1 -1
  59. package/dist/core/ollama-provider.js +3 -0
  60. package/dist/core/ollama-provider.js.map +1 -1
  61. package/dist/core/openai-provider.d.ts +1 -0
  62. package/dist/core/openai-provider.d.ts.map +1 -1
  63. package/dist/core/openai-provider.js +3 -0
  64. package/dist/core/openai-provider.js.map +1 -1
  65. package/dist/core/swarm-base.d.ts +88 -0
  66. package/dist/core/swarm-base.d.ts.map +1 -0
  67. package/dist/core/swarm-base.js +427 -0
  68. package/dist/core/swarm-base.js.map +1 -0
  69. package/dist/index.d.ts +7 -5
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +4 -3
  72. package/dist/index.js.map +1 -1
  73. package/dist/tests/agents/default-agents.test.d.ts +2 -0
  74. package/dist/tests/agents/default-agents.test.d.ts.map +1 -0
  75. package/dist/tests/agents/default-agents.test.js +106 -0
  76. package/dist/tests/agents/default-agents.test.js.map +1 -0
  77. package/dist/tests/core/agent-definition.test.d.ts +2 -0
  78. package/dist/tests/core/agent-definition.test.d.ts.map +1 -0
  79. package/dist/tests/core/agent-definition.test.js +99 -0
  80. package/dist/tests/core/agent-definition.test.js.map +1 -0
  81. package/dist/tests/core/scripted-provider.d.ts +38 -0
  82. package/dist/tests/core/scripted-provider.d.ts.map +1 -0
  83. package/dist/tests/core/scripted-provider.js +85 -0
  84. package/dist/tests/core/scripted-provider.js.map +1 -0
  85. package/dist/tests/core/swarm-base.test.d.ts +2 -0
  86. package/dist/tests/core/swarm-base.test.d.ts.map +1 -0
  87. package/dist/tests/core/swarm-base.test.js +387 -0
  88. package/dist/tests/core/swarm-base.test.js.map +1 -0
  89. package/dist/tests/index.d.ts +3 -0
  90. package/dist/tests/index.d.ts.map +1 -1
  91. package/dist/tests/index.js +3 -0
  92. package/dist/tests/index.js.map +1 -1
  93. package/dist/tests/tools/agent/agent-tools.test.js +225 -153
  94. package/dist/tests/tools/agent/agent-tools.test.js.map +1 -1
  95. package/dist/tools/agent/active.d.ts +5 -0
  96. package/dist/tools/agent/active.d.ts.map +1 -0
  97. package/dist/tools/agent/active.js +40 -0
  98. package/dist/tools/agent/active.js.map +1 -0
  99. package/dist/tools/agent/fire.d.ts +5 -0
  100. package/dist/tools/agent/fire.d.ts.map +1 -0
  101. package/dist/tools/agent/fire.js +38 -0
  102. package/dist/tools/agent/fire.js.map +1 -0
  103. package/dist/tools/agent/hire.d.ts +2 -2
  104. package/dist/tools/agent/hire.d.ts.map +1 -1
  105. package/dist/tools/agent/hire.js +43 -10
  106. package/dist/tools/agent/hire.js.map +1 -1
  107. package/dist/tools/agent/index.d.ts +22 -19
  108. package/dist/tools/agent/index.d.ts.map +1 -1
  109. package/dist/tools/agent/index.js +29 -21
  110. package/dist/tools/agent/index.js.map +1 -1
  111. package/dist/tools/agent/prompt.d.ts +2 -2
  112. package/dist/tools/agent/prompt.d.ts.map +1 -1
  113. package/dist/tools/agent/prompt.js +19 -53
  114. package/dist/tools/agent/prompt.js.map +1 -1
  115. package/dist/tools/agent/registry.d.ts +74 -24
  116. package/dist/tools/agent/registry.d.ts.map +1 -1
  117. package/dist/tools/agent/registry.js +93 -48
  118. package/dist/tools/agent/registry.js.map +1 -1
  119. package/dist/tools/agent/report-group.d.ts +5 -0
  120. package/dist/tools/agent/report-group.d.ts.map +1 -0
  121. package/dist/tools/agent/report-group.js +50 -0
  122. package/dist/tools/agent/report-group.js.map +1 -0
  123. package/dist/tools/agent/report-parent.d.ts +5 -0
  124. package/dist/tools/agent/report-parent.d.ts.map +1 -0
  125. package/dist/tools/agent/report-parent.js +36 -0
  126. package/dist/tools/agent/report-parent.js.map +1 -0
  127. package/dist/tools/agent/types.d.ts +5 -0
  128. package/dist/tools/agent/types.d.ts.map +1 -0
  129. package/dist/tools/agent/types.js +17 -0
  130. package/dist/tools/agent/types.js.map +1 -0
  131. package/docs/agents.md +173 -228
  132. package/docs/api-reference.md +9 -4
  133. package/docs/getting-started.md +78 -78
  134. package/docs/index.md +24 -23
  135. package/docs/mcp-architecture.md +29 -6
  136. package/docs/providers.md +258 -254
  137. package/docs/skills.md +0 -1
  138. package/docs/streaming.md +133 -133
  139. package/docs/swarm.md +503 -0
  140. package/docs/tools.md +214 -212
  141. package/package.json +1 -1
package/docs/providers.md CHANGED
@@ -1,254 +1,258 @@
1
- # Providers
2
-
3
- An `AIProvider` is OrbitX's abstraction over a model API. All four built-in providers implement the same interface (`src/core/ai-provider.ts`):
4
-
5
- ```ts
6
- abstract class AIProvider {
7
- abstract chat(
8
- messages: Message[],
9
- streamCallback?: StreamCallback,
10
- tools?: ToolSchema[],
11
- signal?: AbortSignal
12
- ): Promise<ChatResponse>;
13
-
14
- abstract getCapabilities(): ProviderCapabilities;
15
-
16
- abstract setOption(key: string, value: unknown): void;
17
- }
18
- ```
19
-
20
- - `chat` sends the full message history (plus a provider-agnostic `tools` schema, when supplied) and returns a `ChatResponse` (`{ content, inputMissTokens, inputCacheTokens, outputTokens, toolCalls? }`) — `inputMissTokens`/`inputCacheTokens` are real, provider-reported non-cached/cache-hit input token counts (see "User tracking & prompt caching" below), not a client-side estimate. If `streamCallback` is passed, text is streamed to it incrementally as it arrives — see [Streaming](./streaming.md).
21
- - `signal`, when passed and later aborted, cancels the in-flight request to the provider's API immediately rather than just abandoning it client-side — `BaseAgent.immediateStop()` uses this so the call stops generating (and billing for) further output right away. It doesn't retroactively waive input tokens or output already generated before the abort. Anthropic, OpenAI, and DeepSeek honor it for both streaming and non-streaming calls (their SDKs forward it straight to `fetch`). Ollama only honors it for streaming calls — its client has no way to abort a non-streaming request — so a `chat()` call made without a `streamCallback` against `OllamaProvider` can't be cancelled mid-flight; see the retry-loop guard in `ollama-provider.ts`.
22
- - **Streaming calls resolve normally on abort**, returning a `ChatResponse` built from whatever content/thinking/tool-calls were accumulated before the cutoff, instead of throwing — so text already delivered via `streamCallback` still ends up recorded rather than discarded. A tool call still being generated when the abort lands is dropped (its arguments may be truncated JSON); only tool calls that had already fully finished streaming are included in `toolCalls`.
23
- - **Non-streaming calls still throw on abort** — there's no partial response to salvage since the API only returns once, in full — and `BaseAgent` discards that turn.
24
- - `getCapabilities()` returns a static description used by `BaseAgent` to decide things like when to trigger memory compaction — it is **not** re-queried per call:
25
-
26
- ```ts
27
- interface ProviderCapabilities {
28
- supportsTools: boolean; // must be true — BaseAgent requires native tool-calling
29
- supportsImages: boolean; // whether this provider/model accepts image content
30
- contextWindow: number; // total context window, in tokens
31
- safeUsageRatio?: number; // fraction of contextWindow safe to fill before compacting (default 0.5)
32
- supportsThinking?: boolean; // whether reasoning/thinking text is surfaced — see "Think effort" below
33
- maxOutputTokens?: number; // the model's max output tokens per response, where confirmed — not yet enforced anywhere, reserved for a future release
34
- supportsVideo?: boolean; // whether this provider/model accepts video content
35
- supportsAudio?: boolean; // whether this provider/model accepts audio content
36
- supportsImageGeneration?: boolean; // whether this provider/model can generate new images
37
- supportsImageEditing?: boolean; // whether this provider/model can edit an existing image
38
- supportsVideoGeneration?: boolean; // whether this provider/model can generate new video
39
- supportsVideoEditing?: boolean; // whether this provider/model can edit existing video
40
- supportsAudioGeneration?: boolean; // whether this provider/model can generate new audio (speech, music, sound effects)
41
- supportsAudioDesign?: boolean; // whether this provider/model can design/produce sound effects or soundscapes
42
- supportsAudioClone?: boolean; // whether this provider/model can clone a voice from a reference sample
43
- supports3DModelGeneration?: boolean; // whether this provider/model can generate a new 3D model
44
- }
45
- ```
46
-
47
- These flags describe what the model itself can natively do — they're unrelated to `ProviderType` (see "Provider roles" below), which is just a role name used to look a provider up in a `ProviderRegistry`. A provider registered under the `"image-generation"` role doesn't need `supportsImageGeneration: true` (or vice versa) — the role name and the capability flag are independent. All four built-in providers are chat/vision LLMs, not generation APIs, so every one of the new flags above is always `false` on them.
48
-
49
- `BaseAgent.run()` throws immediately if `getCapabilities().supportsTools` is `false` — the legacy JSON-in-text tool-call convention has been removed from the main loop. All providers default `supportsTools` to `true` except Ollama, which defaults it to `false` (many locally-hosted models don't support native function calling; pass `{ supportsTools: true }` explicitly if your model does).
50
-
51
- ## Changing an option without reconstructing
52
-
53
- `setOption(key, value)` mutates an already-constructed provider in place — e.g. `provider.setOption("thinkEffort", 0.8)` — instead of building a new one. Every built-in provider only allows keys it already reads fresh on every `chat()` call (never something baked into derived state at construction, like `contextWindow`); an unrecognized key throws.
54
-
55
- | Provider | Settable keys |
56
- |---|---|
57
- | Anthropic | `thinkEffort`, `userId`, `disablePromptCaching`, `maxTokens`, `supportsTools` |
58
- | OpenAI | `thinkEffort`, `userId`, `supportsTools` |
59
- | DeepSeek | `thinkEffort`, `userId`, `supportsTools` |
60
- | Ollama | `thinkEffort`, `supportsTools` |
61
-
62
- ## Usage on the streaming path
63
-
64
- OpenAI and DeepSeek's Chat-Completions-style streaming protocol doesn't compute token usage until a turn is fully complete — it arrives once, in a special final chunk (`stream_options: { include_usage: true }`), alongside `done: true`. Every chunk before that has `usage: { inputMissTokens: 0, inputCacheTokens: 0, outputTokens: 0 }` — not because those numbers are unknown-but-estimated, but because the API genuinely hasn't computed them yet. `OpenAIProvider` and `DeepSeekProvider` both trust this real, provider-reported final chunk directly — no local estimation involved on either. DeepSeek's final chunk includes its own `prompt_cache_hit_tokens`/`prompt_cache_miss_tokens` fields (falling back to `prompt_tokens_details.cached_tokens` if those aren't present), parsed the same way real cache data is read from any other provider.
65
-
66
- If a call is cut off before that final chunk arrives (e.g. `BaseAgent.immediateStop()` aborting the connection to stop paying for further output), the real usage number simply never gets computed or sent for either provider — not partially, not late, just never — so `inputMissTokens`/`inputCacheTokens`/`outputTokens` stay `0` on `ChatResponse` too in that case. Anthropic doesn't have this problem (it streams real usage incrementally, see [Streaming](./streaming.md#usage-chunks)); Ollama has the same final-chunk-only limitation but isn't covered by what's described here.
67
-
68
- This is the only usage source DeepSeek's streaming path has, so it's treated as the real number, not flagged as a lesser "estimate" — whatever lands on the final `done: true` chunk (and the returned `ChatResponse`) is what a consumer should use for billing/accounting, the same as it would for any other provider's real, API-reported usage. The two sides of that number are revealed at different points, though: `outputTokens` grows on every streamed chunk as content arrives, since it's cheap to recompute from what's been generated so far; `inputMissTokens`/`inputCacheTokens` are only computed once, on that final chunk, since the input side never changes mid-call and there's no reason to resend the same number on every content delta.
69
-
70
- ## Think effort
71
-
72
- Reasoning/thinking models each expose a different native "how hard should the model think" knob — OpenAI and Ollama use `low`/`medium`/`high`, DeepSeek uses `none`/`low`/`high`/`max`, Anthropic uses a numeric thinking-token budget. OrbitX replaces all of that with one universal option on every provider's constructor:
73
-
74
- ```ts
75
- thinkEffort?: number; // 0-1, e.g. 0.5 or 0.8 — never a provider-specific string
76
- ```
77
-
78
- Each provider maps that 0-1 value onto whatever scale it actually accepts (`src/core/think-effort.ts`'s `resolveThinkEffortLevel()` buckets it evenly across a provider's supported levels; Anthropic instead scales it into a `budget_tokens` range). A provider or model that doesn't support thinking at all — or doesn't recognize the resulting native value — simply ignores it:
79
-
80
- ```ts
81
- import { AnthropicProvider, DeepSeekProvider } from "orbitx";
82
-
83
- const anthropicProvider = new AnthropicProvider("api-key", "claude-sonnet-5", { thinkEffort: 0.7 });
84
- const deepseekProvider = new DeepSeekProvider("api-key", "deepseek-v4-flash", { thinkEffort: 0.3 });
85
- ```
86
-
87
- Per-provider specifics:
88
-
89
- | Provider | Native scale | Notes |
90
- |---|---|---|
91
- | Anthropic | `thinking.budget_tokens` (numeric) | Scales with `maxTokens`; `max_tokens` sent to the API is automatically bumped so it always exceeds the thinking budget. Signed thinking blocks are carried forward automatically via `Message.providerThinking` when a thinking turn also made a tool call — required by Anthropic's API, handled for you. |
92
- | DeepSeek | `none` / `low` / `high` / `max` | Sent as `reasoning_effort` on reasoner models. |
93
- | OpenAI | `low` / `medium` / `high` | Sent as `reasoning_effort`, and **only** for reasoning-capable model families (`o1`/`o3`/`o4`/`gpt-5*` prefixes) — silently ignored for every other model, since the Chat Completions API rejects the param on non-reasoning models. |
94
- | Ollama | `false` / `low` / `medium` / `high` | `thinkEffort <= 0` maps to `false` (works as an on/off switch for models that only understand a boolean, e.g. deepseek-r1/qwen3); higher values map to a level string for models that support graded effort (e.g. gpt-oss). |
95
-
96
- See [Streaming](./streaming.md#thinking-chunks) for how (and whether) a given provider's reasoning text is observable while it streams.
97
-
98
- ## User tracking & prompt caching
99
-
100
- Every provider's constructor accepts `userId?: string` — an opaque end-user identifier forwarded to the underlying API for that provider's own abuse-monitoring/analytics, same universal-option pattern as `thinkEffort`: pass it uniformly and providers that have no equivalent field just ignore it.
101
-
102
- | Provider | Where `userId` goes | Notes |
103
- |---|---|---|
104
- | Anthropic | `metadata.user_id` on every request | Anthropic's documented field for this purpose. |
105
- | OpenAI | `user_id` on every request | Sent as an extra body field alongside the standard params. |
106
- | DeepSeek | `user_id` on every request | Same mechanism as OpenAI (DeepSeek's API is OpenAI-compatible). |
107
- | Ollama | Ignored | Accepted in the options type for interface parity only — Ollama's API has no equivalent field. |
108
-
109
- **Anthropic prompt caching is on by default.** Every request marks the last content block with an ephemeral `cache_control` breakpoint, so a growing conversation gets served from cache instead of being fully reprocessed each turn — pass `disablePromptCaching: true` to send every request uncached. DeepSeek's cache is automatic server-side (no `cache_control` equivalent to toggle) and reports its own explicit `prompt_cache_hit_tokens`/`prompt_cache_miss_tokens`. OpenAI's cache is also automatic, reporting hits via `prompt_tokens_details.cached_tokens`. Ollama has no cache concept at all — `inputCacheTokens` is always `0`.
110
-
111
- ## Built-in providers
112
-
113
- ### Ollama
114
-
115
- ```ts
116
- import { OllamaProvider } from "orbitx";
117
-
118
- new OllamaProvider(model: string, host = "http://localhost:11434", options?: {
119
- supportsTools?: boolean; // default false
120
- contextWindow?: number; // default 32_000
121
- thinkEffort?: number; // 0-1, see "Think effort" above
122
- userId?: string; // accepted for interface parity only — ignored, Ollama has no equivalent field
123
- });
124
- ```
125
-
126
- Wraps the `ollama` npm package, talking to a local (or remote) Ollama server. Always reports `supportsImages: true`.
127
-
128
- ### DeepSeek
129
-
130
- ```ts
131
- import { DeepSeekProvider } from "orbitx";
132
-
133
- new DeepSeekProvider(apiKey: string, model = "deepseek-v4-flash", options?: {
134
- supportsTools?: boolean; // default true
135
- supportsImages?: boolean; // default: true only for deepseek-v4-flash-vision-exp
136
- contextWindow?: number; // default 1_000_000 (deepseek-v4-flash / deepseek-v4-flash-vision-exp / deepseek-v4-pro)
137
- thinkEffort?: number; // 0-1, see "Think effort" above
138
- userId?: string; // see "User tracking & prompt caching" above
139
- });
140
- ```
141
-
142
- Uses the `openai` SDK pointed at `https://api.deepseek.com` (DeepSeek's API is OpenAI-compatible). `getCapabilities().supportsImages` is `false` for every model except `deepseek-v4-flash-vision-exp`, which is `deepseek-v4-flash` plus native image input (same context window, same reasoning support) — pass `supportsImages: true` explicitly if a future model adds vision support before this SDK's allowlist is updated.
143
-
144
- `stream_options: { include_usage: true }` is set on every streaming call, so `inputMissTokens`/`inputCacheTokens`/`outputTokens` are accurate on both the streaming and non-streaming paths — DeepSeek sends a usage-bearing final chunk (including its own `prompt_cache_hit_tokens`/`prompt_cache_miss_tokens` split) the same way OpenAI does.
145
-
146
- ### Anthropic
147
-
148
- ```ts
149
- import { AnthropicProvider } from "orbitx";
150
-
151
- new AnthropicProvider(apiKey: string, model = "claude-sonnet-5", options?: {
152
- supportsTools?: boolean; // default true
153
- supportsImages?: boolean; // default true
154
- contextWindow?: number; // default from a per-model table, else 200_000; an explicit value here always wins over extendedContext below
155
- maxTokens?: number; // output token cap, default 4096
156
- thinkEffort?: number; // 0-1, see "Think effort" above
157
- userId?: string; // see "User tracking & prompt caching" above
158
- disablePromptCaching?: boolean; // default false — prompt caching is on by default, see above
159
- extendedContext?: boolean | string; // default false/undefined — see "Context window" below
160
- });
161
- ```
162
-
163
- Wraps `@anthropic-ai/sdk`. `maxTokens` is the only provider with an explicit output-length cap in its options today.
164
-
165
- ### OpenAI
166
-
167
- ```ts
168
- import { OpenAIProvider } from "orbitx";
169
-
170
- new OpenAIProvider(apiKey: string, model = "gpt-5", options?: {
171
- supportsTools?: boolean; // default true
172
- supportsImages?: boolean; // default true unless model is in a small denylist (e.g. o3-mini, o1-mini)
173
- contextWindow?: number; // default from a per-model table, else 128_000
174
- baseURL?: string; // point at an OpenAI-compatible endpoint
175
- thinkEffort?: number; // 0-1, see "Think effort" above — ignored on non-reasoning models
176
- userId?: string; // see "User tracking & prompt caching" above
177
- });
178
- ```
179
-
180
- Uses the Chat Completions API (not the Responses API). `baseURL` makes this usable against any OpenAI-compatible provider, not just OpenAI itself.
181
-
182
- ## Provider roles
183
-
184
- An agent's providers are registered under open-ended role names, not a fixed pair. `"main"` — the provider driving the agent loop itself — is the only one that's required; every other role is optional and looked up on demand by whatever tool needs it.
185
-
186
- ```ts
187
- export type ProviderType =
188
- | "main"
189
- | "image-describer"
190
- | "image-generation"
191
- | "image-editing"
192
- | "video-describer"
193
- | "video-generation"
194
- | "video-editing"
195
- | "audio-describer"
196
- | "audio-generation"
197
- | "audio-design"
198
- | "audio-clone"
199
- | "3d-model-generator"
200
- | "llm-low"
201
- | "llm-medium"
202
- | "llm-high"
203
- | (string & {});
204
- ```
205
-
206
- `ProviderType` (exported from `orbitx`) is the documented starting vocabulary for every role name — not a closed union, and purely for naming/registry lookup. It has no relationship to `ProviderCapabilities` (see above) — a role name doesn't imply anything about what the provider registered under it can actually do. The trailing `(string & {})` (not bare `string`) is what makes the open-endedness work: a plain `| string` union collapses to just `string` and an editor stops suggesting the literals at all, while intersecting with `{}` keeps TS from widening it away, so a custom name still type-checks the moment nothing here fits. `llm-low`/`medium`/`high` are a self-declared overall quality/durability tier (cheap-and-good-enough vs. worth relying on for longer/harder tasks) — not a specific claim about reasoning power.
207
-
208
- Pass a single provider to use it as `main` (same as today); pass an array of `{ type, provider, default? }` entries to register more:
209
-
210
- ```ts
211
- import { BaseAgent, DeepSeekProvider, OllamaProvider } from "orbitx";
212
-
213
- const agent = new BaseAgent({
214
- aiProvider: [
215
- { type: "main", provider: new DeepSeekProvider("api-key", "deepseek-v4-flash") },
216
- { type: "image-describer", provider: new OllamaProvider("llava:latest") },
217
- ],
218
- instruction: "You are a helpful assistant.",
219
- mcpClient,
220
- allowedTools: [/* ... */],
221
- });
222
- ```
223
-
224
- `provider` can also be an array — a pool of candidates for that role — with `default` (0-based, defaults to `0`) picking which one is used when code needs exactly one. The rest of the pool is there to be enumerated later; nothing built-in does smarter selection than "the default" yet.
225
-
226
- ### Resolving a provider by role
227
-
228
- `ProviderRegistry.getProvider(type)` tries an exact role-name match first (that role's pool default). If nothing is registered under `type`, it falls back to scanning every provider registered under *any* role for the first one whose `getCapabilities()` reports the matching capability flag — not a generic capability bag keyed by `ProviderType` (that's gone; `ProviderType` is purely a registry role name and has no relationship to what a model can do), but a fixed, small mapping from a handful of well-known role names to their one corresponding `ProviderCapabilities` flag:
229
-
230
- | Role (`ProviderType`) | Capability flag checked in the fallback |
231
- |---|---|
232
- | `"image-describer"` | `supportsImages` |
233
- | `"image-generation"` | `supportsImageGeneration` |
234
- | `"image-editing"` | `supportsImageEditing` |
235
- | `"video-describer"` | `supportsVideo` |
236
- | `"video-generation"` | `supportsVideoGeneration` |
237
- | `"video-editing"` | `supportsVideoEditing` |
238
- | `"audio-describer"` | `supportsAudio` |
239
- | `"audio-generation"` | `supportsAudioGeneration` |
240
- | `"audio-design"` | `supportsAudioDesign` |
241
- | `"audio-clone"` | `supportsAudioClone` |
242
- | `"3d-model-generator"` | `supports3DModelGeneration` |
243
-
244
- This is why a single vision-capable `main` provider "just works" as an image describer with zero extra config: nothing needs to be registered under `"image-describer"` explicitly — the fallback finds `main` because its `getCapabilities().supportsImages` is `true`. Role names outside this table (`"main"`, `"llm-low"`/`"medium"`/`"high"`, or any custom string) have no capability equivalent, so the fallback never applies to them — an unregistered role like that returns `undefined` from `getProvider` regardless of what any registered provider can do.
245
-
246
- `BaseAgent` is the only thing that ever calls `getProvider` directly — a tool never touches a `ProviderRegistry`, or a raw provider, itself. It reaches this indirectly via `mcp.executeProvider(toolCallId, type, input)` (see [Tools](./tools.md#mcpexecuteprovider--calling-a-provider-from-inside-a-tool)), which works the same way whether the tool is registered on the local `MCPClient` or a sandboxed/remote `MCPServer` — the request is always resolved on `BaseAgent`'s side, never handed across as a live object.
247
-
248
- `resolveAgentProviders()` (`AgentProvidersInput = AIProvider | AgentProviderEntry[]`) builds the `ProviderRegistry` this all runs on — both are exported from `orbitx` if you need them directly (`getMain()`, `getProvider(type)`, `getProviders(type)` for the full pool under an exact role). There is no separate "utils" role — every tool/skill is included in the system prompt from the start, so there's nothing left for a separate pass to decide.
249
-
250
- ## Writing a custom provider
251
-
252
- Extend `AIProvider` and implement `chat`/`getCapabilities`. Follow an existing provider (e.g. `src/core/ollama-provider.ts`) as a template: translate `Message[]` to your API's wire format, stream text chunks to `streamCallback` as they arrive, accumulate tool-call deltas and emit them on the final chunk, and translate `ToolSchema[]` into your API's function-calling format (see `src/core/tool-schema-translator.ts` for existing OpenAI/Anthropic translators you can reuse or reference).
253
-
254
- `inputMissTokens`/`inputCacheTokens`/`outputTokens` on the returned `ChatResponse` are optional — every built-in provider is token-billed and always sets them, but a provider registered under a per-request-billed role (e.g. `image-generation`, `audio-clone`) should omit them and set `cost` instead. `BaseAgent#executeProvider` (see [Tools](./tools.md#mcpexecuteprovider--calling-a-provider-from-inside-a-tool)) checks `cost` first and records a `unit: "cost"` usage entry when it's present, falling back to the token fields (defaulting any missing one to `0`) otherwise — see [Token accounting](./agents.md#token-accounting).
1
+ # Providers
2
+
3
+ An `AIProvider` is OrbitX's abstraction over a model API. All four built-in providers implement the same interface (`src/core/ai-provider.ts`):
4
+
5
+ ```ts
6
+ abstract class AIProvider {
7
+ abstract chat(
8
+ messages: Message[],
9
+ streamCallback?: StreamCallback,
10
+ tools?: ToolSchema[],
11
+ signal?: AbortSignal
12
+ ): Promise<ChatResponse>;
13
+
14
+ abstract getModel(): string;
15
+
16
+ abstract getCapabilities(): ProviderCapabilities;
17
+
18
+ abstract setOption(key: string, value: unknown): void;
19
+ }
20
+ ```
21
+
22
+ - `chat` sends the full message history (plus a provider-agnostic `tools` schema, when supplied) and returns a `ChatResponse` (`{ content, inputMissTokens, inputCacheTokens, outputTokens, toolCalls? }`) — `inputMissTokens`/`inputCacheTokens` are real, provider-reported non-cached/cache-hit input token counts (see "User tracking & prompt caching" below), not a client-side estimate. If `streamCallback` is passed, text is streamed to it incrementally as it arrives — see [Streaming](./streaming.md).
23
+ - `signal`, when passed and later aborted, cancels the in-flight request to the provider's API immediately rather than just abandoning it client-side — `BaseAgent.immediateStop()` uses this so the call stops generating (and billing for) further output right away. It doesn't retroactively waive input tokens or output already generated before the abort. Anthropic, OpenAI, and DeepSeek honor it for both streaming and non-streaming calls (their SDKs forward it straight to `fetch`). Ollama only honors it for streaming calls — its client has no way to abort a non-streaming request — so a `chat()` call made without a `streamCallback` against `OllamaProvider` can't be cancelled mid-flight; see the retry-loop guard in `ollama-provider.ts`.
24
+ - **Streaming calls resolve normally on abort**, returning a `ChatResponse` built from whatever content/thinking/tool-calls were accumulated before the cutoff, instead of throwing — so text already delivered via `streamCallback` still ends up recorded rather than discarded. A tool call still being generated when the abort lands is dropped (its arguments may be truncated JSON); only tool calls that had already fully finished streaming are included in `toolCalls`.
25
+ - **Non-streaming calls still throw on abort** — there's no partial response to salvage since the API only returns once, in full — and `BaseAgent` discards that turn.
26
+ - `getModel()` returns the model id this instance talks to (e.g. `"deepseek-v4-flash"`). It is stamped onto every usage entry the agent records for this provider, so a total can be broken down per model and not just per role — see [Token accounting](./agents.md#token-accounting). A custom provider must implement it; return whatever string identifies the model you're billing against.
27
+
28
+ - `getCapabilities()` returns a static description used by `BaseAgent` to decide things like when to trigger memory compaction — it is **not** re-queried per call:
29
+
30
+ ```ts
31
+ interface ProviderCapabilities {
32
+ supportsTools: boolean; // must be true — BaseAgent requires native tool-calling
33
+ supportsImages: boolean; // whether this provider/model accepts image content
34
+ contextWindow: number; // total context window, in tokens
35
+ safeUsageRatio?: number; // fraction of contextWindow safe to fill before compacting (default 0.5)
36
+ supportsThinking?: boolean; // whether reasoning/thinking text is surfaced — see "Think effort" below
37
+ maxOutputTokens?: number; // the model's max output tokens per response, where confirmed — not yet enforced anywhere, reserved for a future release
38
+ supportsVideo?: boolean; // whether this provider/model accepts video content
39
+ supportsAudio?: boolean; // whether this provider/model accepts audio content
40
+ supportsImageGeneration?: boolean; // whether this provider/model can generate new images
41
+ supportsImageEditing?: boolean; // whether this provider/model can edit an existing image
42
+ supportsVideoGeneration?: boolean; // whether this provider/model can generate new video
43
+ supportsVideoEditing?: boolean; // whether this provider/model can edit existing video
44
+ supportsAudioGeneration?: boolean; // whether this provider/model can generate new audio (speech, music, sound effects)
45
+ supportsAudioDesign?: boolean; // whether this provider/model can design/produce sound effects or soundscapes
46
+ supportsAudioClone?: boolean; // whether this provider/model can clone a voice from a reference sample
47
+ supports3DModelGeneration?: boolean; // whether this provider/model can generate a new 3D model
48
+ }
49
+ ```
50
+
51
+ These flags describe what the model itself can natively do — they're unrelated to `ProviderType` (see "Provider roles" below), which is just a role name used to look a provider up in a `ProviderRegistry`. A provider registered under the `"image-generation"` role doesn't need `supportsImageGeneration: true` (or vice versa) — the role name and the capability flag are independent. All four built-in providers are chat/vision LLMs, not generation APIs, so every one of the new flags above is always `false` on them.
52
+
53
+ `BaseAgent.run()` throws immediately if `getCapabilities().supportsTools` is `false` — the legacy JSON-in-text tool-call convention has been removed from the main loop. All providers default `supportsTools` to `true` except Ollama, which defaults it to `false` (many locally-hosted models don't support native function calling; pass `{ supportsTools: true }` explicitly if your model does).
54
+
55
+ ## Changing an option without reconstructing
56
+
57
+ `setOption(key, value)` mutates an already-constructed provider in place — e.g. `provider.setOption("thinkEffort", 0.8)` — instead of building a new one. Every built-in provider only allows keys it already reads fresh on every `chat()` call (never something baked into derived state at construction, like `contextWindow`); an unrecognized key throws.
58
+
59
+ | Provider | Settable keys |
60
+ |---|---|
61
+ | Anthropic | `thinkEffort`, `userId`, `disablePromptCaching`, `maxTokens`, `supportsTools` |
62
+ | OpenAI | `thinkEffort`, `userId`, `supportsTools` |
63
+ | DeepSeek | `thinkEffort`, `userId`, `supportsTools` |
64
+ | Ollama | `thinkEffort`, `supportsTools` |
65
+
66
+ ## Usage on the streaming path
67
+
68
+ OpenAI and DeepSeek's Chat-Completions-style streaming protocol doesn't compute token usage until a turn is fully complete — it arrives once, in a special final chunk (`stream_options: { include_usage: true }`), alongside `done: true`. Every chunk before that has `usage: { inputMissTokens: 0, inputCacheTokens: 0, outputTokens: 0 }` — not because those numbers are unknown-but-estimated, but because the API genuinely hasn't computed them yet. `OpenAIProvider` and `DeepSeekProvider` both trust this real, provider-reported final chunk directly — no local estimation involved on either. DeepSeek's final chunk includes its own `prompt_cache_hit_tokens`/`prompt_cache_miss_tokens` fields (falling back to `prompt_tokens_details.cached_tokens` if those aren't present), parsed the same way real cache data is read from any other provider.
69
+
70
+ If a call is cut off before that final chunk arrives (e.g. `BaseAgent.immediateStop()` aborting the connection to stop paying for further output), the real usage number simply never gets computed or sent for either provider — not partially, not late, just never — so `inputMissTokens`/`inputCacheTokens`/`outputTokens` stay `0` on `ChatResponse` too in that case. Anthropic doesn't have this problem (it streams real usage incrementally, see [Streaming](./streaming.md#usage-chunks)); Ollama has the same final-chunk-only limitation but isn't covered by what's described here.
71
+
72
+ This is the only usage source DeepSeek's streaming path has, so it's treated as the real number, not flagged as a lesser "estimate" — whatever lands on the final `done: true` chunk (and the returned `ChatResponse`) is what a consumer should use for billing/accounting, the same as it would for any other provider's real, API-reported usage. The two sides of that number are revealed at different points, though: `outputTokens` grows on every streamed chunk as content arrives, since it's cheap to recompute from what's been generated so far; `inputMissTokens`/`inputCacheTokens` are only computed once, on that final chunk, since the input side never changes mid-call and there's no reason to resend the same number on every content delta.
73
+
74
+ ## Think effort
75
+
76
+ Reasoning/thinking models each expose a different native "how hard should the model think" knob — OpenAI and Ollama use `low`/`medium`/`high`, DeepSeek uses `none`/`low`/`high`/`max`, Anthropic uses a numeric thinking-token budget. OrbitX replaces all of that with one universal option on every provider's constructor:
77
+
78
+ ```ts
79
+ thinkEffort?: number; // 0-1, e.g. 0.5 or 0.8 — never a provider-specific string
80
+ ```
81
+
82
+ Each provider maps that 0-1 value onto whatever scale it actually accepts (`src/core/think-effort.ts`'s `resolveThinkEffortLevel()` buckets it evenly across a provider's supported levels; Anthropic instead scales it into a `budget_tokens` range). A provider or model that doesn't support thinking at all — or doesn't recognize the resulting native value — simply ignores it:
83
+
84
+ ```ts
85
+ import { AnthropicProvider, DeepSeekProvider } from "orbitx";
86
+
87
+ const anthropicProvider = new AnthropicProvider("api-key", "claude-sonnet-5", { thinkEffort: 0.7 });
88
+ const deepseekProvider = new DeepSeekProvider("api-key", "deepseek-v4-flash", { thinkEffort: 0.3 });
89
+ ```
90
+
91
+ Per-provider specifics:
92
+
93
+ | Provider | Native scale | Notes |
94
+ |---|---|---|
95
+ | Anthropic | `thinking.budget_tokens` (numeric) | Scales with `maxTokens`; `max_tokens` sent to the API is automatically bumped so it always exceeds the thinking budget. Signed thinking blocks are carried forward automatically via `Message.providerThinking` when a thinking turn also made a tool call — required by Anthropic's API, handled for you. |
96
+ | DeepSeek | `none` / `low` / `high` / `max` | Sent as `reasoning_effort` on reasoner models. |
97
+ | OpenAI | `low` / `medium` / `high` | Sent as `reasoning_effort`, and **only** for reasoning-capable model families (`o1`/`o3`/`o4`/`gpt-5*` prefixes) — silently ignored for every other model, since the Chat Completions API rejects the param on non-reasoning models. |
98
+ | Ollama | `false` / `low` / `medium` / `high` | `thinkEffort <= 0` maps to `false` (works as an on/off switch for models that only understand a boolean, e.g. deepseek-r1/qwen3); higher values map to a level string for models that support graded effort (e.g. gpt-oss). |
99
+
100
+ See [Streaming](./streaming.md#thinking-chunks) for how (and whether) a given provider's reasoning text is observable while it streams.
101
+
102
+ ## User tracking & prompt caching
103
+
104
+ Every provider's constructor accepts `userId?: string` — an opaque end-user identifier forwarded to the underlying API for that provider's own abuse-monitoring/analytics, same universal-option pattern as `thinkEffort`: pass it uniformly and providers that have no equivalent field just ignore it.
105
+
106
+ | Provider | Where `userId` goes | Notes |
107
+ |---|---|---|
108
+ | Anthropic | `metadata.user_id` on every request | Anthropic's documented field for this purpose. |
109
+ | OpenAI | `user_id` on every request | Sent as an extra body field alongside the standard params. |
110
+ | DeepSeek | `user_id` on every request | Same mechanism as OpenAI (DeepSeek's API is OpenAI-compatible). |
111
+ | Ollama | Ignored | Accepted in the options type for interface parity only — Ollama's API has no equivalent field. |
112
+
113
+ **Anthropic prompt caching is on by default.** Every request marks the last content block with an ephemeral `cache_control` breakpoint, so a growing conversation gets served from cache instead of being fully reprocessed each turn — pass `disablePromptCaching: true` to send every request uncached. DeepSeek's cache is automatic server-side (no `cache_control` equivalent to toggle) and reports its own explicit `prompt_cache_hit_tokens`/`prompt_cache_miss_tokens`. OpenAI's cache is also automatic, reporting hits via `prompt_tokens_details.cached_tokens`. Ollama has no cache concept at all — `inputCacheTokens` is always `0`.
114
+
115
+ ## Built-in providers
116
+
117
+ ### Ollama
118
+
119
+ ```ts
120
+ import { OllamaProvider } from "orbitx";
121
+
122
+ new OllamaProvider(model: string, host = "http://localhost:11434", options?: {
123
+ supportsTools?: boolean; // default false
124
+ contextWindow?: number; // default 32_000
125
+ thinkEffort?: number; // 0-1, see "Think effort" above
126
+ userId?: string; // accepted for interface parity only — ignored, Ollama has no equivalent field
127
+ });
128
+ ```
129
+
130
+ Wraps the `ollama` npm package, talking to a local (or remote) Ollama server. Always reports `supportsImages: true`.
131
+
132
+ ### DeepSeek
133
+
134
+ ```ts
135
+ import { DeepSeekProvider } from "orbitx";
136
+
137
+ new DeepSeekProvider(apiKey: string, model = "deepseek-v4-flash", options?: {
138
+ supportsTools?: boolean; // default true
139
+ supportsImages?: boolean; // default: true only for deepseek-v4-flash-vision-exp
140
+ contextWindow?: number; // default 1_000_000 (deepseek-v4-flash / deepseek-v4-flash-vision-exp / deepseek-v4-pro)
141
+ thinkEffort?: number; // 0-1, see "Think effort" above
142
+ userId?: string; // see "User tracking & prompt caching" above
143
+ });
144
+ ```
145
+
146
+ Uses the `openai` SDK pointed at `https://api.deepseek.com` (DeepSeek's API is OpenAI-compatible). `getCapabilities().supportsImages` is `false` for every model except `deepseek-v4-flash-vision-exp`, which is `deepseek-v4-flash` plus native image input (same context window, same reasoning support) — pass `supportsImages: true` explicitly if a future model adds vision support before this SDK's allowlist is updated.
147
+
148
+ `stream_options: { include_usage: true }` is set on every streaming call, so `inputMissTokens`/`inputCacheTokens`/`outputTokens` are accurate on both the streaming and non-streaming paths — DeepSeek sends a usage-bearing final chunk (including its own `prompt_cache_hit_tokens`/`prompt_cache_miss_tokens` split) the same way OpenAI does.
149
+
150
+ ### Anthropic
151
+
152
+ ```ts
153
+ import { AnthropicProvider } from "orbitx";
154
+
155
+ new AnthropicProvider(apiKey: string, model = "claude-sonnet-5", options?: {
156
+ supportsTools?: boolean; // default true
157
+ supportsImages?: boolean; // default true
158
+ contextWindow?: number; // default from a per-model table, else 200_000; an explicit value here always wins over extendedContext below
159
+ maxTokens?: number; // output token cap, default 4096
160
+ thinkEffort?: number; // 0-1, see "Think effort" above
161
+ userId?: string; // see "User tracking & prompt caching" above
162
+ disablePromptCaching?: boolean; // default false — prompt caching is on by default, see above
163
+ extendedContext?: boolean | string; // default false/undefined — see "Context window" below
164
+ });
165
+ ```
166
+
167
+ Wraps `@anthropic-ai/sdk`. `maxTokens` is the only provider with an explicit output-length cap in its options today.
168
+
169
+ ### OpenAI
170
+
171
+ ```ts
172
+ import { OpenAIProvider } from "orbitx";
173
+
174
+ new OpenAIProvider(apiKey: string, model = "gpt-5", options?: {
175
+ supportsTools?: boolean; // default true
176
+ supportsImages?: boolean; // default true unless model is in a small denylist (e.g. o3-mini, o1-mini)
177
+ contextWindow?: number; // default from a per-model table, else 128_000
178
+ baseURL?: string; // point at an OpenAI-compatible endpoint
179
+ thinkEffort?: number; // 0-1, see "Think effort" above — ignored on non-reasoning models
180
+ userId?: string; // see "User tracking & prompt caching" above
181
+ });
182
+ ```
183
+
184
+ Uses the Chat Completions API (not the Responses API). `baseURL` makes this usable against any OpenAI-compatible provider, not just OpenAI itself.
185
+
186
+ ## Provider roles
187
+
188
+ An agent's providers are registered under open-ended role names, not a fixed pair. `"main"` — the provider driving the agent loop itself — is the only one that's required; every other role is optional and looked up on demand by whatever tool needs it.
189
+
190
+ ```ts
191
+ export type ProviderType =
192
+ | "main"
193
+ | "image-describer"
194
+ | "image-generation"
195
+ | "image-editing"
196
+ | "video-describer"
197
+ | "video-generation"
198
+ | "video-editing"
199
+ | "audio-describer"
200
+ | "audio-generation"
201
+ | "audio-design"
202
+ | "audio-clone"
203
+ | "3d-model-generator"
204
+ | "llm-low"
205
+ | "llm-medium"
206
+ | "llm-high"
207
+ | (string & {});
208
+ ```
209
+
210
+ `ProviderType` (exported from `orbitx`) is the documented starting vocabulary for every role name — not a closed union, and purely for naming/registry lookup. It has no relationship to `ProviderCapabilities` (see above) — a role name doesn't imply anything about what the provider registered under it can actually do. The trailing `(string & {})` (not bare `string`) is what makes the open-endedness work: a plain `| string` union collapses to just `string` and an editor stops suggesting the literals at all, while intersecting with `{}` keeps TS from widening it away, so a custom name still type-checks the moment nothing here fits. `llm-low`/`medium`/`high` are a self-declared overall quality/durability tier (cheap-and-good-enough vs. worth relying on for longer/harder tasks) — not a specific claim about reasoning power.
211
+
212
+ Pass a single provider to use it as `main` (same as today); pass an array of `{ type, provider, default? }` entries to register more:
213
+
214
+ ```ts
215
+ import { BaseAgent, DeepSeekProvider, OllamaProvider } from "orbitx";
216
+
217
+ const agent = new BaseAgent({
218
+ aiProvider: [
219
+ { type: "main", provider: new DeepSeekProvider("api-key", "deepseek-v4-flash") },
220
+ { type: "image-describer", provider: new OllamaProvider("llava:latest") },
221
+ ],
222
+ instruction: "You are a helpful assistant.",
223
+ mcpClient,
224
+ allowedTools: [/* ... */],
225
+ });
226
+ ```
227
+
228
+ `provider` can also be an array — a pool of candidates for that role — with `default` (0-based, defaults to `0`) picking which one is used when code needs exactly one. The rest of the pool is there to be enumerated later; nothing built-in does smarter selection than "the default" yet.
229
+
230
+ ### Resolving a provider by role
231
+
232
+ `ProviderRegistry.getProvider(type)` tries an exact role-name match first (that role's pool default). If nothing is registered under `type`, it falls back to scanning every provider registered under *any* role for the first one whose `getCapabilities()` reports the matching capability flag — not a generic capability bag keyed by `ProviderType` (that's gone; `ProviderType` is purely a registry role name and has no relationship to what a model can do), but a fixed, small mapping from a handful of well-known role names to their one corresponding `ProviderCapabilities` flag:
233
+
234
+ | Role (`ProviderType`) | Capability flag checked in the fallback |
235
+ |---|---|
236
+ | `"image-describer"` | `supportsImages` |
237
+ | `"image-generation"` | `supportsImageGeneration` |
238
+ | `"image-editing"` | `supportsImageEditing` |
239
+ | `"video-describer"` | `supportsVideo` |
240
+ | `"video-generation"` | `supportsVideoGeneration` |
241
+ | `"video-editing"` | `supportsVideoEditing` |
242
+ | `"audio-describer"` | `supportsAudio` |
243
+ | `"audio-generation"` | `supportsAudioGeneration` |
244
+ | `"audio-design"` | `supportsAudioDesign` |
245
+ | `"audio-clone"` | `supportsAudioClone` |
246
+ | `"3d-model-generator"` | `supports3DModelGeneration` |
247
+
248
+ This is why a single vision-capable `main` provider "just works" as an image describer with zero extra config: nothing needs to be registered under `"image-describer"` explicitly — the fallback finds `main` because its `getCapabilities().supportsImages` is `true`. Role names outside this table (`"main"`, `"llm-low"`/`"medium"`/`"high"`, or any custom string) have no capability equivalent, so the fallback never applies to them — an unregistered role like that returns `undefined` from `getProvider` regardless of what any registered provider can do.
249
+
250
+ `BaseAgent` calls `getProvider` directly, and so does `SwarmBase` when an agent is part of a swarm (see [Swarm](./swarm.md#providers-a-tool-reaches-for)) — a tool never touches a `ProviderRegistry`, or a raw provider, itself. It reaches this indirectly via `mcp.executeProvider(toolCallId, type, input)` (see [Tools](./tools.md#mcpexecuteprovider--calling-a-provider-from-inside-a-tool)), which works the same way whether the tool is registered on the local `MCPClient` or a sandboxed/remote `MCPServer` — the request is always resolved on `BaseAgent`'s side, never handed across as a live object.
251
+
252
+ `resolveAgentProviders()` (`AgentProvidersInput = AIProvider | AgentProviderEntry[]`) builds the `ProviderRegistry` this all runs on — both are exported from `orbitx` if you need them directly (`getMain()`, `getProvider(type)`, `getProviders(type)` for the full pool under an exact role). There is no separate "utils" role — every tool/skill is included in the system prompt from the start, so there's nothing left for a separate pass to decide.
253
+
254
+ ## Writing a custom provider
255
+
256
+ Extend `AIProvider` and implement `chat`/`getCapabilities`. Follow an existing provider (e.g. `src/core/ollama-provider.ts`) as a template: translate `Message[]` to your API's wire format, stream text chunks to `streamCallback` as they arrive, accumulate tool-call deltas and emit them on the final chunk, and translate `ToolSchema[]` into your API's function-calling format (see `src/core/tool-schema-translator.ts` for existing OpenAI/Anthropic translators you can reuse or reference).
257
+
258
+ `inputMissTokens`/`inputCacheTokens`/`outputTokens` on the returned `ChatResponse` are optional — every built-in provider is token-billed and always sets them, but a provider registered under a per-request-billed role (e.g. `image-generation`, `audio-clone`) should omit them and set `cost` instead. `BaseAgent#executeProvider` (see [Tools](./tools.md#mcpexecuteprovider--calling-a-provider-from-inside-a-tool)) checks `cost` first and records a `unit: "cost"` usage entry (stamped with the provider's `getModel()`, like every other entry) when it's present, falling back to the token fields (defaulting any missing one to `0`) otherwise — see [Token accounting](./agents.md#token-accounting).
package/docs/skills.md CHANGED
@@ -30,7 +30,6 @@ class Skill {
30
30
  | `CodeVerificationSkill` | Running the right type checker/build/lint/test suite before handing code back, and telling real bugs apart from sandbox artifacts. |
31
31
  | `WebEndToEndTestSkill` | Testing a live web app like a real user would (fill, click, navigate) and verifying via the rendered page, console, and network. |
32
32
  | `ResearchSkill` | Answering questions that need real, current information from the live web — cross-checks multiple sources. |
33
- | `ShoppingSkill` | Purchase-decision help — real current listings, prices, links, and review sentiment instead of recommending from memory. |
34
33
  | `PresentSkill` | Deciding how to hand back a job's output as files (individually or zipped) and cleaning build artifacts out first — pairs with `PresentTools`. |
35
34
  | `QuestionAnswerSkill` | Pausing to ask the user for missing information, a decision, or confirmation before continuing, instead of guessing — batches every pending question into one call and stops the turn immediately after asking (`QuestionAnswerTool` sets `stopIterationAfterUsingThisTool`). |
36
35
  | `QuestionAnswerSkill` | Wraps `QuestionAnswerTool` with instructions for when it's appropriate to stop and ask the human operator a clarifying question. |