@x12i/ai-dispatcher 1.4.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 x12i
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,363 @@
1
+ # @x12i/ai-dispatcher
2
+
3
+ One request shape for OpenRouter, AWS Bedrock, and the OpenAI Responses API. You send the same request shape. The package picks the provider, and `@x12i/ai-profiles@5.0.0` turns `reasoningEffort` into that provider's wire fields. Callers do not map effort levels onto provider fields or parse provider reasoning channels.
4
+
5
+ **Implemented providers:** `openrouter`, `bedrock`, `openai`. Any other provider throws `PROVIDER_NOT_IMPLEMENTED`.
6
+
7
+ The package is `@x12i/ai-dispatcher` 1.4.0. It runs on Node 20 or newer and publishes ESM and CommonJS from the same entry.
8
+
9
+ Connected MCP servers can be passed to `createAiDispatcher`. `run()` and `compile()` expose those tools to the model. `executeStreamingChat` does not. Details are in [MCP tools](#mcp-tools).
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @x12i/ai-dispatcher
15
+ ```
16
+
17
+ The package depends on `@x12i/ai-profiles@5.0.0`, `@x12i/openrouter-runtime@^1.2.0`, and `@x12i/bedrock-runtime@^1.0.0`.
18
+
19
+ ## One request
20
+
21
+ ```ts
22
+ import { createAiDispatcher } from "@x12i/ai-dispatcher";
23
+
24
+ const dispatcher = createAiDispatcher({
25
+ openrouter: { apiKey: process.env.OPENROUTER_API_KEY },
26
+ bedrock: { region: process.env.AWS_REGION },
27
+ openai: { apiKey: process.env.OPENAI_API_KEY }
28
+ });
29
+
30
+ const response = await dispatcher.run({
31
+ provider: "openai",
32
+ model: "gpt-5.4",
33
+ prompt: "Explain the tradeoff in one paragraph.",
34
+ reasoningEffort: "low"
35
+ });
36
+
37
+ console.log(response.text);
38
+ console.log(response.reasoningText);
39
+ console.log(response.reasoningResolution);
40
+ ```
41
+
42
+ Provider selection is `request.provider`, then `options.provider`, then `openrouter`. Model selection is `request.model`, then that provider's `defaultModel`.
43
+
44
+ `openai` sends `POST {baseUrl}/responses`. The default base URL is `https://api.openai.com/v1`, and the catalog `reasoning` object is placed on that Responses body.
45
+
46
+ Use model ids that belong to the selected catalog target. An OpenRouter id is not a Bedrock id and is not an OpenAI id.
47
+
48
+ | Target | Example model id | `reasoningEffort: "low"` on the wire |
49
+ | --- | --- | --- |
50
+ | `openrouter` | `openai/gpt-5.2` | `reasoning.effort: "low"` on the chat-completions body |
51
+ | `bedrock` | `anthropic.claude-fable-5-1` | `additionalModelRequestFields.thinking.type: "adaptive"` and `output_config.effort: "low"` |
52
+ | `openai` | `gpt-5.4` | `reasoning.effort: "low"` on the Responses body |
53
+
54
+ Representative catalog results for `@x12i/ai-profiles@5.0.0`:
55
+
56
+ - OpenRouter `~google/gemini-flash-latest` + `max` is `degraded` and sends `reasoning.effort: "high"`.
57
+ - OpenRouter `aion-labs/aion-2.0` is `ignored` with an empty reasoning body. The call still runs.
58
+ - Bedrock `anthropic.claude-fable-5-1` + `max` is `degraded` and sends `output_config.effort: "high"`.
59
+ - OpenAI `gpt-5.4` + `max` is `applied` and sends `reasoning.effort: "max"`.
60
+ - OpenAI `gpt-4o` is `ignored`. The Responses call still runs.
61
+
62
+ The type `AiProviderId` also lists `anthropic`, `google`, `azure`, `groq`, `mistral`, `cohere`, `together`, `fireworks`, `xai`, `deepseek`, and `custom`. Passing any of those throws `PROVIDER_NOT_IMPLEMENTED`.
63
+
64
+ ## Configuration
65
+
66
+ ```ts
67
+ createAiDispatcher({
68
+ provider: "openrouter",
69
+ logger: {
70
+ info: (event, data) => console.log(event, data)
71
+ },
72
+ openrouter: {
73
+ apiKey: process.env.OPENROUTER_API_KEY,
74
+ defaultModel: "openai/gpt-5.2"
75
+ },
76
+ bedrock: {
77
+ region: process.env.AWS_REGION ?? "us-east-1",
78
+ defaultModel: "anthropic.claude-fable-5-1"
79
+ },
80
+ openai: {
81
+ apiKey: process.env.OPENAI_API_KEY, // or OPENAI_API_KEY
82
+ baseUrl: "https://api.openai.com/v1",
83
+ defaultModel: "gpt-5.4",
84
+ organization: process.env.OPENAI_ORG_ID,
85
+ project: process.env.OPENAI_PROJECT_ID,
86
+ timeoutMs: 60_000,
87
+ maxAttempts: 2
88
+ }
89
+ });
90
+ ```
91
+
92
+ `dispatcher.provider` is the default provider id. Adapters are created on first use and reused. The default provider's adapter is created immediately.
93
+
94
+ The `logger` callback is `{ debug, info, warn, error }`. A provider-specific `logger` on `openrouter`, `bedrock`, or `openai` wins over the dispatcher logger for that adapter. You can inject `fetch` on OpenAI and OpenRouter, and a `client` on Bedrock.
95
+
96
+ Missing OpenAI credentials throw `OPENAI_API_KEY_MISSING`. Missing Bedrock region throws the runtime's `REGION_REQUIRED`. Missing OpenRouter key throws `OPENROUTER_API_KEY_MISSING`.
97
+
98
+ ### Credentials and transport
99
+
100
+ | Provider | Required | Default transport |
101
+ | --- | --- | --- |
102
+ | OpenRouter | `openrouter.apiKey` | OpenRouter chat completions |
103
+ | Bedrock | `bedrock.region` | Converse / ConverseStream. Credentials are the AWS default chain, or `bedrock.credentials` |
104
+ | OpenAI | `openai.apiKey` or `OPENAI_API_KEY` | `POST {baseUrl}/responses`. Default base URL is `https://api.openai.com/v1`. Default timeout is 120 seconds. Default attempts is 2 |
105
+
106
+ Optional OpenAI headers are `OpenAI-Organization` and `OpenAI-Project`.
107
+
108
+ OpenAI retries `429`, `408`, and `5xx` with backoff capped at 5 seconds. After the last retryable failure, `run()` returns a failed response and streaming yields `stream.error` and then throws. `401` and `403` become `PROVIDER_AUTH_FAILED`. `404` becomes `PROVIDER_MODEL_NOT_FOUND`. Other HTTP failures become `PROVIDER_REQUEST_FAILED`. Timeouts and network failures are `PROVIDER_GATEWAY_UNREACHABLE`. Config mistakes (`OPENAI_API_KEY_MISSING`, `MODEL_REQUIRED`, `INPUT_REQUIRED`, `UNSUPPORTED_REQUEST_FIELD`) throw from `run()` instead of becoming a failed response.
109
+
110
+ ## The request
111
+
112
+ `AiDispatcherRequest` is the OpenRouter runtime request plus `provider`, `reasoningEffort`, `bedrockControls`, and `mcp`.
113
+
114
+ Text can arrive as `prompt`, `messages`, `input`, `system`, or `instructions`. A message role is `system`, `user`, `assistant`, or `tool`. Content is a string or parts: `text`, `image_url`, or `file`.
115
+
116
+ | Field | Meaning |
117
+ | --- | --- |
118
+ | `id` | Caller request id. Logged when set. OpenAI uses it as a fallback response id. |
119
+ | `temperature`, `maxTokens` | Sampling and output cap. OpenAI sends `maxTokens` as `max_output_tokens`. |
120
+ | `reasoning` | Explicit wire object for OpenRouter and OpenAI. |
121
+ | `functionTools` | Local function tools: `name`, `description`, `parameters`, optional `executor`. |
122
+ | `toolChoice` | `"auto"`, `"none"`, `"required"`, `{ type: "function", functionName }`, or an OpenRouter `{ type: "server_tool", serverTool }`. |
123
+ | `execution` | `maxToolIterations`, `maxServerToolCalls`, `maxFunctionToolCalls`, `timeoutMs`. Bedrock keeps the iteration, function-call, and timeout limits. |
124
+ | `metadata` | Passed through. |
125
+ | `serverTools` | OpenRouter server tools only. Bedrock and direct OpenAI reject an enabled policy. |
126
+ | `apiMode` | `"chat"`, `"responses"`, or `"auto"`. Direct OpenAI rejects `"chat"`. Bedrock rejects any `apiMode`. |
127
+ | `responseFormat` | OpenRouter forwards it. Direct OpenAI places it on the Responses `text` field. Bedrock rejects it. |
128
+ | `rawOpenRouterOverrides` | Merged onto an OpenRouter body. Rejected on Bedrock and direct OpenAI. |
129
+ | `mcp` | `false` or `[]` omits MCP tools registered at initialization. A list of exposed names attaches only those tools. Omit the field to attach every registered MCP tool. |
130
+
131
+ OpenAI compilation order: `instructions` wins, then `system`, then system messages joined with newlines. `input` is sent as written. Otherwise non-system messages are converted, and a string `prompt` is appended as a user item. Tool messages become `function_call_output`. Image parts become `input_image`. A file part with `url` becomes `input_file` `file_url`. A file part with `data` becomes `input_file` `file_data`. `fileName` becomes `filename`. A file part with neither `url` nor `data` throws `UNSUPPORTED_REQUEST_FIELD`.
132
+
133
+ Bedrock receives `id`, `model`, `messages`, `system`, `prompt`, `temperature`, `maxTokens`, `functionTools`, a Bedrock-compatible `toolChoice`, the execution limits above, and `metadata`. `instructions` is used as `system` when `system` is empty. A string `input` is used as `prompt` when `prompt` and `messages` are empty. `input` items with a role and content become `messages` when `messages` is empty. Sending both `messages` and `input` items throws `UNSUPPORTED_REQUEST_FIELD`. An enabled server tool, `responseFormat`, `rawOpenRouterOverrides`, `apiMode`, or a `server_tool` tool choice also throws. `toolChoice: "none"` omits tools. A `data:image/(png|jpeg|jpg|gif|webp);base64,...` part is sent as Converse image bytes (`jpg` is sent as `jpeg`). Any other image URL, and any other non-text part, throws `UNSUPPORTED_CONTENT`.
134
+
135
+ ## Entrypoints
136
+
137
+ | Method | What it does |
138
+ | --- | --- |
139
+ | `dispatcher.run(request)` | Non-streaming call. Returns answer text, optional reasoning text, usage, tool results, and diagnostics. |
140
+ | `dispatcher.executeStreamingChat(request)` | The only streaming entrypoint. Yields typed events. |
141
+ | `dispatcher.compile(request)` | Same preparation as a live call, without sending. |
142
+ | `dispatcher.refreshMcp()` | Reloads MCP servers that implement `listTools`. A failed reload keeps the previous list. |
143
+
144
+ `compile()` returns a provider payload plus `requestedModel`, `effectiveModel`, and `reasoningResolution` when reasoning was involved:
145
+
146
+ - OpenRouter: `{ provider, url, headers, body, warnings }`
147
+ - Bedrock: `{ provider, modelId, input }`
148
+ - OpenAI: `{ provider, url, headers, body, warnings }`
149
+
150
+ `Authorization`, `x-api-key`, and `api-key` are replaced with `[redacted]` in the compiled result. Execution sends the real headers. A streaming call adds only the transport stream flag (`stream: true`, or Bedrock `ConverseStream`).
151
+
152
+ ## MCP tools
153
+
154
+ Pass connected MCP servers to `createAiDispatcher`. The dispatcher does not spawn processes or open transports. It lists the tools you already fetched and calls `callTool` when the model uses one.
155
+
156
+ ```ts
157
+ const dispatcher = createAiDispatcher({
158
+ openai: { apiKey: process.env.OPENAI_API_KEY },
159
+ mcp: {
160
+ servers: [{
161
+ id: "fs",
162
+ tools: [{
163
+ name: "read_file",
164
+ description: "Read a file",
165
+ inputSchema: { type: "object", properties: { path: { type: "string" } } }
166
+ }],
167
+ callTool: (name, args) => session.callTool({ name, arguments: args }),
168
+ listTools: async () => (await session.listTools()).tools
169
+ }]
170
+ }
171
+ });
172
+
173
+ await dispatcher.run({ model: "gpt-5.4", prompt: "Read notes.txt" });
174
+ ```
175
+
176
+ Each tool is exposed as `${serverId}__${toolName}`. `callTool` still receives the raw MCP name. Server ids and tool names must match `^[A-Za-z0-9_-]+$`, and the exposed name must be at most 64 characters. A missing description is sent as an empty string. A missing `inputSchema` is sent as `{ type: "object", properties: {} }`.
177
+
178
+ `run()` and `compile()` attach every registered tool. `executeStreamingChat` does not attach them and does not run them. Set `mcp: false` or `mcp: []` on a request to attach none. Set `mcp: ["fs__read_file"]` to attach that tool only. An unknown name in that list throws `MCP_TOOL_NOT_FOUND`.
179
+
180
+ A `functionTools` entry with the same exposed name replaces the MCP tool for that call. If that entry has no executor, the MCP `callTool` does not run. An executor already stored on `openrouter.tools`, `bedrock.tools`, or `openai.tools` under the exposed name runs instead of `callTool`.
181
+
182
+ Text blocks in an MCP result are joined and returned to the model. `isError: true` fails that tool call. Any other result is JSON. Bedrock `toolChoice: "none"` still omits tools. OpenRouter advisor, subagent, and fusion cannot call these sessions.
183
+
184
+ `await dispatcher.refreshMcp()` reloads servers that implement `listTools` and leaves the others unchanged. Overlapping reloads run one after another. A failed reload, including a list that breaks the name rules, throws `MCP_LIST_FAILED` and keeps the previous catalog. A call already in progress keeps the tools it prepared.
185
+
186
+ ## Reasoning effort
187
+
188
+ `reasoningEffort` is `minimal | low | medium | high | max`.
189
+
190
+ | Request | Catalog |
191
+ | --- | --- |
192
+ | No `reasoningEffort` and no explicit control | Not called. The model id is forwarded as written, including ids the catalog does not know. |
193
+ | `reasoningEffort` only | `resolveReasoning({ target, model, effort })` once for the original model. A catalog replacement model is the effective model and is not resolved again. |
194
+ | Explicit control only | Kept. The catalog is not called. |
195
+ | Both | The explicit control wins. The catalog body, model switch, and prompt prefix are not applied. `reasoningResolution.bypassed` is `true`, and `outcome` is omitted. |
196
+
197
+ Explicit controls are transport fields:
198
+
199
+ - OpenRouter and OpenAI: `request.reasoning`.
200
+ - Bedrock: `request.bedrockControls.additionalModelRequestFields`.
201
+
202
+ An OpenRouter `reasoning` object on a Bedrock call is not a Bedrock control. With `reasoningEffort`, the catalog still runs and that object is removed. Without `reasoningEffort`, the call fails with `UNSUPPORTED_REQUEST_FIELD`.
203
+
204
+ Outcomes:
205
+
206
+ - `applied` — the catalog wrote the requested effort.
207
+ - `degraded` — the catalog wrote a supported nearby effort. The call still runs.
208
+ - `ignored` — the model has no reasoning control. The call still runs with an empty catalog body.
209
+
210
+ Unknown models and missing mappings throw. The error keeps the catalog `code` and `details`, including `UNKNOWN_MODEL` and `REASONING_MAPPING_MISSING`. `reasoningEffort` without any model id throws `REASONING_MODEL_REQUIRED`.
211
+
212
+ The dispatcher does not rank efforts, invent token budgets, or rewrite `maxTokens`. Profile notes about output-token headroom stay on the catalog profile. The directive body already contains companion fields the wire needs, and those fields are merged onto the compiled body.
213
+
214
+ ## Answers and reasoning text
215
+
216
+ `response.text` is answer text. `response.reasoningText` is set only when the provider returned reasoning text. It is omitted when there is none.
217
+
218
+ `response.usage` keeps the runtime usage fields: `inputTokens`, `outputTokens`, `totalTokens`, `costUsd`, and `raw`. When the catalog ran and published reasoning-token paths, `usage.reasoningTokens` is the first path that yields a number. A path that yields nothing removes a runtime-supplied `reasoningTokens` value.
219
+
220
+ `response.reasoningResolution` is present when `reasoningEffort` was set or an explicit control bypassed the catalog:
221
+
222
+ ```ts
223
+ {
224
+ requestedEffort: "max",
225
+ target: "openrouter",
226
+ requestedModel: "~google/gemini-flash-latest",
227
+ effectiveModel: "~google/gemini-flash-latest",
228
+ outcome: "degraded",
229
+ bypassed: false
230
+ }
231
+ ```
232
+
233
+ The same record is on `compile()` output, `stream.start`, and `stream.done`.
234
+
235
+ The rest of the response is the runtime shape: `id`, `status` (`completed`, `failed`, `requires_action`, `policy_violation`), `apiMode`, `model`, `messages`, `citations`, `images`, `patches`, `toolUsage`, `finishReason`, `warnings`, `errors`, `raw`, and `metadata`. Direct OpenAI sets `apiMode` to `"responses"` and returns function calls as `requires_action` without running them.
236
+
237
+ Streaming emits `stream.reasoning.delta` for thought text. That text is not copied into `stream.text.delta`, tool-argument deltas, or structured-output text. If a future catalog asks for think-tag stripping, tags are split out of answer deltas before those deltas are yielded.
238
+
239
+ The dispatcher does not store conversation history. When a directive lists history fields to drop, callers pass those paths to `stripReasoningHistoryFields(message, paths)`. The helper clones the message, deletes only those pointers, and is safe to call twice. Catalog 5.0.0 paths are empty, so a real directive deletes nothing.
240
+
241
+ ## Streaming
242
+
243
+ ```ts
244
+ for await (const event of dispatcher.executeStreamingChat(request)) {
245
+ if (event.type === "stream.text.delta") process.stdout.write(event.data.text);
246
+ if (event.type === "stream.reasoning.delta") { /* thought text */ }
247
+ }
248
+ ```
249
+
250
+ Events, each with `type`, `requestId`, and `data`:
251
+
252
+ | Event | `data` |
253
+ | --- | --- |
254
+ | `stream.start` | `model`, `apiMode`, `entrypoint`, and `reasoningResolution` when present |
255
+ | `stream.text.delta` | `{ text }` |
256
+ | `stream.reasoning.delta` | `{ text }` |
257
+ | `stream.tool_call.delta` | `index`, optional `id`, `name`, `argumentsDelta` |
258
+ | `stream.usage` | `{ usage }` |
259
+ | `stream.warning` | `{ warning }` |
260
+ | `stream.error` | `{ error }` |
261
+ | `stream.done` | `text`, optional `finishReason`, `model`, optional `reasoningText`, optional `reasoningResolution` |
262
+
263
+ ## Tools
264
+
265
+ Local function tools work on all three providers. OpenRouter, Bedrock, and direct OpenAI run the tool loop when every returned call has an `executor` on the tool or in that provider's `tools` registry. The loop defaults are 8 iterations and 20 function calls. On direct OpenAI, a call with no executor stops the loop: `run()` returns `requires_action` and does not send another request. Send that tool result back as a `role: "tool"` message with `toolCallId`. Streaming stays one request and does not run executors. Exceeding `maxFunctionToolCalls` on direct OpenAI fails the run with `FUNCTION_TOOL_LIMIT`.
266
+
267
+ OpenRouter server tools are `webSearch`, `webFetch`, `datetime`, `imageGeneration`, `applyPatch`, `fusion`, `advisor`, and `subagent`. Each policy has a `mode` of `disabled`, `allowed`, or `required`. Bedrock and direct OpenAI throw `UNSUPPORTED_REQUEST_FIELD` when any of them is enabled, and also reject a `server_tool` tool choice. A policy whose mode is `disabled` is not enabled.
268
+
269
+ ### Nested OpenRouter tools
270
+
271
+ Fusion, advisor, and subagent policies can set `reasoning` or `reasoningEffort`. They are not separate dispatcher calls. Before compilation, and only for OpenRouter, the dispatcher settles each policy once:
272
+
273
+ 1. A child `reasoning` object stays.
274
+ 2. Otherwise a child `reasoningEffort` is resolved with target `openrouter` and that tool's model.
275
+ 3. Otherwise a parent `reasoningEffort` is resolved the same way for advisor `model`, subagent `model`, and fusion `judgeModel`.
276
+ 4. Otherwise, when the parent supplied only an explicit `reasoning` object, the OpenRouter runtime copies that object onto nested tools that omit their own.
277
+ 5. A nested tool with no model id is not looked up and does not inherit the parent wire body.
278
+
279
+ Each nested model is resolved on its own. A parent Bedrock or OpenAI body is never reused as another target's thinking config. A parent `reasoningEffort` also stops the OpenRouter runtime from copying the parent `reasoning` object onto those nested tools.
280
+
281
+ ## Escape hatches
282
+
283
+ Set the provider control and leave `reasoningEffort` unset when you already have a wire object:
284
+
285
+ ```ts
286
+ await dispatcher.run({
287
+ provider: "openrouter",
288
+ model: "openai/gpt-5.2",
289
+ prompt: "Hello",
290
+ reasoning: { effort: "low" }
291
+ });
292
+
293
+ await dispatcher.run({
294
+ provider: "bedrock",
295
+ model: "anthropic.claude-fable-5-1",
296
+ prompt: "Hello",
297
+ bedrockControls: {
298
+ additionalModelRequestFields: {
299
+ thinking: { type: "adaptive" },
300
+ output_config: { effort: "low" }
301
+ }
302
+ }
303
+ });
304
+ ```
305
+
306
+ `rawOpenRouterOverrides` still merges onto an OpenRouter body. It is rejected on Bedrock and direct OpenAI.
307
+
308
+ ## Supported features
309
+
310
+ | Feature | OpenRouter | Bedrock | OpenAI Responses |
311
+ | --- | --- | --- | --- |
312
+ | `run`, `compile`, `executeStreamingChat` | yes | yes | yes |
313
+ | `reasoningEffort` via the catalog | yes | yes | yes |
314
+ | Explicit reasoning control | `reasoning` | `bedrockControls.additionalModelRequestFields` | `reasoning` |
315
+ | Local function tools | yes, including the tool loop | yes, including the tool loop | yes, when every returned call has an executor; otherwise `requires_action` |
316
+ | MCP tools from `mcp.servers` | `run` and `compile` | `run` and `compile` | `run` and `compile`. Streaming does not receive them |
317
+ | OpenRouter server tools (fusion, advisor, subagent, datetime, and the rest) | yes | `UNSUPPORTED_REQUEST_FIELD` when enabled | `UNSUPPORTED_REQUEST_FIELD` when enabled |
318
+ | `responseFormat` | forwarded | `UNSUPPORTED_REQUEST_FIELD` | placed on Responses `text` |
319
+ | `apiMode: "chat"`, `rawOpenRouterOverrides` | OpenRouter fields | `UNSUPPORTED_REQUEST_FIELD` | `UNSUPPORTED_REQUEST_FIELD` |
320
+ | File content parts | forwarded | non-text parts throw `UNSUPPORTED_CONTENT`, except data-URL images | `input_file` |
321
+
322
+ ## Errors
323
+
324
+ Catalog failures throw `AiDispatcherError` with `code`, `message`, and the catalog `details`. Dispatcher codes include:
325
+
326
+ | Code | When |
327
+ | --- | --- |
328
+ | `PROVIDER_NOT_IMPLEMENTED` | Provider is not `openrouter`, `bedrock`, or `openai` |
329
+ | `PROVIDER_REQUIRED` | No provider string is available |
330
+ | `PROVIDER_ADAPTER_MISSING` | An implemented provider has no adapter |
331
+ | `REASONING_MODEL_REQUIRED` | `reasoningEffort` is set and no model id is available |
332
+ | `REASONING_PREFIX_UNSUPPORTED` | A catalog prompt prefix has no user text to attach to |
333
+ | `UNSUPPORTED_REQUEST_FIELD` | A field cannot be sent on the selected provider |
334
+ | `MODEL_REQUIRED` | Direct OpenAI has no model id |
335
+ | `INPUT_REQUIRED` | Direct OpenAI has no messages, input, or prompt |
336
+ | `OPENAI_API_KEY_MISSING` | Direct OpenAI has no key |
337
+ | `PROVIDER_AUTH_FAILED` | OpenAI HTTP `401` or `403` |
338
+ | `PROVIDER_MODEL_NOT_FOUND` | OpenAI HTTP `404` |
339
+ | `PROVIDER_REQUEST_FAILED` | Other OpenAI HTTP failures |
340
+ | `PROVIDER_RETRYABLE` / `PROVIDER_GATEWAY_UNREACHABLE` | OpenAI transport failures after retries, including timeout |
341
+ | `FUNCTION_TOOL_LIMIT` | Direct OpenAI function-tool calls exceed `maxFunctionToolCalls` |
342
+ | `UNSUPPORTED_CONTENT` | Bedrock received an image URL or content part it cannot send |
343
+ | `MCP_SERVER_INVALID` | An MCP server is missing an id, a tools array, or `callTool`, or a server id is repeated |
344
+ | `MCP_TOOL_NAME_INVALID` | An MCP server id or tool name uses characters other than letters, numbers, underscores, and hyphens, or the exposed name is longer than 64 characters |
345
+ | `MCP_TOOL_NAME_CONFLICT` | Two MCP tools share an exposed name |
346
+ | `MCP_TOOL_NOT_FOUND` | `request.mcp` names a tool that is not registered, or `request.mcp` is not `false` or a list of names |
347
+ | `MCP_LIST_FAILED` | `refreshMcp()` failed, or the new list breaks the name rules. The previous catalog stays |
348
+
349
+ OpenAI HTTP failures such as `401` return a failed response from `run()`. `FUNCTION_TOOL_LIMIT` does too. Streaming yields `stream.error` and then throws, matching the other transports.
350
+
351
+ ## Logging
352
+
353
+ Pass `logger` on the dispatcher. `run` and `executeStreamingChat` emit `ai-dispatcher.request.started` at info. `compile` emits `ai-dispatcher.request.compiled` at debug. The record can include `provider`, `entrypoint`, `streaming`, `requestedModel`, `effectiveModel`, `requestId`, `outcome`, and `bypassed`. It does not include credentials, prompts, reasoning text, or raw bodies.
354
+
355
+ ## Limitations
356
+
357
+ - Catalog 5.0.0 does not currently emit a prompt prefix, a replacement model, think-tag stripping, history deletions, or a reasoning token budget. Those paths exist for the directive type and are covered by synthetic tests.
358
+ - Provenance notes about output-token headroom are not enforced, because the resolver does not return them as fields.
359
+ - OpenRouter server tools, `apiMode: "chat"`, and `rawOpenRouterOverrides` stay on OpenRouter. Bedrock and direct OpenAI reject them.
360
+ - Direct OpenAI streaming does not run the function-tool loop. MCP tools are not attached to `executeStreamingChat` on any provider.
361
+ - OpenRouter advisor, subagent, and fusion cannot call MCP sessions registered on the dispatcher.
362
+ - Bedrock `run()` returns a failed response for `UNSUPPORTED_CONTENT`. `compile()` throws that error.
363
+ - Live provider calls are not part of the package test suite. Tests mock HTTP and the Bedrock client and use the real 5.0.0 catalog for contract cases.