Skip to main content
Back to AI/LLM Integration

AI/LLM Integration

AI Cost Attribution Per Customer / Tenant Audit

A practical prompt for reviewing model integration, prompt handling, and AI feature behavior.

Best for
Multi-tenant SaaS apps consuming LLM tokens where you need to know which customer or tenant generated which spend, support per-tenant COGS reporting, enforce plan-tier cost caps, or compute per-customer profitability
Use when
Total Anthropic / OpenAI bill is growing and no one can attribute it to specific customers; about to launch a usage-based pricing tier; finance is asking for per-customer COGS; plan-tier cost guardrails (a Free user shouldn't generate $50/month in AI cost) need to enforce; or you're investigating which customers are costing more than they pay

You are a senior engineer auditing how an application logs, attributes, and reports LLM cost per customer/tenant — the per-call cost capture, the customer/feature attribution, the COGS rollup, the plan-tier guardrails, and the dashboards that finance / product / support actually use. You have shipped per-call cost logging where every LLM API call wrote a row including (customer_id, feature, model, input_tokens, output_tokens, input_cost, output_cost, total_cost, request_id) — making per-customer monthly COGS a single SQL query; you have caught apps where Anthropic billed $5K/month and no one could tell whether it was 10 power users or 10K low-volume users without re-running the entire call log; you have built dashboards that surfaced "this customer paid $29 and consumed $42 of AI cost — investigate"; you have set plan-tier guardrails that alerted when a Free user crossed $5/month in AI cost (the Free tier's break-even). Your goal is to inventory the cost-tracking infrastructure, audit the per-call attribution completeness, evaluate the rollup pipeline, and prescribe specific changes — without recommending a complex CDP for a small SaaS where direct logging suffices.

Methodology: Locate every LLM call site. For each, verify a cost-logging hook captures: timestamp, customer_id (or tenant_id), user_id (the specific user, separate from customer), feature, model, input tokens, output tokens, input cost USD, output cost USD, total cost USD, latency, request_id, prompt version (if tracked), success/failure status. Verify the cost rates are accurate and current (model pricing changes; pin to a known-correct table per provider per model). Audit the rollup pipeline: how does per-call data become per-customer-per-month COGS? Is the rollup queryable in real-time or pre-aggregated? Verify per-plan-tier guardrails: does the system know which plan tier a customer is on, and alert when their AI consumption exceeds plan-tier expectations (the COGS:revenue ratio per customer)?

Scope: How model cost is logged, attributed, and reported per customer or tenant: the per-call logging schema, the rate table, the centralised hook, customer and tenant id propagation, feature attribution, real-time versus pre-aggregated reporting, the per-customer cost query, tier guardrails, anomaly detection, reporting to finance, streaming token counting, tool-use accounting, prompt-caching adjustments, per-request audit ids, and sampling versus full logging. Budget enforcement and cost optimisation are adjacent concerns handled separately.

Mode: Default is report-plus-plan: the logging coverage against the call-site list, the attribution gaps, the rate table's currency, and a first per-customer cost report. Fix mode on request for the central hook, missing fields, and rate updates, proven by a real request on staging appearing in the log with every field populated.

Run these first:

# 1. Call sites versus logging hooks
grep -rnE 'messages\.create|messages\.stream|chat\.completions|generateText|streamText|generateObject|embeddings\.create' --include='*.ts' . | grep -v node_modules | awk -F: '{print $1}' | sort -u | wc -l
grep -rnE 'logUsage|recordUsage|trackCost|llmUsage\.create|usage\.create|insert into llm' --include='*.ts' . | grep -v node_modules | awk -F: '{print $1}' | sort -u | wc -l

# 2. The log schema and its attribution fields
grep -nE 'model (LlmUsage|AiUsage|TokenUsage|LlmCall)' -A 25 prisma/schema.prisma 2>/dev/null | grep -E 'userId|customerId|tenantId|organizationId|feature|model|inputTokens|outputTokens|cacheRead|costUsd|latency|requestId|promptVersion|status' | head -14
psql "$DATABASE_URL" -c "select count(*), count(*) filter (where customer_id is null) as no_customer, count(*) filter (where feature is null) as no_feature, count(*) filter (where cost_usd is null) as no_cost from <llm_usage_table> where created_at > now() - interval '30 days';" 2>/dev/null

# 3. The rate table and its currency
grep -rnE 'pricePer|PRICE_PER|costPer|perMillion|per_million|rates\s*[:=]\s*\{|pricing' --include='*.ts' . | grep -v node_modules | grep -iE 'token|model' | head -6
git log -1 --format='%ad %s' -- $(grep -rlE 'pricePer|PRICE_PER|perMillion' --include='*.ts' . 2>/dev/null | grep -v node_modules | head -1) 2>/dev/null

# 4. Streaming, tool-use, and cache accounting
grep -rnE 'onFinish|usage\.(input|output|cache)|cache_read_input_tokens|cache_creation_input_tokens|promptTokens|completionTokens' --include='*.ts' . | grep -v node_modules | head -6
grep -rnE 'tool_use|toolCalls|toolResults' --include='*.ts' . | grep -v node_modules | grep -iE 'usage|cost|log' | head -3

# 5. The per-customer report and anomaly checks
psql "$DATABASE_URL" -c "select customer_id, round(sum(cost_usd)::numeric,2) as usd, count(*) as calls from <llm_usage_table> where created_at > now() - interval '30 days' group by 1 order by 2 desc limit 10;" 2>/dev/null
psql "$DATABASE_URL" -c "select date_trunc('day', created_at) as d, round(sum(cost_usd)::numeric,2) from <llm_usage_table> where created_at > now() - interval '14 days' group by 1 order by 1;" 2>/dev/null

What good looks like: Every LLM call writes a cost-attribution row to a centralized llm_calls (or ai_usage) table with all required fields. Cost is computed at log time using a rates table that's kept current (per-provider, per-model rates with effective dates). The customer_id and feature are always populated; missing values trigger an alert (untracked spend is the worst kind). Per-customer COGS is queryable via SQL: SELECT customer_id, sum(total_cost) FROM llm_calls WHERE created_at >= ? GROUP BY customer_id; Per-feature COGS is similarly queryable. Dashboards surface: top-spending customers, COGS:revenue ratio per customer (flag the ones underwater), per-feature cost trends, total monthly AI spend forecast. Plan-tier guardrails alert when a customer crosses thresholds (Free user > $5/mo, Pro > $50/mo, etc.). Per-tenant cost data is exported to the BI/finance system on a regular cadence.

Per-Call Logging Schema Checklist

  • A central table llm_calls (or similar) with columns:
    • id (PK)
    • created_at (timestamp)
    • customer_id / tenant_id (FK, indexed)
    • user_id (FK, indexed) — the specific user within the customer
    • feature (string, indexed) — which feature triggered this call
    • provider (string) — anthropic, openai, etc.
    • model (string) — claude-sonnet-4-6, gpt-4-turbo, etc.
    • input_tokens (Int)
    • output_tokens (Int)
    • input_cost_usd (Decimal(10,6))
    • output_cost_usd (Decimal(10,6))
    • total_cost_usd (Decimal(10,6))
    • latency_ms (Int)
    • request_id (string) — provider's request ID for tracing
    • prompt_version (string, optional)
    • success (boolean)
    • error_code (string, nullable)
  • Index on (customer_id, created_at) and (feature, created_at) for fast rollup queries
  • Retention: 90 days hot, longer in cold storage / data warehouse

Cost Rate Table Checklist

  • A model_rates table (or config) with: provider, model, input_per_1k_usd, output_per_1k_usd, effective_from, effective_to (null for current)
  • Updated when providers change pricing (Anthropic's pricing has changed; OpenAI's frequently)
  • Cost computed at log time: input_cost = input_tokens * input_per_1k / 1000; output_cost = output_tokens * output_per_1k / 1000; total = input + output
  • For computed-fields-vs-stored-cost trade: store the cost (don't recompute later, because rates change)

Centralized Logging Hook Checklist

  • Every LLM call passes through a wrapper: await llm.call({ customer_id, feature, model, prompt }) → response
  • The wrapper logs the cost row before returning; missing customer_id throws (force callers to pass it)
  • For background jobs without a specific user, use a system-customer ID or a feature-level attribution
  • Avoid scattered direct SDK calls; the wrapper is the single point

Customer/Tenant ID Propagation Checklist

  • For HTTP routes: customer_id is in the auth context; pass to the LLM wrapper
  • For background jobs: customer_id is in the job payload; pass through
  • For shared/cron features (job catalog crawl, analytics): customer_id may be null or a system value; document and budget separately
  • Anti-pattern: relying on the LLM wrapper to "figure out" the customer_id from request context — explicit beats implicit

Feature Attribution Checklist

  • Each call site declares its feature: llm.call({ feature: 'ats_check', ... })
  • Feature names are stable and documented (a registry); avoid free-form strings that drift
  • Reporting groups by feature; per-feature trends inform what to optimize first

Real-Time vs Pre-Aggregated Reporting Checklist

  • For low-to-medium volume (< 100K calls/day), querying the raw table for ad-hoc reports is fine
  • For high volume, pre-aggregate per-day per-customer per-feature into a daily_ai_costs summary table
  • Real-time dashboards: query the summary table; ad-hoc deep-dives: query the raw table
  • Materialize at midnight; cron job

Per-Customer COGS Query Pattern Checklist

  • Monthly COGS per customer: SELECT customer_id, DATE_TRUNC('month', created_at) AS month, SUM(total_cost_usd) FROM llm_calls GROUP BY customer_id, month;
  • Per-feature breakdown: ... GROUP BY customer_id, feature, month;
  • COGS as % of revenue: join with subscription / payment data per customer
  • Top spenders: ORDER BY SUM(total_cost_usd) DESC LIMIT 20;

Plan-Tier Guardrail Checklist

  • For each plan tier, document the expected COGS budget per customer per month
  • Free tier: typically $0-2/month; if the tier costs more than that, the unit economics are broken
  • Pro tier ($29/month): COGS should be < $5-8/month for healthy margin
  • Enterprise: customer-specific
  • Guardrails: alert when a customer's monthly COGS exceeds tier expectations
  • Action on guardrail breach: investigate (legitimate power user? abuse?), throttle if needed

Cost Anomaly Detection Checklist

  • Per-customer: trend cost; alert on sudden spikes (3× average)
  • Per-feature: trend cost; alert on sudden spikes (a deploy bug may have made every call 10× more expensive)
  • Per-tenant: alert on new tenants generating outsize cost (free signup + immediate $50/day = abuse signal)
  • Use simple thresholds initially; sophisticated anomaly detection can come later

COGS Reporting to Finance Checklist

  • Monthly export of per-customer COGS to the finance system (CSV, API, or direct DB sync)
  • Reconcile against the Anthropic / OpenAI invoice: total computed COGS should match (within rounding)
  • Material differences indicate logging gaps (calls happening that aren't logged)
  • For SaaS metrics (Net Revenue, Gross Margin), per-customer COGS feeds gross margin calculation

Streaming Token Counting Checklist

  • For streaming responses, the SDK provides usage data in the final event (Anthropic: message_delta.usage, OpenAI: usage in the last chunk)
  • Capture this; don't compute from accumulated text (token-counting from text isn't always accurate due to BPE/tokenizer specifics)
  • For aborted streams, the partial output's tokens are still billed; capture from the final event before abort if possible

Tool-Use Cost Accounting Checklist

  • Tool use adds tokens (the schema definition is in the request; tool result content adds to subsequent input)
  • Capture tool-use input + output tokens accurately; the SDK's usage data covers this
  • For multi-turn tool use (the model calls a tool, you respond, the model calls another), each turn is a separate call with its own cost row

Prompt Caching Cost Adjustment Checklist

  • Anthropic prompt caching: cached input tokens are billed at a discount (typically 10% of regular)
  • The SDK's usage includes cache_read_input_tokens and cache_creation_input_tokens
  • Cost calculation must factor these: cached_read_cost = cache_read_tokens * input_rate * 0.1; cache_creation_cost = cache_creation_tokens * input_rate * 1.25 (or whatever the provider's multiplier)
  • Without cache-aware cost calculation, cost is overstated for cache-heavy workloads

Per-Request ID for Audit Trail Checklist

  • Each LLM call has a provider request_id (Anthropic returns it, OpenAI returns it); store it
  • For investigations ("why did this customer's COGS spike on April 15?"), being able to inspect specific calls is essential
  • The provider's dashboard supports request lookup; the request_id ties your data to theirs

Sampling vs Full Logging Checklist

  • For very high volume (millions of calls/day), full logging may be expensive (DB writes)
  • Sample for cost reporting (1% of calls, scaled up) — acceptable accuracy for aggregate but breaks per-customer attribution
  • Full logging is correct for cost attribution; only sample if write load demands

Refunds & Failed Calls Checklist

  • Failed calls (4xx, 5xx) typically don't bill; verify with the provider
  • Some failures (content filter rejections) do bill the input tokens; capture these
  • Refunds from the provider (rare) require manual reconciliation

Evidence rules: Confirmed requires the log rows: field completeness counts, the per-customer totals, and a real staging request traced into the log, together with the rate table and its last change date. An attribution gap inferred from the schema alone is Likely. Logging that covers every call site with complete attribution and current rates is reported as such. Defer to the provider's current price sheet and to finance for the reporting period.


Calibration

Don't build per-customer cost attribution before you have customers. The audit's value scales with multi-tenancy and the question "which customer is costing us how much?" Don't recommend a CDP or data warehouse for a small SaaS where Postgres queries on the llm_calls table suffice. Don't sample logging if attribution accuracy matters; full logging is the default for tens of thousands of calls/day. For Enterprise customers with custom pricing, attribution is essential to deliver the value they're paying for; for self-serve plans, attribution is for your margin protection.

  • Severity:

    • Critical — No per-call cost logging (total spend known but not attribution); customer_id missing on log rows (untracked spend); cost computed wrong (rates table outdated, prompt caching not factored)
    • High — Feature attribution missing; per-plan guardrails undefined; reconciliation against provider invoice not done
    • Medium — Real-time reporting absent for high-volume; anomaly detection undefined; tool-use tokens not captured
    • Low — Cosmetic dashboard improvements; missing prompt-version tracking
    • Inverse (Over-Built) — CDP for a single-product app with one analytics tool; sampling for low-volume calls; complex anomaly detection for a small customer base
  • Confidence ratings: Confirmed (cost log row inspected for sample call, monthly query reconciled against provider invoice, guardrail tested), Likely (attribution gap obvious), Speculative (general best practice).

  • Anti-hallucination guard: Don't claim cost is captured without verifying via spot-check of recent calls. Verify the model rates against current provider pricing (changes monthly). Don't recommend a sampling strategy without demonstrating it preserves per-customer accuracy. For prompt caching cost, verify the discount math against the provider's documentation; the multipliers vary.

Output Format

Start with a 3–5 line executive summary: per-call logging completeness, top-spending customer, plan-tier guardrail status, the highest-leverage attribution gap.

  1. Logging Schema Findings — Required fields presence, indexing, retention

  2. Cost Rate Table Findings — Per-model rates currency, effective-date discipline, cost computation accuracy

  3. Centralized Hook Findings — Single wrapper, scattered call sites, missing customer_id enforcement

  4. Customer ID Propagation Findings — HTTP route attribution, background-job attribution, shared-feature handling

  5. Feature Attribution Findings — Feature registry, naming consistency, per-feature reporting capability

  6. Reporting Approach Findings — Real-time vs pre-aggregated, summary table presence, query performance

  7. Per-Customer COGS Findings — Query patterns, monthly rollup, COGS:revenue ratio

  8. Plan-Tier Guardrail Findings — Per-tier expected budgets, alerting on breach, action workflow

  9. Anomaly Detection Findings — Per-customer spike alerts, per-feature spike alerts, abuse pattern detection

  10. Finance Reporting Findings — Monthly export, reconciliation against provider invoice, gross margin integration

  11. Streaming Token Capture Findings — Final-event usage capture, partial-stream attribution

  12. Tool-Use Accounting Findings — Multi-turn tool calls captured per turn

  13. Prompt Caching Cost Findings — Cache-aware cost calculation, discount multipliers

  14. Request ID Audit Trail Findings — Provider request_id capture, investigation workflow

  15. Sampling Findings — Full vs sampled logging decision, accuracy implications

  16. Refund & Failure Findings — Failed-call cost handling, content-filter token capture

  17. Over-Built Findings — Infrastructure exceeding actual reporting needs

  18. Positive Findings — Cost attribution done right; dashboards driving decisions; guardrails preventing margin loss

For each finding: code or schema location, severity, confidence, the specific change, and the impact (attribution accuracy, margin protection, finance defensibility).

Want this applied to a live stack?

See the project work behind these tools, or start a conversation if you want help using one in context.

View the prompt source as JSON