OASA

Open Agent Spend Attribution

Attributing agent spend

For: someone connecting a provider bill, a gateway ledger, or OpenTelemetry spans to OASA 0.1.3, and the finance and platform teams who configure each provider. Rules: Attribution in the specification. What each platform actually puts on a bill: Attribution identity by platform, documentation read 2026-10-22.

Sections 1–5 are the implementer's procedure. Finance and platform teams is the per-platform part. Do the work in this order. Stop at the first step that fills the fields you need.

1. Name the four values

Field Write this Leave it empty when
agent_id The stable id of the software agent. The same string for the life of that agent. The bill only names a person, a seat, or a shared profile.
agent_owner The team or principal who answers for the agent. You only know the end user of one request.
cost_center The finance object that should carry the cost. You do not know it yet. Do not invent a default.
project_id The organization's project or use case. The provider's project id is a different string. Keep the provider id as the native identity and map it.

If a value will also be written as a tag or label, write it in tag form before it goes anywhere else: lowercase, starting with a letter, only a-z, 0-9, _, and -, at most 63 characters.

customer_id, task, session, and trace stay on the record. They are not these four fields.

2. Put a durable key on the thing the bill copies

Tag or label the resource the provider copies onto the cost row. Use these keys:

Key Becomes
oasa_cost_center cost_center
oasa_agent agent_id
oasa_owner agent_owner
oasa_project project_id
environment environment (prod, staging, or dev)

A convention key already on the billing row is declared. The provenance identity_type is the tag mechanism (aws.bedrock.inference_profile_tag, azure.tag, gcp.label). The provenance identity_value is the key, oasa_cost_center, not the tag's value. The tag's value is the common field. Copy the rest of the tag map to tags.

An implementation may accept an alias for a key a customer already writes, such as CostCenter for oasa_cost_center. An accepted alias is still declared, and identity_value is the key that was on the row.

Where the key goes:

These never go in a tag or label, as a key or as a value: trace_id, span_id, session_id, task_id, request id, message id, end-user id, email, timestamp. They belong on the usage record or in the provider's request-metadata channel. Request metadata does not reach the provider invoice.

A profile that serves more than one agent does not become agent_id. Put oasa_agent on it only when that value is one agent. Otherwise map the profile id, or leave agent_id on the usage record.

3. Bind a native id with a mapping record

Use a mapping record when the bill names something else: a workspace, an API key, a seat, an IAM principal, a gateway virtual key, or a resource id that is not itself the agent.

The record says: this native identifier means these fields, from valid_from (inclusive) until valid_to (exclusive; omit valid_to while it is still in force). occurred_at on the mapping is when you recorded it, which can be later than valid_from. A charge selects the mapping whose window contains the charge's occurred_at. Changing the mapping on Tuesday does not rewrite Monday.

mapped means a mapping record supplied the field. A convention key on the bill is not mapped.

Straight copies stay declared, with the platform identity type and the id as identity_value:

A null Anthropic workspace id, an all-zero Copilot user id, and a Scale Tier bundle that has no project are no_identity. Do not invent a mapping key for them.

A seat or a person (Microsoft 365 Copilot user, OpenAI organization user, Bedrock IAM principal used as a person) can fill agent_owner or cost_center through a mapping record. It does not fill agent_id. With no mapping, the reason is seat_without_owner.

A gateway virtual key, team, or consumer id fills a field on the gateway's own usage record through a mapping record. That ledger reaches the provider invoice only when the upstream call also sets a provider tag, label, project, or key. Otherwise the provider charge, if it has no join key, is not_on_bill.

Two mapping records for the same identity type and value with overlapping windows are a bad set. Apply neither. If that leaves the record with no common field, the reason is mapping_conflict.

4. Write provenance on each field you set

attribution has one entry per common field that is set. Each entry has method, source_system, identity_type, and identity_value. source_system here is who minted the identity. The envelope source_system is who emitted the OASA record. They are often different.

Resolve each field on its own, highest source first:

  1. declared
  2. manual
  3. mapped
  4. inherited
  5. rule
  6. unattributed, only when none of the four fields was set

A source that does not have a field leaves it for the next source. A Bedrock charge tagged oasa_cost_center=cc-4100, with no agent tag, declares cost_center. A mapping record on the profile ARN can still set agent_id as mapped, because declared left that field empty. The golden record is examples/charge-declared-tag-mapped-agent.json.

A usage record and the charge that bills it stay two records. Join them on trace_id when both have one. Inheritance may fill a field the charge left empty. It does not replace a field the charge declared.

A manual assignment overrides mapped, inherited, and rule. It does not edit a declared value. To correct a declared value, publish an allocation record, or a charge with tags.adjustment set to true. Leave the original record as it was.

Set attribution_unattributed_reason only when none of the four fields is set. Use tag_missing when the billing channel is a tag or label and no convention key or accepted alias is present. Use not_on_bill when the only identity is on a span, a gateway log, or request metadata and the charge has no join key. Use identity_not_mapped when an id is present and no mapping covers it. Do not fill a default cost center.

5. On a span, use the oasa attributes

Attribute Field Method
oasa.agent.id agent_id declared
oasa.cost_center cost_center declared
oasa.agent.owner agent_owner declared
oasa.project.id project_id declared

oasa.agent.id is the application's stable agent id. gen_ai.agent.id is a hosted agent resource id (an assistant id, a Bedrock agent ARN, a reasoning-engine name). Copy gen_ai.agent.id to external_identity_ref with identity_scheme other. If gen_ai.agent.id is absent, gen_ai.main_agent.id may fill external_identity_ref. Do not copy either into agent_id or parent_agent_id.

gen_ai.agent.name still maps to agent_name. service.name may fill agent_name when no agent name is present. It does not fill agent_id.

Output from oasa-otel-exporter 0.1.0, which wrote gen_ai.agent.id into agent_id, remains valid 0.1.2. It is not the 0.1.3 mapping. New instrumentation should set oasa.agent.id.

Checklist

Finance and platform teams

One section per platform. Each says what to configure, what the bill can attribute, what it cannot, and the mistakes that leave a charge without a common field. A feature the research note marks as preview is marked Preview here. Every platform claim cites that note.

The four fields and the mapping record are in sections 1–3 above. Structure the account before you write a mapping.

Anthropic

Configure. Structure the account: one workspace or project or key per agent or cost object. On Anthropic, do that with a workspace, then map. The Cost API groups dollars by workspace_id. The default allowance is 100 workspaces per organization, and archived workspaces do not count. (§2.1)

There is no cost-allocation tag. After the workspace exists, bind wrkspc_… with a mapping record, or copy it into project_id when that id is the organization's project.

What it can attribute.

What it cannot attribute.

Common mistakes.

OpenAI

Configure. Structure the account: one workspace or project or key per agent or cost object. On OpenAI, do that with a project or an API key, then map. Costs group by project_id and by api_key_id. API keys belong to a project. (§2.2)

There is no cost-allocation tag. After the project or key exists, bind it with a mapping record, or copy the project id into project_id when that id is the organization's project.

What it can attribute.

What it cannot attribute.

Common mistakes.

Amazon Bedrock

Configure. Tag the resource the bill copies.

Use the keys in step 2. Put oasa_agent on a profile only when that profile is one agent.

What it can attribute.

What it cannot attribute.

Common mistakes.

Azure

Configure. Put user-defined tags on the Foundry resource and on the model deployment. The Foundry cost page says to add deployment tags and user-defined tags to segment model-deployment cost. (§2.4)

Foundry also applies a tag key project to the project's underlying usage. That tag is Preview. It is supported for models sold by Azure, including Azure OpenAI. It is not supported for Azure Marketplace models. project is not oasa_project. You may configure project as an alias of oasa_project. Do not also write a user tag named project on that usage. (§2.4)

Tag inheritance copies billing, resource-group, and subscription tags onto child usage records for an Enterprise Agreement, a Microsoft Customer Agreement, or a Microsoft Partner Agreement with an Azure plan. (§2.4)

A resource, resource group, or subscription holds 50 name-value pairs. A tag name is at most 512 characters, a value at most 256. A name cannot contain < > % & \ ? /. (§2.4)

Wait 24 hours for a new tag to arrive on usage. The resource has to be incurring charges. (§2.4)

What it can attribute.

What it cannot attribute.

Common mistakes.

Vertex AI and Gemini

Configure. On a PayGo call to a Google model, send the convention keys in the labels object on generateContent or streamGenerateContent. For the partner models the labels page lists, Anthropic and Mistral, send X-Vertex-AI-Labels (base64-encoded JSON) on rawPredict and streamRawPredict. (§2.5)

Google's 1,000-value limit counts distinct values for a label key over the billing account's lifetime, including retired agents. Past that count, the key is dropped without notice. A Google model call holds 64 labels. A listed partner-model call holds 32. Key length is 1–63, value length is 0–63, lowercase. Do not put a person's name or title in a label. (§2.5)

If retired and live agents together would pass 1,000 values for oasa_agent, leave that key off the label. When the Cloud project is one agent, bind agent_id with a mapping record from the project id. Keep the labels to oasa_cost_center, oasa_owner, oasa_project, and environment.

The Cloud project (project.id, project.number) is the billing boundary inside a Cloud Billing account. (§2.5)

What it can attribute.

What it cannot attribute.

Common mistakes.

Copilot Studio and Microsoft 365

Configure. These products do not tag a cloud resource per request. Do not invent a tag channel. (§2.6)

A Microsoft 365 Copilot license is assigned to a user. Map that user to agent_owner or cost_center. The seat does not fill agent_id. (§2.6, §3.4)

In the credits report, an agent is an element of an app. Agent ID is the app identifier Microsoft generates, and the agent name comes from the app manifest. Copy that app id into agent_id when it is the organization's agent id. Each agent belongs to one environment. Prepaid credits are purchased at the tenant, pooled, then allocated to the environment. (§2.6)

Pay-as-you-go links an environment to an Azure subscription with a billing policy. (§2.6)

A session with no signed-in user uses the user id 00000000-0000-0000-0000-000000000000. That is no_identity. Do not invent a mapping key for it. (§2.6)

What it can attribute.

What it cannot attribute.

Common mistakes.

Gateways

LiteLLM, Kong, and Portkey each keep a ledger of their own. That identity reaches the provider bill only when the credential, tag, or label the gateway sends is what the provider records. A virtual key that fans into one provider key does not, by itself, split that provider's invoice. (§2.7, §3.5)

Map the gateway key, team, or consumer onto the gateway's own usage record. The provider charge, if it has no join key, is not_on_bill.

LiteLLM

Configure. Spend tracking requires virtual keys and a database. Optional headers on the request: x-litellm-tags (comma-separated), x-litellm-spend-logs-metadata (a JSON object; the documented example uses user_id, project_id, and request_type), x-litellm-customer-id or x-litellm-end-user-id, and x-litellm-trace-id, then x-litellm-session-id, then any x-*-session-id. (§2.7)

What it can attribute. One row per priced request in LiteLLM_SpendLogs: the key hash, the internal user who owns the key, team_id, request_tags, end_user, the model group, token counts, and a calculated spend. The response header x-litellm-response-cost carries that cost. (§2.7)

What it cannot attribute. That row on the provider invoice, unless the deployment underneath also tags or keys the call. (§2.7)

Common mistakes.

Kong

Configure. Kong's identity object is a Consumer. Key Authentication binds a credential to that Consumer. The client presents the credential in a header, a query parameter, or the body. AI Proxy authenticates to the upstream provider on behalf of the Consumer and records usage into the selected Kong log plugin. (§2.7)

What it can attribute. A gateway control figure. AI Proxy can calculate cost into the log plugin. AI Rate Limiting Advanced turns provider token usage into a cost and limits that cost per policy. Policies can match consumer, consumer group, header, path, model, and provider. From Gateway 3.15, a consumer counter can use consumer.id (the default), consumer.username, or consumer.custom_id. (§2.7)

What it cannot attribute. That cost on the upstream invoice. No Kong page read for this note writes it there. A durable per-consumer cost table that reconciles to an upstream invoice is not specified. The cost of a request is applied on the next request, so the request that crosses the limit is not the one that is rejected. (§2.7, open question 10)

Common mistakes.

Portkey

Configure. Send metadata as a string map in the SDK or in the x-portkey-metadata header. Any number of keys may be sent. Every value is a string of at most 128 characters. _user drives user-level analytics. If the OpenAI user field is present and _user is absent, Portkey copies user into _user. On an Enterprise self-hosted gateway at 2.5.0 or later, HEADERS_TO_METADATA copies named request headers into metadata. Enterprise metadata can sit on the request, the API key, and the workspace. (§2.7)

Virtual keys have been migrated to Model Catalog. One Portkey API key can call many providers, and the provider credential stays in Portkey. Catalog integrations can be shared to all workspaces or to selected workspaces. (§2.7)

What it can attribute. Analytics inside Portkey, grouped by any metadata key, and filterable by virtual key. Logs can be filtered by the same keys. (§2.7)

What it cannot attribute. That metadata on the provider invoice. The page describes observability and cost attribution inside Portkey. Budgets (USD or tokens) reject later requests. They apply only to requests made after the limit is set. They do not split the provider invoice. The minimum cost limit documented is $1, and the minimum token limit is 100. Workspace budgets are Enterprise, and selected Pro customers. (§2.7)

Common mistakes.

OpenTelemetry

Configure. On the span, set oasa.agent.id, oasa.cost_center, oasa.agent.owner, and oasa.project.id, as in step 5. Every attribute in the GenAI registry table is stability Development. (§2.8) The reference exporter fills cost_center, project_id, and customer_id from operator-configured span or resource attributes, environment from deployment.environment.name, and tags from a configured attribute list. A missing attribute is omitted. That exporter writes usage records only. (§1.4)

gen_ai.provider.name uses gcp.vertex_ai for aiplatform.googleapis.com and gcp.gemini for generativelanguage.googleapis.com. (§2.8)

What it can attribute.

What it cannot attribute.

Common mistakes.