@x12i/ai-dispatcher 2.2.0 → 2.3.1
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/README.md +22 -11
- package/dist/index.cjs +36 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -5
- package/dist/index.d.ts +19 -5
- package/dist/index.js +35 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# @x12i/ai-dispatcher
|
|
2
2
|
|
|
3
|
-
One request shape for OpenRouter, AWS Bedrock, the OpenAI Responses API, and Cloudflare AI. You send the same request shape. The package picks the provider, and `@x12i/ai-profiles
|
|
3
|
+
One request shape for OpenRouter, AWS Bedrock, the OpenAI Responses API, and Cloudflare AI. You send the same request shape. The package picks the provider, and `@x12i/ai-profiles@^5.1.0` turns `reasoningEffort` into that provider's wire fields. Callers do not map effort levels onto provider fields or parse provider reasoning channels.
|
|
4
4
|
|
|
5
5
|
**Implemented providers:** `openrouter`, `bedrock`, `openai`, `cloudflare`. Any other provider throws `PROVIDER_NOT_IMPLEMENTED`.
|
|
6
6
|
|
|
7
|
-
The package is `@x12i/ai-dispatcher` 2.
|
|
7
|
+
The package is `@x12i/ai-dispatcher` 2.3.1. It runs on Node 20 or newer and publishes ESM and CommonJS from the same entry.
|
|
8
8
|
|
|
9
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
10
|
|
|
@@ -14,7 +14,7 @@ Connected MCP servers can be passed to `createAiDispatcher`. `run()` and `compil
|
|
|
14
14
|
npm install @x12i/ai-dispatcher
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
The package depends on `@x12i/ai-profiles
|
|
17
|
+
The package depends on `@x12i/ai-profiles@^5.1.0`, `@x12i/provider-metadata@^1.2.0`, `@x12i/openrouter-runtime@^2.2.0`, and `@x12i/bedrock-runtime@^2.2.0`. `registerHostIntents` is re-exported from this package so a host registers intents on the same `@x12i/ai-profiles` copy the dispatcher resolves with.
|
|
18
18
|
|
|
19
19
|
## One request
|
|
20
20
|
|
|
@@ -56,7 +56,7 @@ Use model ids that belong to the selected catalog target. An OpenRouter id is no
|
|
|
56
56
|
| `openai` | `gpt-5.4` | `reasoning.effort: "low"` on the Responses body |
|
|
57
57
|
| `cloudflare` | `openai/gpt-5.4` | `reasoning.effort: "low"` on the Cloudflare Responses body |
|
|
58
58
|
|
|
59
|
-
Representative catalog results for `@x12i/ai-profiles@5.
|
|
59
|
+
Representative catalog results for `@x12i/ai-profiles@5.1.0`:
|
|
60
60
|
|
|
61
61
|
- OpenRouter `~google/gemini-flash-latest` + `max` is `degraded` and sends `reasoning.effort: "high"`.
|
|
62
62
|
- OpenRouter `aion-labs/aion-2.0` is `ignored` with an empty reasoning body. The call still runs.
|
|
@@ -153,7 +153,7 @@ Text can arrive as `prompt`, `messages`, `input`, `system`, or `instructions`. A
|
|
|
153
153
|
| `cloudflare` | Per-call overrides: `endpoint`, `gatewayId`, `run`, and `cf-aig-*` controls. Stripped before compilation. |
|
|
154
154
|
| `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. |
|
|
155
155
|
|
|
156
|
-
Set `metadata` for the pairs you want back from a log. `agentId`, `orgId`, `stepId`, and `skillId` are fields on the same record. A dedicated field wins over the same key inside `metadata`. Omit an id you do not have. The dispatcher does not invent one.
|
|
156
|
+
Set `metadata` for the pairs you want back from a log. `agentId`, `orgId`, `stepId`, and `skillId` are fields on the same record. A `role` object expands to `role`, `roleLayer`, `roleDigest`, and `profileChoice` on that record. A dedicated field wins over the same key inside `metadata`. Omit an id you do not have. The dispatcher does not invent one.
|
|
157
157
|
|
|
158
158
|
```ts
|
|
159
159
|
import { decodeProviderMetadata } from "@x12i/ai-dispatcher";
|
|
@@ -177,7 +177,7 @@ const record = decodeProviderMetadata(logMetadata);
|
|
|
177
177
|
|
|
178
178
|
`metadata` is the one record. When `rawOpenRouterOverrides` only repeats `metadata`, the dispatcher folds it in. `metadata` wins when the same key is in both places. String, number, and boolean values are stored, including a JSON string. Objects and arrays stay on the normalized response and the tool context.
|
|
179
179
|
|
|
180
|
-
Every provider writes that record as `m.0`, `m.1`, and so on: one base64 string, 256 characters per piece, at most 16 pieces. The base64 is the UTF-8 JSON object of the string map, with keys sorted. `decodeProviderMetadata` reads those pieces from OpenRouter body `metadata`, OpenAI Responses `metadata`, Bedrock `requestMetadata`, or Cloudflare body `metadata` and `cf-aig-metadata`. The pieces are the same on every provider. The dispatcher trims on every call. Pass `metadataPriority` (highest priority first) to choose what stays. The default keeps `agentId`, `orgId`, `stepId`, `skillId`, `recordId`, and `objectType`. A trimmed call adds a `METADATA_RECORD_TRIMMED` warning.
|
|
180
|
+
Every provider writes that record as `m.0`, `m.1`, and so on: one base64 string, 256 characters per piece, at most 16 pieces. The base64 is the UTF-8 JSON object of the string map, with keys sorted. `decodeProviderMetadata` reads those pieces from OpenRouter body `metadata`, OpenAI Responses `metadata`, Bedrock `requestMetadata`, or Cloudflare body `metadata` and `cf-aig-metadata`. The pieces are the same on every provider. The dispatcher trims on every call. Pass `metadataPriority` (highest priority first) to choose what stays. The default keeps `agentId`, `orgId`, `stepId`, `skillId`, `role`, `roleLayer`, `roleDigest`, `profileChoice`, `recordId`, and `objectType`. A trimmed call adds a `METADATA_RECORD_TRIMMED` warning.
|
|
181
181
|
|
|
182
182
|
Repeating vendor fields normalize to one key. Prefer `metadata.api_key_name` over `metadata.openrouter.api_key_name`. `normalizeMetadata` and `decodeProviderMetadata` promote known aliases and keep the vendor key. Grow `METADATA_NORMALIZATION_MAP` as more shared fields appear.
|
|
183
183
|
|
|
@@ -243,7 +243,7 @@ Text blocks in an MCP result are joined and returned to the model. `isError: tru
|
|
|
243
243
|
|
|
244
244
|
## Reasoning effort
|
|
245
245
|
|
|
246
|
-
`reasoningEffort` is `minimal | low | medium | high | max`.
|
|
246
|
+
`reasoningEffort` is `minimal | low | medium | high | max`. `extra-high` is rejected with `INVALID_EFFORT`. It is not stored as `max`.
|
|
247
247
|
|
|
248
248
|
| Request | Catalog |
|
|
249
249
|
| --- | --- |
|
|
@@ -296,7 +296,7 @@ The rest of the response is the runtime shape: `id`, `status` (`completed`, `fai
|
|
|
296
296
|
|
|
297
297
|
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.
|
|
298
298
|
|
|
299
|
-
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.
|
|
299
|
+
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.1.0 paths are empty, so a real directive deletes nothing.
|
|
300
300
|
|
|
301
301
|
## Streaming
|
|
302
302
|
|
|
@@ -446,6 +446,17 @@ await dispatcher.run({
|
|
|
446
446
|
|
|
447
447
|
Cloudflare supports the same `run`, `compile`, and `executeStreamingChat` entrypoints. `executeStreamingChat` rejects `/ai/run`. `reasoningEffort` uses the OpenAI or OpenRouter catalog, as described above. `cache` sets AI Gateway headers and, for GPT on Responses and Claude on Messages, the native breakpoint. Local function tools and MCP tools follow the OpenAI loop. OpenRouter server tools are rejected. `rawOpenRouterOverrides` is rejected except for a `metadata` object, which is folded into `metadata`. `responseFormat` is `text` on Responses, `response_format` on chat, and rejected on messages and run.
|
|
448
448
|
|
|
449
|
+
## Credentials
|
|
450
|
+
|
|
451
|
+
A provider is available when the dispatcher can resolve the credential it sends. `compile` and `run` throw when a required value is missing.
|
|
452
|
+
|
|
453
|
+
| Provider | Option | Environment |
|
|
454
|
+
| --- | --- | --- |
|
|
455
|
+
| OpenRouter | `openrouter.apiKey` | `OPENROUTER_API_KEY`, then `OPEN_ROUTER_KEY` |
|
|
456
|
+
| Bedrock | `bedrock.region` and optional `bedrock.credentials` | `AWS_REGION` or `AWS_DEFAULT_REGION`. When `credentials` is omitted, the AWS SDK default chain is used. An explicit credential is `accessKeyId`, `secretAccessKey`, and optional `sessionToken`. |
|
|
457
|
+
| OpenAI | `openai.apiKey` | `OPENAI_API_KEY` |
|
|
458
|
+
| Cloudflare | `cloudflare.apiToken` and `cloudflare.accountId` | `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` |
|
|
459
|
+
|
|
449
460
|
## Errors
|
|
450
461
|
|
|
451
462
|
Catalog failures throw `AiDispatcherError` with `code`, `message`, and the catalog `details`. Dispatcher codes include:
|
|
@@ -466,7 +477,7 @@ Catalog failures throw `AiDispatcherError` with `code`, `message`, and the catal
|
|
|
466
477
|
| `PROVIDER_AUTH_FAILED` | OpenAI or Cloudflare HTTP `401` or `403` |
|
|
467
478
|
| `PROVIDER_MODEL_NOT_FOUND` | OpenAI or Cloudflare HTTP `404` |
|
|
468
479
|
| `PROVIDER_REQUEST_FAILED` | Other OpenAI or Cloudflare HTTP failures |
|
|
469
|
-
| `PROVIDER_RATE_LIMITED` | Cloudflare HTTP `429` after retries |
|
|
480
|
+
| `PROVIDER_RATE_LIMITED` | OpenAI or Cloudflare HTTP `429` after retries |
|
|
470
481
|
| `PROVIDER_RETRYABLE` / `PROVIDER_GATEWAY_UNREACHABLE` | OpenAI or Cloudflare transport failures after retries, including timeout |
|
|
471
482
|
| `FUNCTION_TOOL_LIMIT` | Function-tool calls exceed `maxFunctionToolCalls` |
|
|
472
483
|
| `UNSUPPORTED_CONTENT` | Bedrock received an image URL or content part it cannot send |
|
|
@@ -484,11 +495,11 @@ Pass `logger` on the dispatcher. `run` and `executeStreamingChat` emit `ai-dispa
|
|
|
484
495
|
|
|
485
496
|
## Limitations
|
|
486
497
|
|
|
487
|
-
- Catalog 5.
|
|
498
|
+
- Catalog 5.1.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.
|
|
488
499
|
- Prompt caching is a dispatcher profile, not an `@x12i/ai-profiles` field. It writes one breakpoint after `system` or `instructions`. It does not place extra breakpoints on tools or on a prefix that changes in the middle of the call.
|
|
489
500
|
- Provenance notes about output-token headroom are not enforced, because the resolver does not return them as fields.
|
|
490
501
|
- OpenRouter server tools, `apiMode: "chat"`, and `rawOpenRouterOverrides` stay on OpenRouter. Bedrock, direct OpenAI, and Cloudflare reject the server-tool fields and `rawOpenRouterOverrides` keys other than `metadata`. Cloudflare accepts `apiMode: "chat"` as the chat-completions endpoint.
|
|
491
502
|
- Direct OpenAI and Cloudflare streaming do not run the function-tool loop. MCP tools are not attached to `executeStreamingChat` on any provider. Cloudflare `/ai/run` does not stream.
|
|
492
503
|
- OpenRouter advisor, subagent, and fusion cannot call MCP sessions registered on the dispatcher.
|
|
493
504
|
- Bedrock `run()` returns a failed response for `UNSUPPORTED_CONTENT`. `compile()` throws that error.
|
|
494
|
-
- Live provider calls are not part of the package test suite. Tests mock HTTP and the Bedrock client and use the real 5.
|
|
505
|
+
- Live provider calls are not part of the package test suite. Tests mock HTTP and the Bedrock client and use the real 5.1.0 catalog for contract cases.
|
package/dist/index.cjs
CHANGED
|
@@ -30,10 +30,12 @@ __export(index_exports, {
|
|
|
30
30
|
isImplementedAiProvider: () => isImplementedAiProvider,
|
|
31
31
|
normalizeMetadata: () => import_openrouter_runtime4.normalizeMetadata,
|
|
32
32
|
providerNotImplemented: () => providerNotImplemented,
|
|
33
|
+
registerHostIntents: () => import_ai_profiles2.registerHostIntents,
|
|
33
34
|
resolveProvider: () => resolveProvider,
|
|
34
35
|
stripReasoningHistoryFields: () => stripReasoningHistoryFields
|
|
35
36
|
});
|
|
36
37
|
module.exports = __toCommonJS(index_exports);
|
|
38
|
+
var import_ai_profiles2 = require("@x12i/ai-profiles");
|
|
37
39
|
var import_openrouter_runtime4 = require("@x12i/openrouter-runtime");
|
|
38
40
|
|
|
39
41
|
// src/create-dispatcher.ts
|
|
@@ -2352,6 +2354,7 @@ function isRecord5(value) {
|
|
|
2352
2354
|
// src/prepare-request.ts
|
|
2353
2355
|
function prepareDispatchRequest(params) {
|
|
2354
2356
|
const cloned = cloneRequest(params.request);
|
|
2357
|
+
applyRoleStamp(cloned);
|
|
2355
2358
|
applyDispatchIdentity(cloned);
|
|
2356
2359
|
absorbOverrideMetadata(cloned);
|
|
2357
2360
|
const provider = params.provider;
|
|
@@ -2659,6 +2662,23 @@ function defaultModelFor(provider, options) {
|
|
|
2659
2662
|
return options.openai?.defaultModel;
|
|
2660
2663
|
}
|
|
2661
2664
|
var DISPATCH_IDENTITY_FIELDS = ["orgId", "stepId", "skillId"];
|
|
2665
|
+
var ROLE_STAMP_FIELDS = ["role", "roleLayer", "roleDigest", "profileChoice"];
|
|
2666
|
+
function applyRoleStamp(request) {
|
|
2667
|
+
const stamp = request.role;
|
|
2668
|
+
delete request.role;
|
|
2669
|
+
if (!isRecord6(stamp)) return;
|
|
2670
|
+
const identity = {};
|
|
2671
|
+
for (const field of ROLE_STAMP_FIELDS) {
|
|
2672
|
+
const value = stamp[field];
|
|
2673
|
+
if (typeof value !== "string") continue;
|
|
2674
|
+
const trimmed = value.trim();
|
|
2675
|
+
if (trimmed) identity[field] = trimmed;
|
|
2676
|
+
}
|
|
2677
|
+
if (!Object.keys(identity).length) return;
|
|
2678
|
+
const rest = { ...request.metadata ?? {} };
|
|
2679
|
+
for (const key of Object.keys(identity)) delete rest[key];
|
|
2680
|
+
request.metadata = { ...identity, ...rest };
|
|
2681
|
+
}
|
|
2662
2682
|
function absorbOverrideMetadata(request) {
|
|
2663
2683
|
const overrides = request.rawOpenRouterOverrides;
|
|
2664
2684
|
if (!isRecord6(overrides) || !Object.prototype.hasOwnProperty.call(overrides, "metadata")) return;
|
|
@@ -3260,8 +3280,7 @@ async function readFailure2(response) {
|
|
|
3260
3280
|
function classifyStatus2(status) {
|
|
3261
3281
|
if (status === 401 || status === 403) return "PROVIDER_AUTH_FAILED";
|
|
3262
3282
|
if (status === 404) return "PROVIDER_MODEL_NOT_FOUND";
|
|
3263
|
-
if (status === 429) return "
|
|
3264
|
-
if (status === 408 || status >= 500) return "PROVIDER_RETRYABLE";
|
|
3283
|
+
if (status === 429 || status === 408 || status >= 500) return "PROVIDER_RETRYABLE";
|
|
3265
3284
|
return "PROVIDER_REQUEST_FAILED";
|
|
3266
3285
|
}
|
|
3267
3286
|
function isConfigError2(code) {
|
|
@@ -3269,6 +3288,8 @@ function isConfigError2(code) {
|
|
|
3269
3288
|
}
|
|
3270
3289
|
function asTerminalError2(error) {
|
|
3271
3290
|
if (error instanceof AiDispatcherError && error.code === "PROVIDER_RETRYABLE") {
|
|
3291
|
+
const status = isRecord7(error.details) ? error.details.status : void 0;
|
|
3292
|
+
if (status === 429) return new AiDispatcherError("PROVIDER_RATE_LIMITED", error.message, error.details);
|
|
3272
3293
|
return new AiDispatcherError("PROVIDER_GATEWAY_UNREACHABLE", error.message, error.details);
|
|
3273
3294
|
}
|
|
3274
3295
|
if (error instanceof AiDispatcherError) return error;
|
|
@@ -3528,7 +3549,18 @@ function resolveProvider(requestProvider, defaultProvider) {
|
|
|
3528
3549
|
|
|
3529
3550
|
// src/create-dispatcher.ts
|
|
3530
3551
|
var SECRET_HEADERS = /* @__PURE__ */ new Set(["authorization", "x-api-key", "api-key"]);
|
|
3531
|
-
var DEFAULT_METADATA_PRIORITY = [
|
|
3552
|
+
var DEFAULT_METADATA_PRIORITY = [
|
|
3553
|
+
"agentId",
|
|
3554
|
+
"orgId",
|
|
3555
|
+
"stepId",
|
|
3556
|
+
"skillId",
|
|
3557
|
+
"role",
|
|
3558
|
+
"roleLayer",
|
|
3559
|
+
"roleDigest",
|
|
3560
|
+
"profileChoice",
|
|
3561
|
+
"recordId",
|
|
3562
|
+
"objectType"
|
|
3563
|
+
];
|
|
3532
3564
|
function createAiDispatcher(options = {}) {
|
|
3533
3565
|
const packer = (0, import_provider_metadata.createMetadataPacker)({ priority: options.metadataPriority ?? DEFAULT_METADATA_PRIORITY });
|
|
3534
3566
|
const defaultProvider = resolveProvider(options.provider, "openrouter");
|
|
@@ -3694,6 +3726,7 @@ function stripReasoningHistoryFields(message, paths) {
|
|
|
3694
3726
|
isImplementedAiProvider,
|
|
3695
3727
|
normalizeMetadata,
|
|
3696
3728
|
providerNotImplemented,
|
|
3729
|
+
registerHostIntents,
|
|
3697
3730
|
resolveProvider,
|
|
3698
3731
|
stripReasoningHistoryFields
|
|
3699
3732
|
});
|