Attributing model spend to features and teams
Splitting one provider invoice across an org chart looks like an accounting problem. It is really a question about where you draw the boundary, and the boundary you pick changes behaviour.
A platform engineer I know inherited a single API key. One key, one invoice, four hundred engineers, eleven product areas, and a finance team that had just decided AI spend needed to be allocated to cost centres like everything else. His first instinct was to issue four hundred keys. His second instinct, about a day later, was that four hundred keys would tell him who spent the money but nothing about what the money bought, which was the actual question.
He was right, and the distinction he stumbled into is the whole subject. Attribution by identity is easy and nearly useless. Attribution by outcome is hard and is the only version anyone actually wants.
Three boundaries, and they are not equivalent
Every attribution scheme you can build draws one of three boundaries. It is worth naming them because teams routinely build one and then ask it questions only another can answer.
By identity. Who made the call. Implemented with per-user keys, per-service credentials, or an OAuth-style identity on every request. This is the cheapest to build and the easiest to defend to an auditor. It tells you that Priya’s team spent more than Marco’s team. It does not tell you why, and it invites the single worst use of this data, which is ranking individuals.
By workload. What kind of activity the call was. Code generation, test authoring, semantic verification, summarisation, triage, search. Implemented with a purpose label set at the call site. This is the boundary that answers the most useful strategic questions, because it tells you whether your growth is more building or more checking, and those two have completely different implications for what you do next.
By artefact. Which piece of work the call served. A ticket key, an epic, a repository, a service, a feature flag. Implemented by threading a work identifier through whatever calls the model. This is the boundary finance thinks it is asking for when it says “by feature”, and it is the hardest of the three, because the work identifier has to be present at the moment of the call and there are always paths where it is not.
By identity
Cheap to build, easy to audit. Answers "who". Becomes a scoreboard within a fortnight if you let it.
By workload
One label at the call site. Answers "what kind of activity". The best ratio of effort to insight.
By artefact
Ticket, epic, repo, service. Answers "what did it buy". Hardest to thread, most valuable once threaded.
By org unit
Derived, never recorded. Comes from joining artefact to the tracker. Re-derivable when reorgs happen.
Notice the fourth box, and notice that it is derived rather than recorded. This is the single most important design decision in the whole exercise, so I want to dwell on it.
Never record the org unit
The temptation is enormous. You are being asked for spend by team, so you add a team field to your usage records and stamp it on every call. It works beautifully for about seven months, until the reorg.
After the reorg, half your historical data is attributed to teams that no longer exist, a service has moved from one group to another, and the year-on-year comparison finance wanted is now impossible to produce. You cannot fix it retroactively, because you recorded the conclusion instead of the evidence.
Record the evidence. Record the ticket key, the repository, the service identifier, the pull request. Those are stable facts about the world. Then derive the org unit at query time by joining to whatever your current source of truth for ownership is, usually the tracker’s project structure or a service catalogue. When the reorg happens, you change the join, and your entire history re-attributes correctly.
This applies to features too. A feature is a grouping of tickets, and groupings change. Store the ticket, derive the feature.
The same logic argues for storing token counts rather than costs. Prices change. If you store dollars, a price change permanently fractures your trend line and there is no way to ask what last year would have cost at this year’s rates. If you store counts and a model identifier, you can re-derive spend against any rate card, including a hypothetical one, which turns out to be exactly what you need when someone asks whether switching models would have saved money.
Threading the work identifier
Everything above depends on a work identifier being present when the call happens, so let me be concrete about where it comes from, because this is the part that gets hand-waved.
If the agent is running in CI, you have it. The branch name, the pull request number and the commit are all right there in the environment, and most teams already encode the ticket key in the branch name. This is the easy case and it should be a hard requirement: no work identifier, no CI job.
If the agent is running in a developer’s editor, you have it about seventy percent of the time. The current branch usually carries the ticket key. Sometimes the developer is on a scratch branch, or on main, or exploring. Take what you can get from the branch, and treat the rest as unattributed rather than guessing.
If the call is a background job, an automated review, a nightly summarisation, a triage pass, then the identifier depends on the job knowing what it is working on, which it usually does. A job that summarises a pull request knows the pull request. A job that scores a backlog knows the tickets. Push the identifier into the job’s inputs rather than trying to recover it from logs afterwards.
If the call is genuinely ambient, someone asking a question in a chat window, there is no work identifier and there never will be. Do not invent one. Bucket it as ambient and track the share.
The shared cost problem
Once per-artefact attribution works, the first hard question arrives: what about the calls that serve everybody.
A shared embedding index that all teams query. A nightly job that keeps a knowledge base current. A caching layer whose whole purpose is that the second team to ask a question pays nothing. These are real costs that no single feature caused, and how you allocate them determines whether people use them.
There are three defensible answers and one bad one.
The bad one is to allocate shared cost proportionally to each team’s direct spend, because it means the teams doing the most work subsidise the teams doing the least, and it makes the shared service look expensive to exactly the people you want using it.
The three defensible answers are: leave it central and do not allocate at all, treating platform cost as platform cost; allocate by usage of the shared service specifically, if you can meter it; or allocate by a flat headcount split, which is crude but predictable and does not distort behaviour. I have a mild preference for the first, because the alternative usually costs more in argument than it recovers in accuracy.
Caching deserves a special note here, because it creates a genuinely awkward accounting artefact. If a result is cached and the second caller pays nothing, then the first caller is subsidising everyone after them, and if you attribute honestly, one arbitrary team eats the whole cost of a shared computation. The fix is to attribute cache misses to the shared service rather than to the unlucky first caller, and to report cache hit rate as a separate efficiency metric rather than folding it into anyone’s bill.
Showback before chargeback
The last structural decision is what you do with the numbers, and this is where more attribution programmes fail than on any technical ground.
Showback means every team can see what they spend. Chargeback means it hits their budget. The gap between these two is enormous behaviourally, and almost every organisation should live in showback for at least two quarters before considering the other.
Here is why. Attribution data is imperfect in ways I will detail in a moment, and imperfect data is fine for insight and terrible for billing. The first time a team is charged for spend they believe is not theirs, they stop trusting the system, and from that point on every conversation about cost is a conversation about the accuracy of the allocation rather than about the cost. You will spend more engineering effort defending the numbers than you saved.
Showback gets you most of the behavioural benefit anyway. Teams that can see their own spend, next to a peer median, adjust without being charged. The visibility is the mechanism. The invoice is just an enforcement layer bolted onto it, and you should only bolt it on when the underlying data has earned enough trust to survive being contested.
Chargeback too early
- Every dispute becomes a data-accuracy fight
- Teams route around the metered path
- Unattributed spend becomes political
- Shared services look expensive and get avoided
Showback first
- Teams see themselves against a peer median
- Errors get reported instead of hidden
- Unattributed share becomes a shared engineering problem
- The data earns the right to be billed on
Where this breaks down
I have made this sound more tractable than it is, so here are the parts that stay broken.
Attribution will never be complete. There is always a slice of spend that belongs to no artefact: exploration, ambient questions, prototypes on scratch branches, an engineer trying three approaches and discarding two. In my experience that slice is not small, and the instinct to force it into buckets produces numbers that are precisely wrong. Report unattributed as its own line. If leadership finds that unsatisfying, the honest answer is that the alternative is a fabricated allocation, and a fabricated allocation is worse than an admitted gap.
Attribution also cannot distinguish value from cost, and people constantly read it as if it can. A team with high spend per ticket might be working on the hardest part of the system, or might be careless. The data does not know. The moment cost attribution is used in a performance conversation without that caveat, you have created an incentive to under-use the tools on exactly the hard problems where they help most.
There is a privacy edge here too. Per-identity attribution at fine granularity is, functionally, a log of what each engineer worked on and how long they struggled with it. That may be perfectly acceptable in your context, and in some jurisdictions and under some works agreements it is not. Decide deliberately, aggregate to the team where you can, and say out loud what is being collected. Discovering this later, via a complaint, is a bad way to discover it.
And the whole model assumes a stable enough rate structure to make trends meaningful. If your provider mix is changing every month, if you are moving between hosted and self-hosted inference, if you are mid-migration between model families, then cost trends are dominated by the migration rather than by behaviour. During those periods, look at token volume by workload instead of cost. Volume is a cleaner behavioural signal, and it does not move when a price does.
The takeaway
The invoice cannot be split after the fact. Attribution is a design decision you make at the call site, and the specific decision that matters most is to record facts rather than conclusions: ticket keys, repositories, purpose labels and raw token counts, never team names and never dollars.
Derive the org chart at query time, keep shared costs central rather than smearing them across teams, live in showback until the data is trusted, and report the unattributed slice honestly instead of pretending it does not exist.
Do all of that and you will have a number you can defend. Which is when the next question arrives, and it is the one everybody actually wants answered: now that we can see it, how do we bring it down without making the output worse. That is the subject of the next piece, and the short version is that almost every crude cost control trades quality for savings in a way that shows up two weeks later on a different line item.