How to Track LLM Costs Per Product Feature (Not Just Per API Key)
A single OpenAI API key can fund five different product features with no visibility into which one is driving costs. Here is how to add feature-level attribution without changing your LLM provider.
by Orviupdated 2 min read#llm-cost-attribution #cost-tracking #openai
A single OpenAI API key can back a chat feature, a summarization feature, a search re-ranker, and an internal admin tool simultaneously. The provider's usage dashboard shows one number: total spend on that key. When the bill jumps 40% in a week, that dashboard cannot tell you whether it was the chat feature getting more traffic or the summarizer processing larger documents — you find out by grepping application logs, if you logged the right thing in the first place.
Why per-API-key visibility isn't enough
Most teams start with one API key per environment (or per provider), which is the right call for secret management but the wrong granularity for cost attribution. The provider's billing page aggregates at the key level because that's the unit it authenticates — it has no concept of your product's feature boundaries. Splitting into one key per feature technically works but scales badly: key rotation, rate limit pooling, and secret storage all get harder with every key you add, and you still don't get spend broken down by user within a feature.
The fix is attribution at the application layer: tag every LLM call with metadata about which feature triggered it, log that alongside the actual cost, and aggregate afterward. This works with a single API key because the attribution lives in your own data, not the provider's.
Tagging calls with feature metadata
Use a fixed vocabulary for feature. Free-text feature names drift ("summary", "summarize", "thread-summary") and fragment your aggregates. A TypeScript union or an enum forces the names to stay consistent:
type Feature = "chat" | "summarization" | "search_rerank" | "admin_tools";
async function trackedCall(feature: Feature, userId: string, request: LLMRequest) { const start = Date.now(); const response = await llmClient.call(request); await db.insert("llm_events", { feature, user_id: userId, model: request.model, tokens_in: response.usage.prompt_tokens, tokens_out: response.usage.completion_tokens, cost_usd: computeCost(request.model, response.usage), latency_ms: Date.now() - start, created_at: new Date(), }); return response;}Every call site passes its feature tag explicitly rather than the tracking logic trying to infer it — inference from context (which endpoint called this, which module imported it) breaks the first time code gets refactored, while an explicit tag at the call site stays correct because it's part of the function signature.
Aggregating and surfacing the breakdown
Once every call is tagged and logged, feature-level cost is a GROUP BY:
SELECT feature, SUM(cost_usd) as total_cost, COUNT(*) as callsFROM llm_eventsWHERE created_at > now() - interval '7 days'GROUP BY featureORDER BY total_cost DESC;This is the query that answers "why did the bill jump" in seconds instead of a log-grepping session — pull the same query for the prior week and diff the two, and the feature responsible for the increase is immediately visible rather than inferred from provider-level totals.
The same tagging pattern extends to per-user cost within a feature (add user_id to the GROUP BY), which is the same data you need if you're planning to charge customers for their usage rather than just monitor internal cost — attribution and billing turn out to be the same underlying problem viewed from two angles.
Where noburn fits
The tools discussed here handle observability, routing or evaluation, and all of them act once the LLM call has been made. noburn acts before it: one check() in your own code, placed before the provider call, returns allow or block against a project, per-user or per-run cap, so an over-budget call is never sent. It is not a proxy: your OpenAI or Anthropic client and its connection stay as they are. How the caps resolve, and the reason each one returns, is on the homepage.
Per-user caps and usage quotas keep one customer from spending another's budget, and Pro adds passthrough billing through Stripe. The Free plan covers 10,000 recorded calls a month with no card (current plans: pricing). Setup takes one install and one check: the quickstart, then per-user budgets and agent runs in the SDK reference.