@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 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@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.
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.2.0. It runs on Node 20 or newer and publishes ESM and CommonJS from the same entry.
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@5.0.0`, `@x12i/provider-metadata@^1.2.0`, `@x12i/openrouter-runtime@^2.2.0`, and `@x12i/bedrock-runtime@^2.2.0`.
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.0.0`:
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.0.0 paths are empty, so a real directive deletes nothing.
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.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.
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.0.0 catalog for contract cases.
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 "PROVIDER_RATE_LIMITED";
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 = ["agentId", "orgId", "stepId", "skillId", "recordId", "objectType"];
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
  });