@x12i/provider-metadata
v1.2.1
Published
Pack, open, and normalize one provider metadata record. The same wire object for every provider.
Readme
@x12i/provider-metadata
Pack and open one metadata record. The keys are whatever the caller stored.
packMetadata writes the same object for every provider. openMetadata reads that object back. A reader passes the log object and gets every stored pair, for any provider and any set of keys.
Repeating concepts are normalized to one canonical key. Vendor prefixes may stay (openrouter.api_key_name), and the shared key is always available (api_key_name). Grow METADATA_NORMALIZATION_MAP as more aliases appear.
The package runs on Node 20 or newer and publishes ESM and CommonJS from the same entry.
Install
npm install @x12i/provider-metadataPack and open
import { openMetadata, packMetadata } from "@x12i/provider-metadata";
const wire = packMetadata({
objectType: "finding",
stepId: "nx-p1-context-finding",
agentId: "neo-networks-v2",
skillId: "skill-9",
recordId: "rec-1"
});
const record = openMetadata(wire);record is the full string map. Another caller map, with different keys, packs and opens the same way.
Normalization
Use normalizeMetadata on provider logs, OTLP attribute bags, or nested vendor objects. Known aliases are promoted to canonical keys. The vendor key remains when it was present.
import { METADATA_NORMALIZATION_MAP, normalizeMetadata } from "@x12i/provider-metadata";
const record = normalizeMetadata({
"trace.metadata.openrouter.api_key_name": "prod-key",
"trace.metadata.tenant": "acme"
});
// { api_key_name: "prod-key", "openrouter.api_key_name": "prod-key", tenant: "acme" }
record.api_key_name;
METADATA_NORMALIZATION_MAP.api_key_name; // ["openrouter.api_key_name", ...]packMetadata and openMetadata apply the same map. Nested objects under known provider namespaces (for example openrouter) are flattened onto the wire. Other nested objects stay off the wire.
Current canonical keys include api_key_name, provider_slug, provider_name, entity_id, user_id, finish_reason, input_unit_price, output_unit_price, source, and session_id.
Automatic trim
Writers create one packer at initialization. pack keeps the record inside 4096 base64 characters. It drops the lowest-priority pair, then the next, and reports every key it removed.
priority is highest priority first. Listed keys are kept ahead of keys that are not in the list. An earlier list entry is kept ahead of a later one. Among keys with the same priority, the longest value is dropped first.
import { createMetadataPacker, openMetadata } from "@x12i/provider-metadata";
const packer = createMetadataPacker({
priority: ["agentId", "orgId", "stepId", "skillId", "recordId", "objectType"]
});
const packed = packer.pack({
objectType: "finding",
recordId: "rec-1",
note: veryLongText
});
if (packed.dropped.length) {
// packed.dropped lists the keys that did not fit, least important first
}
const record = openMetadata(packed.wire);@x12i/ai-dispatcher does this on every call. Pass the same list once as metadataPriority. A trimmed call adds a METADATA_RECORD_TRIMMED warning. metadataTrimWarning(packed.dropped) builds that warning from a direct pack result.
packMetadata does not drop keys. A record that does not fit throws METADATA_RECORD_TOO_LARGE.
packMetadata(...layers) accepts Record<string, unknown> | undefined. Later layers win on the same key. String, number, and boolean values are stored. A number becomes its decimal string, and a boolean becomes "true" or "false". Nested objects under known provider namespaces are flattened and normalized. Other objects and arrays are omitted. When a later value is an object or array for a non-provider key, the earlier scalar for that key stays. An empty result is undefined.
Wire object
The stored value is UTF-8 JSON of the string map, with keys sorted, then base64. That base64 string is split into m.0, m.1, and so on.
| Constant | Value |
| --- | --- |
| PROVIDER_METADATA_CHUNK_SIZE | 256 characters |
| PROVIDER_METADATA_MAX_CHUNKS | 16 |
The limit is 4096 base64 characters. createMetadataPacker drops pairs until the record fits. packMetadata throws METADATA_RECORD_TOO_LARGE instead.
openMetadata joins m.0, m.1, … in order, decodes the base64, and returns every string in that JSON object. Pass the wire object, or the JSON string of that object (Cloudflare cf-aig-metadata). A missing record, invalid JSON, or a value that is not an object returns {}.
These provider slots hold that same object:
| Provider | Log field |
| --- | --- |
| OpenRouter | Body metadata |
| OpenAI | Responses body metadata |
| Bedrock | requestMetadata |
| Cloudflare | Body metadata and the cf-aig-metadata header (JSON string) |
@x12i/ai-dispatcher creates one packer for the caller. The caller sends one map on the request. The dispatcher writes this object onto the provider that runs the call.
