Guides
Attribution
Token counts alone tell you what you spent. Attribution tells you what it cost to serve any one customer, feature, or team, and that split doesn't exist anywhere in the provider's own data.
Why this matters
OpenAI's invoice, Anthropic's usage export, Bedrock's Cost Explorer line: none of them have a customer field. They know an API key spent $4.02 on gpt-4.1 today. They have no idea whether that was one customer running a heavy report or a thousand customers each sending one message. Without attribution captured at call time, per-customer margin is not a hard problem. It's an impossible one, because the information was never recorded anywhere.
Attribution has to happen at the call, not after
There is no way to reconstruct customer/feature/team after the fact from provider billing data. If a call goes out untagged, that spend is permanently unattributed: it still shows up (Token Meter never hides unattributed spend from a total), but it can't be split by customer margin.Context propagation
action() is backed by native async-local context (Python's contextvars, TypeScript's AsyncLocalStorage) so it propagates automatically through the cases that matter most:
- Async fan-out:
asyncio.gather/create_taskin Python,Promise.allin TypeScript, all inherit the enclosing context. - Nested scopes: a nested
action()inherits unset tags from its parent. - Streams handed off to be iterated later: the context is snapshotted at call time, not read lazily, so it survives the stream outliving the scope it was opened in.
What it can't see across
Real OS thread boundaries and serialize hops break automatic propagation: Python'sconcurrent.futures.ThreadPoolExecutor.submit does not auto-propagate context, and neither language's async-local storage crosses a queue/worker or message-broker hop. Those are the two cases the escape hatch below exists for.current() / attach()
For any hop context can't see across on its own (a thread pool, a queue publish/consume boundary), capture the active context with current() on the producing side, then restore it with attach() on the consuming side. Python's TraceContext is a plain dataclass of strings, so it serializes straight into a queue payload.
// producing side
const ctx = current(); // ActionContext | undefined
// consuming side (e.g. inside a worker picked up off a queue)
if (ctx) {
attach(ctx, () => {
return openai.chat.completions.create({ ... }); // picks up the restored context
});
}