@x12i/ai-dispatcher 2.0.0 → 2.2.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/README.md CHANGED
@@ -4,7 +4,7 @@ One request shape for OpenRouter, AWS Bedrock, the OpenAI Responses API, and Clo
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.0.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.2.0. 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/openrouter-runtime@^2.0.0`, and `@x12i/bedrock-runtime@^2.0.0`.
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`.
18
18
 
19
19
  ## One request
20
20
 
@@ -177,7 +177,9 @@ 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. A larger record throws `METADATA_RECORD_TOO_LARGE`.
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.
181
+
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.
181
183
 
182
184
  OpenRouter also puts `agentId` in `X-Title` and `X-OpenRouter-Title`, ahead of `appAttribution.appName` and `defaultHeaders`. That header is the activity-log App name. `appAttribution.siteUrl` is still `HTTP-Referer` when set. The App header is not a second metadata record.
183
185
 
package/dist/index.cjs CHANGED
@@ -21,10 +21,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AiDispatcherError: () => AiDispatcherError,
24
+ DEFAULT_METADATA_PRIORITY: () => DEFAULT_METADATA_PRIORITY,
24
25
  IMPLEMENTED_AI_PROVIDERS: () => IMPLEMENTED_AI_PROVIDERS,
26
+ METADATA_NORMALIZATION_MAP: () => import_openrouter_runtime4.METADATA_NORMALIZATION_MAP,
27
+ canonicalMetadataKey: () => import_openrouter_runtime4.canonicalMetadataKey,
25
28
  createAiDispatcher: () => createAiDispatcher,
26
29
  decodeProviderMetadata: () => import_openrouter_runtime4.decodeProviderMetadata,
27
30
  isImplementedAiProvider: () => isImplementedAiProvider,
31
+ normalizeMetadata: () => import_openrouter_runtime4.normalizeMetadata,
28
32
  providerNotImplemented: () => providerNotImplemented,
29
33
  resolveProvider: () => resolveProvider,
30
34
  stripReasoningHistoryFields: () => stripReasoningHistoryFields
@@ -32,6 +36,9 @@ __export(index_exports, {
32
36
  module.exports = __toCommonJS(index_exports);
33
37
  var import_openrouter_runtime4 = require("@x12i/openrouter-runtime");
34
38
 
39
+ // src/create-dispatcher.ts
40
+ var import_provider_metadata = require("@x12i/provider-metadata");
41
+
35
42
  // src/cache-usage.ts
36
43
  function readCacheTokens(source) {
37
44
  const cached = [];
@@ -1178,8 +1185,8 @@ function restoreCloudflareModel(model, prefix) {
1178
1185
  if (!prefix || model.includes("/")) return model;
1179
1186
  return `${prefix}${model}`;
1180
1187
  }
1181
- function createCloudflareProviderAdapter(options = {}) {
1182
- const resolved = resolveCloudflareOptions(options);
1188
+ function createCloudflareProviderAdapter(options = {}, packer) {
1189
+ const resolved = resolveCloudflareOptions(options, packer);
1183
1190
  return {
1184
1191
  compile(request, call) {
1185
1192
  return compileCloudflareRequest(request, resolved, call, false);
@@ -1232,12 +1239,13 @@ function compileCloudflareRequest(request, options, call, streaming) {
1232
1239
  if (endpoint !== "run") body = { ...body, stream: streaming };
1233
1240
  const gateway = mergeGateway(options.gateway, call?.cloudflare?.gateway);
1234
1241
  const fromBody = isRecord5(body.metadata) ? body.metadata : void 0;
1235
- const record2 = (0, import_openrouter_runtime.toProviderMetadata)(
1242
+ const packed = (0, import_openrouter_runtime.packProviderRecord)(
1236
1243
  request.metadata,
1237
1244
  request.agentId,
1238
- import_openrouter_runtime.OPENROUTER_METADATA_LIMITS,
1239
- overlayMetadata(gateway?.metadata, fromBody)
1245
+ overlayMetadata(gateway?.metadata, fromBody),
1246
+ options.metadataPacker
1240
1247
  );
1248
+ const record2 = packed.wire;
1241
1249
  body = applyProviderRecord(body, record2);
1242
1250
  if (endpoint === "run") body = wrapRun(body, run);
1243
1251
  return {
@@ -1245,7 +1253,7 @@ function compileCloudflareRequest(request, options, call, streaming) {
1245
1253
  url: endpointUrl(options, endpoint),
1246
1254
  headers: buildHeaders(options, call?.cloudflare, model, record2),
1247
1255
  body,
1248
- warnings: []
1256
+ warnings: packed.warning ? [packed.warning] : []
1249
1257
  };
1250
1258
  }
1251
1259
  function compileResponsesBody(request, model) {
@@ -2294,7 +2302,7 @@ function parseSsePart(part) {
2294
2302
  return void 0;
2295
2303
  }
2296
2304
  }
2297
- function resolveCloudflareOptions(options) {
2305
+ function resolveCloudflareOptions(options, packer) {
2298
2306
  const {
2299
2307
  apiToken,
2300
2308
  accountId,
@@ -2323,7 +2331,8 @@ function resolveCloudflareOptions(options) {
2323
2331
  maxAttempts: maxAttempts ?? 2,
2324
2332
  fetch: fetch ?? globalThis.fetch,
2325
2333
  ...tools ? { tools } : {},
2326
- ...logger ? { logger } : {}
2334
+ ...logger ? { logger } : {},
2335
+ ...packer ? { metadataPacker: packer } : {}
2327
2336
  };
2328
2337
  }
2329
2338
  function hasGateway(gateway) {
@@ -2838,8 +2847,8 @@ function isBedrockToolChoice(toolChoice) {
2838
2847
  var import_openrouter_runtime2 = require("@x12i/openrouter-runtime");
2839
2848
  var DEFAULT_BASE_URL2 = "https://api.openai.com/v1";
2840
2849
  var DEFAULT_TIMEOUT_MS2 = 12e4;
2841
- function createOpenAiProviderAdapter(options = {}) {
2842
- const resolved = resolveOpenAiOptions(options);
2850
+ function createOpenAiProviderAdapter(options = {}, packer) {
2851
+ const resolved = resolveOpenAiOptions(options, packer);
2843
2852
  return {
2844
2853
  compile(request, call) {
2845
2854
  return compileOpenAiRequest(request, resolved, call, false);
@@ -2892,12 +2901,13 @@ function compileOpenAiRequest(request, options, call, streaming) {
2892
2901
  if (call?.finalizeBody) body = call.finalizeBody(body);
2893
2902
  if (streaming) body = { ...body, stream: true };
2894
2903
  else body = { ...body, stream: false };
2895
- body = attachProviderMetadata(body, request);
2904
+ const packed = attachProviderMetadata(body, request, options.metadataPacker);
2905
+ body = packed.body;
2896
2906
  return {
2897
2907
  url: `${options.baseUrl}/responses`,
2898
2908
  headers: buildHeaders2(options),
2899
2909
  body,
2900
- warnings: []
2910
+ warnings: packed.warning ? [packed.warning] : []
2901
2911
  };
2902
2912
  }
2903
2913
  function assertSupported2(request) {
@@ -3397,14 +3407,14 @@ function messageFrom2(body) {
3397
3407
  function numberValue2(value) {
3398
3408
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
3399
3409
  }
3400
- function attachProviderMetadata(body, request) {
3410
+ function attachProviderMetadata(body, request, packer) {
3401
3411
  const fromBody = isRecord7(body.metadata) ? body.metadata : void 0;
3402
- const metadata = (0, import_openrouter_runtime2.toProviderMetadata)(request.metadata, request.agentId, import_openrouter_runtime2.OPENROUTER_METADATA_LIMITS, fromBody);
3403
- if (metadata) return { ...body, metadata };
3404
- if (body.metadata === void 0) return body;
3412
+ const packed = (0, import_openrouter_runtime2.packProviderRecord)(request.metadata, request.agentId, fromBody, packer);
3413
+ if (packed.wire) return { body: { ...body, metadata: packed.wire }, ...packed.warning ? { warning: packed.warning } : {} };
3414
+ if (body.metadata === void 0) return { body, ...packed.warning ? { warning: packed.warning } : {} };
3405
3415
  const rest = { ...body };
3406
3416
  delete rest.metadata;
3407
- return rest;
3417
+ return { body: rest, ...packed.warning ? { warning: packed.warning } : {} };
3408
3418
  }
3409
3419
  function clean2(body) {
3410
3420
  return Object.fromEntries(Object.entries(body).filter(([, value]) => value !== void 0));
@@ -3452,7 +3462,7 @@ function parseSsePart2(part) {
3452
3462
  return void 0;
3453
3463
  }
3454
3464
  }
3455
- function resolveOpenAiOptions(options) {
3465
+ function resolveOpenAiOptions(options, packer) {
3456
3466
  const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
3457
3467
  return {
3458
3468
  apiKey: options.apiKey ?? readEnv3("OPENAI_API_KEY") ?? "",
@@ -3464,7 +3474,8 @@ function resolveOpenAiOptions(options) {
3464
3474
  maxAttempts: options.maxAttempts ?? 3,
3465
3475
  fetch: options.fetch ?? globalThis.fetch,
3466
3476
  ...options.tools ? { tools: options.tools } : {},
3467
- ...options.logger ? { logger: options.logger } : {}
3477
+ ...options.logger ? { logger: options.logger } : {},
3478
+ ...packer ? { metadataPacker: packer } : {}
3468
3479
  };
3469
3480
  }
3470
3481
  function readEnv3(name) {
@@ -3517,7 +3528,9 @@ function resolveProvider(requestProvider, defaultProvider) {
3517
3528
 
3518
3529
  // src/create-dispatcher.ts
3519
3530
  var SECRET_HEADERS = /* @__PURE__ */ new Set(["authorization", "x-api-key", "api-key"]);
3531
+ var DEFAULT_METADATA_PRIORITY = ["agentId", "orgId", "stepId", "skillId", "recordId", "objectType"];
3520
3532
  function createAiDispatcher(options = {}) {
3533
+ const packer = (0, import_provider_metadata.createMetadataPacker)({ priority: options.metadataPriority ?? DEFAULT_METADATA_PRIORITY });
3521
3534
  const defaultProvider = resolveProvider(options.provider, "openrouter");
3522
3535
  const catalog = createMcpCatalog(options.mcp);
3523
3536
  let refreshChain = Promise.resolve();
@@ -3526,6 +3539,7 @@ function createAiDispatcher(options = {}) {
3526
3539
  if (provider === "openrouter") {
3527
3540
  const openrouterOptions = {
3528
3541
  ...options.openrouter,
3542
+ metadataPacker: packer,
3529
3543
  ...options.logger && !options.openrouter?.logger ? { logger: options.logger } : {}
3530
3544
  };
3531
3545
  return createOpenRouterProviderAdapter(openrouterOptions);
@@ -3533,6 +3547,7 @@ function createAiDispatcher(options = {}) {
3533
3547
  if (provider === "bedrock") {
3534
3548
  const bedrockOptions = {
3535
3549
  ...options.bedrock,
3550
+ metadataPacker: packer,
3536
3551
  ...options.logger && !options.bedrock?.logger ? { logger: options.logger } : {}
3537
3552
  };
3538
3553
  return createBedrockProviderAdapter(bedrockOptions);
@@ -3542,14 +3557,14 @@ function createAiDispatcher(options = {}) {
3542
3557
  ...options.openai,
3543
3558
  ...options.logger && !options.openai?.logger ? { logger: options.logger } : {}
3544
3559
  };
3545
- return createOpenAiProviderAdapter(openaiOptions);
3560
+ return createOpenAiProviderAdapter(openaiOptions, packer);
3546
3561
  }
3547
3562
  if (provider === "cloudflare") {
3548
3563
  const cloudflareOptions = {
3549
3564
  ...options.cloudflare,
3550
3565
  ...options.logger && !options.cloudflare?.logger ? { logger: options.logger } : {}
3551
3566
  };
3552
- return createCloudflareProviderAdapter(cloudflareOptions);
3567
+ return createCloudflareProviderAdapter(cloudflareOptions, packer);
3553
3568
  }
3554
3569
  throw new AiDispatcherError(
3555
3570
  "PROVIDER_ADAPTER_MISSING",
@@ -3670,10 +3685,14 @@ function stripReasoningHistoryFields(message, paths) {
3670
3685
  // Annotate the CommonJS export names for ESM import in node:
3671
3686
  0 && (module.exports = {
3672
3687
  AiDispatcherError,
3688
+ DEFAULT_METADATA_PRIORITY,
3673
3689
  IMPLEMENTED_AI_PROVIDERS,
3690
+ METADATA_NORMALIZATION_MAP,
3691
+ canonicalMetadataKey,
3674
3692
  createAiDispatcher,
3675
3693
  decodeProviderMetadata,
3676
3694
  isImplementedAiProvider,
3695
+ normalizeMetadata,
3677
3696
  providerNotImplemented,
3678
3697
  resolveProvider,
3679
3698
  stripReasoningHistoryFields