See also (wiki): ai-cost-chargeback-showback · llm-cost-attribution
AI spend attribution is the single fastest-growing problem in enterprise FinOps. Token-based billing from foundation model providers creates a cost structure unlike anything in prior cloud spend: the marginal cost of one LLM call is invisible until it aggregates, the same endpoint serves a dozen business units simultaneously, and the engineers building on top have zero price signal unless the platform team deliberately inserts one.
By Q1 2026, organizations running mature AI programs report that 30–60% of their AI spend is either untagged or misattributed. The fix is not primarily a tooling problem — it is an organizational design problem, and the tooling is downstream of the decisions. This note covers both layers: the governance decisions that must come first, and the specific implementation patterns (SQL, config, API calls) that encode those decisions.
The four non-negotiable outcomes of a working AI cost attribution program:
- Every dollar of AI spend traces to a cost center within one billing cycle
- Business unit leaders receive a monthly report they can act on without help from platform engineering
- Platform teams can answer “what is our cost per outcome?” for at least the top three AI workloads
- Anomalous spend triggers an alert within 24 hours, not at month-end
1. Showback vs Chargeback vs Split-Billing — When to Use Each
1.1 Definitions
Showback is the practice of allocating AI spend to business units in reports, without transferring funds. The platform team absorbs the actual invoice. Business units see their “share” but face no financial consequence.
Chargeback is the practice of transferring internal funds — via journal entry, cost center billing, or P&L adjustment — from the consuming business unit to the platform team (or directly to the cloud provider’s consolidated billing account). The consuming unit’s budget decreases by the allocated amount.
Split-billing (also called proportional billing or shared-services allocation) is a hybrid: some costs are directly attributed (e.g., dedicated fine-tuned model endpoints billed to a single team) while shared infrastructure costs (API gateways, observability tooling, prompt caching layers) are allocated by formula — typically by token consumption share or API call share.
1.2 Organizational Maturity Thresholds
Use this decision tree:
Has the organization been running AI spend for < 6 months?
YES → Start with showback only. No chargeback until tagging coverage > 80%.
NO ↓
Is AI spend > $500K/year per business unit?
NO → Showback is sufficient. Chargeback overhead exceeds value at lower spend.
YES ↓
Does the finance team have a process for internal cost center transfers?
NO → Build that process first. Chargeback without finance infrastructure creates
reconciliation nightmares.
YES ↓
Do business units have budget authority over AI spend in their annual plan?
NO → Showback with executive escalation. Chargeback requires budget holders.
YES → Implement chargeback.
Maturity thresholds table:
| Maturity Level | AI Spend Range | Attribution Model | Tag Coverage Required | Reporting Cadence |
|---|---|---|---|---|
| Level 1 — Exploratory | < $100K/yr | Showback only | > 50% | Quarterly |
| Level 2 — Scaling | $100K–$1M/yr | Showback + anomaly alerts | > 70% | Monthly |
| Level 3 — Operational | $1M–$10M/yr | Split-billing for shared infra, chargeback for dedicated workloads | > 85% | Monthly, with weekly anomaly digest |
| Level 4 — Governed | > $10M/yr | Full chargeback with internal transfer pricing | > 95% | Weekly spend, monthly P&L journal |
1.3 The Hidden Cost of Premature Chargeback
The most common failure mode is implementing chargeback before tagging infrastructure is in place. When the platform team charges back $1.2M but $400K is untagged, one of three bad things happens:
- The untagged $400K falls on the platform team’s P&L, creating a structural deficit that engineers don’t understand and finance can’t explain
- The untagged $400K gets allocated proportionally, which penalizes well-tagged business units for ungoverned usage from shadow teams
- Finance holds the chargeback until tagging resolves, destroying the monthly close timeline
Rule: Do not implement chargeback until you have 85% tag coverage on AI spend, measured by invoiced dollars, not by number of resources.
1.4 Internal Transfer Pricing Models
When chargeback is implemented, organizations choose among three transfer pricing philosophies:
Cost pass-through: Business units pay exactly what the platform team pays the cloud provider, allocated by their consumption share. Simple, defensible, but removes the platform team’s ability to invest in efficiency improvements (since any cost reduction just reduces charges to BUs rather than creating platform budget).
Marked-up cost pass-through: Platform team charges BUs at cloud cost + an overhead markup (typically 10–20%) to fund platform operations, tooling, and FinOps headcount. The markup must be documented and approved by finance annually.
Unit economics pricing: Platform team charges BUs a flat rate per unit of work — per 1M tokens, per API call, per document processed — regardless of underlying cloud cost. The platform team absorbs variance and captures efficiency gains. This model requires the platform team to forecast accurately and creates internal P&L pressure to optimize. It is the most sophisticated model and appropriate only at Level 4 maturity.
2. AWS-Specific Implementation: Bedrock Cost Attribution
2.1 AWS Cost Categories for Bedrock
AWS Cost Categories allow you to define allocation rules that map tagged resources to named cost categories, which then appear as line items in Cost Explorer and CUR (Cost and Usage Report).
A production Bedrock cost category for a multi-BU deployment:
{
"Name": "AI-BusinessUnit",
"Rules": [
{
"Value": "FinancialServices",
"Rule": {
"Tags": {
"Key": "ai-cost-center",
"Values": ["fin-svc", "fs-trading", "fs-risk", "fs-compliance"]
}
}
},
{
"Value": "Operations",
"Rule": {
"Tags": {
"Key": "ai-cost-center",
"Values": ["ops-supply-chain", "ops-logistics", "ops-field"]
}
}
},
{
"Value": "CustomerExperience",
"Rule": {
"Tags": {
"Key": "ai-cost-center",
"Values": ["cx-support", "cx-onboarding", "cx-retention"]
}
}
},
{
"Value": "SharedPlatform",
"Rule": {
"Not": {
"Tags": {
"Key": "ai-cost-center",
"Values": ["fin-svc", "fs-trading", "fs-risk", "fs-compliance",
"ops-supply-chain", "ops-logistics", "ops-field",
"cx-support", "cx-onboarding", "cx-retention"]
}
}
}
}
],
"DefaultValue": "Untagged-AI-Spend",
"SplitChargeRules": [
{
"Source": "SharedPlatform",
"Targets": ["FinancialServices", "Operations", "CustomerExperience"],
"Method": "PROPORTIONAL"
}
]
}
The SplitChargeRules section is critical: it takes the SharedPlatform bucket (untagged shared infra spend) and proportionally distributes it across the known business units. This prevents the platform team from absorbing shared costs that should be allocated.
2.2 Tag Strategy for Bedrock Invocations
Bedrock charges appear in CUR under the service namespace Amazon Bedrock. The billable dimensions are:
InputTokens— tokens in the promptOutputTokens— tokens in the completionImageCount— for image generation modelsEmbeddingTokens— for embedding model calls
Tags flow from the IAM principal making the API call. The recommended tag set for Bedrock is:
| Tag Key | Values | Purpose |
|---|---|---|
ai-cost-center |
{bu-code} |
Primary allocation dimension |
ai-workload |
{workload-name} |
Secondary allocation, maps to business outcome |
ai-env |
prod / staging / dev |
Filter dev spend out of business unit allocation |
ai-team |
{team-identifier} |
Aligns to LiteLLM team hierarchy |
ai-model |
claude-3-5-sonnet / nova-pro / etc |
Model cost analysis |
Tag enforcement via Service Control Policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RequireAICostCenterTagOnBedrock",
"Effect": "Deny",
"Action": [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream"
],
"Resource": "*",
"Condition": {
"Null": {
"aws:RequestTag/ai-cost-center": "true"
}
}
}
]
}
This SCP blocks any Bedrock invocation that does not include the ai-cost-center tag on the request. Apply it to the OU containing application accounts. Exempt the sandbox OU.
Note: This SCP approach requires application teams to pass tags at call time via the --tags parameter or via the SDK’s tag propagation. Teams using LiteLLM as a proxy do not need to handle this directly — the proxy handles tag injection (see Section 3).
2.3 Account-Level vs Tag-Level Attribution
Account-level attribution is simpler and more reliable. Each business unit gets a dedicated AWS account. Bedrock invocations from that account are 100% attributed to that BU with no tagging required. Use consolidated billing to aggregate costs to a master payer account.
Pros:
- Zero tag drift risk
- Clean blast radius isolation
- Simplest chargeback implementation — read account-level costs from CUR
Cons:
- Service limit quotas are per-account; shared model endpoints get fragmented
- Bedrock Provisioned Throughput cannot be shared across accounts without custom proxy routing
- Multiple accounts increase operational overhead for the platform team
Tag-level attribution is necessary when:
- Multiple business units share a single Bedrock endpoint for cost efficiency (e.g., Provisioned Throughput commitment)
- The organization uses a centralized AI gateway (LiteLLM, Amazon Bedrock Proxy, or similar)
- Cross-BU projects with mixed ownership cannot be cleanly mapped to a single account
The hybrid recommended pattern at Level 3+ maturity:
Master Payer Account
├── AI Platform Account (shared infra: API gateway, LiteLLM proxy, observability)
├── BU-FinancialServices Account (dedicated Bedrock quota, sensitive workloads)
├── BU-Operations Account (dedicated Bedrock quota)
└── AI Shared Workloads Account (cross-BU projects, tag-based allocation within)
2.4 CUR Athena Query for Bedrock Chargeback
This query produces a monthly department allocation table from the Cost and Usage Report stored in S3/Athena:
-- Monthly Bedrock spend by business unit and workload
-- Table: aws_cost_and_usage (your CUR Athena table name)
WITH bedrock_raw AS (
SELECT
bill_billing_period_start_date AS billing_month,
resource_tags_user_ai_cost_center AS cost_center,
resource_tags_user_ai_workload AS workload,
resource_tags_user_ai_env AS environment,
resource_tags_user_ai_model AS model,
line_item_usage_type,
line_item_usage_amount,
line_item_unblended_cost,
line_item_line_item_type
FROM aws_cost_and_usage
WHERE product_service_name = 'Amazon Bedrock'
AND line_item_line_item_type NOT IN ('Tax', 'Credit', 'Refund')
AND bill_billing_period_start_date >= date_trunc('month', current_date - interval '3' month)
),
bedrock_by_bu AS (
SELECT
date_format(billing_month, '%Y-%m') AS month,
COALESCE(cost_center, 'UNTAGGED') AS cost_center,
COALESCE(workload, 'unattributed') AS workload,
COALESCE(environment, 'unknown') AS environment,
COALESCE(model, 'unknown') AS model,
SUM(CASE WHEN line_item_usage_type LIKE '%InputTokens%'
THEN line_item_usage_amount ELSE 0 END) AS input_tokens,
SUM(CASE WHEN line_item_usage_type LIKE '%OutputTokens%'
THEN line_item_usage_amount ELSE 0 END) AS output_tokens,
SUM(line_item_unblended_cost) AS total_cost_usd
FROM bedrock_raw
GROUP BY 1, 2, 3, 4, 5
),
untagged_allocation AS (
-- Proportionally distribute untagged spend to known cost centers
SELECT
month,
cost_center,
workload,
environment,
model,
input_tokens,
output_tokens,
total_cost_usd,
-- What share of tagged spend does this BU represent?
SUM(CASE WHEN cost_center != 'UNTAGGED' THEN total_cost_usd ELSE 0 END)
OVER (PARTITION BY month, cost_center) /
NULLIF(SUM(CASE WHEN cost_center != 'UNTAGGED' THEN total_cost_usd ELSE 0 END)
OVER (PARTITION BY month), 0) AS bu_spend_share
FROM bedrock_by_bu
)
SELECT
month,
cost_center,
workload,
environment,
model,
input_tokens,
output_tokens,
total_cost_usd AS direct_cost_usd,
-- Add proportional share of untagged spend
total_cost_usd +
COALESCE(bu_spend_share, 0) * (
SELECT SUM(total_cost_usd)
FROM bedrock_by_bu b2
WHERE b2.month = untagged_allocation.month
AND b2.cost_center = 'UNTAGGED'
) AS fully_allocated_cost_usd
FROM untagged_allocation
WHERE cost_center != 'UNTAGGED'
ORDER BY month DESC, fully_allocated_cost_usd DESC;
Department summary rollup for finance reporting:
SELECT
month,
cost_center,
SUM(fully_allocated_cost_usd) AS total_allocated_usd,
SUM(input_tokens) AS total_input_tokens,
SUM(output_tokens) AS total_output_tokens,
COUNT(DISTINCT workload) AS active_workloads,
ROUND(
SUM(fully_allocated_cost_usd) /
NULLIF(SUM(input_tokens + output_tokens) / 1000000.0, 0),
4
) AS cost_per_million_tokens
FROM (/* above query */)
GROUP BY month, cost_center
ORDER BY month DESC, total_allocated_usd DESC;
3. LiteLLM Team Budget Hierarchy for Chargeback
3.1 Hierarchy Overview
LiteLLM (the open-source LLM proxy, widely deployed as an enterprise AI gateway) implements a four-level budget hierarchy that maps directly to the organizational structure needed for chargeback:
Organization (org_id)
└── Team (team_id) ← maps to business unit or department
└── Project (key_alias or team tag)
└── User (user_id)
Each level can have independent:
budget_duration— rolling window (daily, weekly, monthly)max_budget— hard cap in USDsoft_budget— threshold that triggers alerts without blockingtpm_limit— tokens per minute (rate limiting)rpm_limit— requests per minute
3.2 Provisioning via API
Create the org-level budget container:
curl -X POST https://your-litellm-proxy/organization/new \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{
"organization_alias": "FinancialServices",
"max_budget": 15000.00,
"budget_duration": "monthly",
"soft_budget": 12000.00,
"metadata": {
"cost_center": "fin-svc",
"finance_owner": "cfo-office",
"chargeback_enabled": true
}
}'
Create a team within that org:
curl -X POST https://your-litellm-proxy/team/new \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{
"team_alias": "fs-trading-algorithms",
"organization_id": "org_fin_svc",
"max_budget": 4000.00,
"budget_duration": "monthly",
"soft_budget": 3200.00,
"tpm_limit": 500000,
"metadata": {
"cost_center": "fin-svc",
"sub_team": "trading",
"aws_tag_ai_team": "fs-trading",
"approver": "vp-quant-research"
}
}'
Create a virtual key (API key) tied to the team, which engineers use to call models:
curl -X POST https://your-litellm-proxy/key/generate \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{
"team_id": "team_fs_trading",
"key_alias": "trading-algo-prod-v1",
"max_budget": 1000.00,
"budget_duration": "monthly",
"models": ["claude-3-5-sonnet", "claude-3-haiku", "bedrock-nova-pro"],
"metadata": {
"env": "prod",
"workload": "algo-signal-extraction",
"aws_tag_ai_workload": "fs-trading-signal"
}
}'
3.3 SpendLogs Schema
LiteLLM writes a row to LiteLLM_SpendLogs for every API call. The schema (PostgreSQL):
CREATE TABLE "LiteLLM_SpendLogs" (
"request_id" TEXT PRIMARY KEY,
"call_type" TEXT, -- 'completion', 'embedding', 'image_generation'
"api_key" TEXT, -- hashed virtual key
"spend" FLOAT, -- USD cost for this call
"total_tokens" INTEGER,
"prompt_tokens" INTEGER,
"completion_tokens" INTEGER,
"model" TEXT, -- model name as routed
"model_id" TEXT, -- deployment identifier
"model_group" TEXT, -- load-balanced group name
"api_base" TEXT, -- provider endpoint
"user" TEXT, -- end-user identifier
"team_id" TEXT, -- LiteLLM team ID
"org_id" TEXT, -- LiteLLM org ID
"startTime" TIMESTAMP,
"endTime" TIMESTAMP,
"completionStartTime" TIMESTAMP, -- time to first token
"cache_hit" TEXT, -- 'True'/'False'
"cache_key" TEXT,
"request_tags" JSONB, -- custom tags passed in request metadata
"requester_ip_address" TEXT,
"messages" JSONB, -- prompt (if logging enabled)
"response" JSONB, -- completion (if logging enabled)
"metadata" JSONB -- key metadata at time of call
);
The request_tags and metadata columns are the extension points for chargeback attribution. Teams can pass arbitrary tags in the request body:
import litellm
response = litellm.completion(
model="bedrock/claude-3-5-sonnet",
messages=[{"role": "user", "content": prompt}],
metadata={
"cost_center": "fin-svc",
"workload": "algo-signal-extraction",
"job_id": "batch-2026-06-18-001",
"user_id": "quant-researcher-42"
}
)
3.4 SpendLogs Chargeback Query
Monthly chargeback report from SpendLogs:
-- Monthly chargeback by org → team → model
WITH spend_enriched AS (
SELECT
date_trunc('month', "startTime") AS billing_month,
"org_id",
"team_id",
"model",
"user",
"spend",
"total_tokens",
"prompt_tokens",
"completion_tokens",
("cache_hit" = 'True') AS is_cache_hit,
(metadata->>'cost_center') AS cost_center,
(metadata->>'workload') AS workload,
(request_tags->>'env') AS environment
FROM "LiteLLM_SpendLogs"
WHERE "startTime" >= date_trunc('month', now() - interval '1 month')
AND "startTime" < date_trunc('month', now())
),
rollup AS (
SELECT
to_char(billing_month, 'YYYY-MM') AS month,
COALESCE(cost_center, 'UNTAGGED') AS cost_center,
COALESCE(org_id, 'no-org') AS org_id,
COALESCE(team_id, 'no-team') AS team_id,
COALESCE(workload, 'unattributed') AS workload,
model,
COALESCE(environment, 'unknown') AS environment,
COUNT(*) AS api_calls,
SUM(total_tokens) AS total_tokens,
SUM(prompt_tokens) AS prompt_tokens,
SUM(completion_tokens) AS completion_tokens,
SUM(spend) AS gross_spend_usd,
SUM(CASE WHEN is_cache_hit THEN spend ELSE 0 END) AS cache_savings_usd,
SUM(CASE WHEN NOT is_cache_hit THEN spend ELSE 0 END) AS net_spend_usd,
AVG(EXTRACT(EPOCH FROM ("endTime" - "startTime")) * 1000) AS avg_latency_ms
FROM spend_enriched
GROUP BY 1, 2, 3, 4, 5, 6, 7
)
SELECT
month,
cost_center,
org_id,
team_id,
workload,
model,
environment,
api_calls,
total_tokens,
ROUND(net_spend_usd::numeric, 4) AS net_spend_usd,
ROUND(cache_savings_usd::numeric, 4) AS cache_savings_usd,
ROUND((net_spend_usd / NULLIF(api_calls, 0))::numeric, 6) AS cost_per_call_usd,
ROUND((net_spend_usd / NULLIF(total_tokens / 1000000.0, 0))::numeric, 4) AS cost_per_million_tokens,
ROUND(avg_latency_ms::numeric, 0) AS avg_latency_ms
FROM rollup
ORDER BY month DESC, net_spend_usd DESC;
Budget utilization report with burn-rate projection:
WITH current_month_spend AS (
SELECT
team_id,
SUM(spend) AS month_to_date_spend
FROM "LiteLLM_SpendLogs"
WHERE "startTime" >= date_trunc('month', now())
GROUP BY team_id
),
team_budgets AS (
SELECT
t.team_id,
t.team_alias,
t.max_budget,
t.soft_budget,
t.org_id
FROM "LiteLLM_TeamTable" t
),
days AS (
SELECT
EXTRACT(DAY FROM now() - date_trunc('month', now())) + 1 AS days_elapsed,
EXTRACT(DAY FROM date_trunc('month', now() + interval '1 month')
- date_trunc('month', now())) AS days_in_month
)
SELECT
tb.team_alias,
tb.org_id,
ROUND(COALESCE(s.month_to_date_spend, 0)::numeric, 2) AS mtd_spend_usd,
ROUND(tb.max_budget::numeric, 2) AS budget_usd,
ROUND(tb.soft_budget::numeric, 2) AS soft_budget_usd,
ROUND((COALESCE(s.month_to_date_spend, 0) / NULLIF(tb.max_budget, 0) * 100)::numeric, 1) AS pct_budget_used,
-- Linear burn-rate projection to end of month
ROUND((COALESCE(s.month_to_date_spend, 0) / NULLIF(d.days_elapsed, 0) * d.days_in_month)::numeric, 2) AS projected_month_spend_usd,
CASE
WHEN (COALESCE(s.month_to_date_spend, 0) / NULLIF(d.days_elapsed, 0) * d.days_in_month) > tb.max_budget
THEN 'OVER_BUDGET'
WHEN (COALESCE(s.month_to_date_spend, 0) / NULLIF(d.days_elapsed, 0) * d.days_in_month) > tb.soft_budget
THEN 'AT_RISK'
ELSE 'ON_TRACK'
END AS budget_status
FROM team_budgets tb
LEFT JOIN current_month_spend s ON tb.team_id = s.team_id
CROSS JOIN days d
ORDER BY pct_budget_used DESC;
3.5 Webhook Integrations for Billing
LiteLLM supports spend tracking webhooks that fire when budget thresholds are crossed. Configure in litellm_config.yaml:
litellm_settings:
callbacks: ["webhook"]
webhook_settings:
url: "https://internal.company.com/ai-billing/webhook"
headers:
Authorization: "Bearer ${WEBHOOK_SECRET}"
X-Source: "litellm-proxy"
# Fire at soft budget (80% default), hard budget (100%), and on reset
events:
- "budget_crossed"
- "budget_reset"
- "max_budget_exceeded"
The webhook payload for a budget alert:
{
"event": "budget_crossed",
"event_group": "team",
"team_id": "team_fs_trading",
"team_alias": "fs-trading-algorithms",
"org_id": "org_fin_svc",
"current_spend": 3256.42,
"max_budget": 4000.00,
"soft_budget": 3200.00,
"pct_used": 81.4,
"budget_duration": "monthly",
"budget_reset_at": "2026-07-01T00:00:00Z",
"token_count": 18432000,
"timestamp": "2026-06-18T14:22:31Z"
}
Route these webhooks to Slack via a Lambda or Cloud Function:
import json
import boto3
import urllib.request
def handler(event, context):
body = json.loads(event['body'])
team = body['team_alias']
pct = body['pct_used']
spend = body['current_spend']
budget = body['max_budget']
status = body['event']
if status == 'max_budget_exceeded':
color = '#FF0000'
urgency = ':rotating_light: HARD LIMIT REACHED'
elif pct >= 90:
color = '#FF6600'
urgency = ':warning: CRITICAL — 90%+ consumed'
else:
color = '#FFCC00'
urgency = ':yellow_circle: Soft budget crossed'
slack_payload = {
"attachments": [{
"color": color,
"title": f"AI Budget Alert: {team}",
"text": f"{urgency}\n*Team:* {team}\n*Spent:* ${spend:,.2f} / ${budget:,.2f} ({pct:.1f}%)\n*Resets:* {body['budget_reset_at'][:10]}",
"footer": "LiteLLM Budget Monitor"
}]
}
# Post to team-specific Slack channel
channel_map = {
'fs-trading-algorithms': '#finops-fin-svc',
'ops-supply-chain': '#finops-operations',
}
webhook_url = get_slack_webhook(channel_map.get(team, '#finops-alerts'))
req = urllib.request.Request(
webhook_url,
data=json.dumps(slack_payload).encode(),
headers={'Content-Type': 'application/json'}
)
urllib.request.urlopen(req)
return {'statusCode': 200}
4. Cross-Charging Mechanisms
4.1 Internal Transfer Pricing in Practice
When a platform team operates an AI gateway and charges back to business units, the internal transfer price must be defensible to both the CFO and the engineering teams consuming it. The three levers:
Direct cost recovery: Charge the exact AWS/Anthropic/OpenAI invoice cost, allocated by consumption. No markup. The platform team’s operational costs (headcount, tooling, observability) are funded separately by a platform budget.
Blended rate card: The platform team publishes a quarterly rate card — a fixed price per million tokens per model tier, per API call for non-token-based services. The rate is set to recover full costs including overhead at projected volume. Surplus is returned to a platform investment fund; deficit triggers a rate adjustment.
Example rate card (illustrative, Q3 2026 enterprise pricing):
| Model Tier | Provider | Token Type | Internal Rate | AWS List Rate | Spread |
|---|---|---|---|---|---|
| Frontier — Sonnet class | Bedrock | Input | $0.0040/1K | $0.003/1K | +33% (covers overhead) |
| Frontier — Sonnet class | Bedrock | Output | $0.0200/1K | $0.015/1K | +33% |
| Standard — Haiku class | Bedrock | Input | $0.0003/1K | $0.00025/1K | +20% |
| Embedding | Bedrock Titan | Per 1K tokens | $0.00015/1K | $0.0001/1K | +50% |
| Image generation | Bedrock Nova Canvas | Per image | $0.060 | $0.04 | +50% |
Rate card markup justification must be documented: support cost, reliability SLA, observability tooling, FinOps headcount allocation, prompt caching infrastructure, and security/compliance tooling.
Outcome-based pricing: The platform team defines a unit of business value and prices the AI service accordingly. “Per document summarized: $0.012.” “Per customer support ticket deflected: $0.08.” This requires instrumentation to connect LLM calls to business outcomes, which most organizations don’t have until Level 4.
4.2 Unit Economics: Cost Per API Call and Cost Per Outcome
Cost per API call is the simplest unit metric. It aggregates well but obscures quality — a cheap API call that requires 5 retries is more expensive than it appears.
-- Weekly cost-per-call trend by workload
SELECT
date_trunc('week', "startTime") AS week,
(metadata->>'workload') AS workload,
COUNT(*) AS total_calls,
SUM(spend) AS total_spend_usd,
ROUND((SUM(spend) / COUNT(*))::numeric, 6) AS cost_per_call_usd,
ROUND(AVG(total_tokens)::numeric, 0) AS avg_tokens_per_call,
-- Cache efficiency
ROUND((COUNT(*) FILTER (WHERE cache_hit = 'True')::float / COUNT(*) * 100)::numeric, 1) AS cache_hit_pct
FROM "LiteLLM_SpendLogs"
WHERE "startTime" >= now() - interval '8 weeks'
GROUP BY 1, 2
ORDER BY week DESC, total_spend_usd DESC;
Cost per outcome requires joining AI spend to a business outcomes table. This is organization-specific, but the general pattern:
-- Example: cost per support ticket resolved by AI
-- Assumes outcomes table tracks which ticket_ids used AI and their resolution status
WITH ai_spend_per_ticket AS (
SELECT
(metadata->>'ticket_id') AS ticket_id,
SUM(spend) AS ai_spend_usd,
SUM(total_tokens) AS tokens_used
FROM "LiteLLM_SpendLogs"
WHERE "startTime" >= date_trunc('month', now() - interval '1 month')
AND (metadata->>'workload') = 'support-ticket-resolution'
AND metadata->>'ticket_id' IS NOT NULL
GROUP BY 1
),
outcomes AS (
SELECT
ticket_id,
resolved_by_ai, -- boolean
human_escalated, -- boolean
resolution_time_minutes,
created_at::date AS ticket_date
FROM support_tickets
WHERE created_at >= date_trunc('month', now() - interval '1 month')
)
SELECT
date_trunc('week', o.ticket_date) AS week,
COUNT(*) AS tickets_touched_by_ai,
COUNT(*) FILTER (WHERE o.resolved_by_ai) AS ai_resolved,
COUNT(*) FILTER (WHERE o.human_escalated) AS escalated_to_human,
ROUND((COUNT(*) FILTER (WHERE o.resolved_by_ai)::float / COUNT(*) * 100)::numeric, 1) AS ai_resolution_rate_pct,
SUM(s.ai_spend_usd) AS total_ai_spend_usd,
ROUND((SUM(s.ai_spend_usd) / NULLIF(COUNT(*) FILTER (WHERE o.resolved_by_ai), 0))::numeric, 4) AS cost_per_ai_resolution_usd,
AVG(o.resolution_time_minutes) FILTER (WHERE o.resolved_by_ai) AS avg_resolution_time_ai_min
FROM outcomes o
JOIN ai_spend_per_ticket s ON o.ticket_id = s.ticket_id
GROUP BY 1
ORDER BY 1 DESC;
4.3 Shadow Billing
Shadow billing is the practice of running the chargeback calculation without actually transferring funds — essentially a dry run at full fidelity. Use it to:
- Validate the billing methodology before going live with real transfers
- Give business units 1–2 quarters of visibility before they face budget impact
- Detect systematic mis-attribution before it causes political conflict
Implement shadow billing as a nightly job that writes to a shadow_billing_ledger table:
CREATE TABLE shadow_billing_ledger (
id SERIAL PRIMARY KEY,
billing_month DATE NOT NULL,
cost_center TEXT NOT NULL,
team_id TEXT,
workload TEXT,
direct_spend_usd NUMERIC(12, 4),
allocated_shared_usd NUMERIC(12, 4),
total_billed_usd NUMERIC(12, 4),
methodology_version TEXT NOT NULL, -- e.g., 'v2.1-proportional-untagged'
is_live_billing BOOLEAN DEFAULT FALSE,
generated_at TIMESTAMP DEFAULT now()
);
Run the live billing query with is_live_billing = FALSE. When governance approves going live, flip the flag and begin journal entries.
5. Tooling: AWS Cost Anomaly Detection, CUDOS, and Custom Athena
5.1 AWS Cost Anomaly Detection for AI Spend
Configure a Cost Anomaly Detection monitor scoped to Bedrock:
{
"MonitorName": "Bedrock-AI-Spend-Monitor",
"MonitorType": "DIMENSIONAL",
"MonitorDimension": "SERVICE",
"MonitorSpecification": {
"And": [
{
"Dimensions": {
"Key": "SERVICE",
"Values": ["Amazon Bedrock"]
}
}
]
}
}
Create a subscription with SNS routing:
{
"SubscriptionName": "Bedrock-Anomaly-Alerts",
"MonitorArnList": ["arn:aws:ce::123456789:anomalymonitor/MONITOR_ID"],
"Subscribers": [
{
"Address": "arn:aws:sns:us-east-1:123456789:ai-finops-alerts",
"Type": "SNS",
"Status": "CONFIRMED"
}
],
"Threshold": 15.0,
"ThresholdExpression": {
"Dimensions": {
"Key": "ANOMALY_TOTAL_IMPACT_PERCENTAGE",
"Values": ["15"]
}
},
"Frequency": "DAILY"
}
The SNS topic should fan out to:
- Slack (via Lambda → Slack webhook) — immediate notification
- Email to FinOps team DL — for audit trail
- PagerDuty (via SNS direct integration) — only if threshold exceeds $10K/day anomaly
SNS → Lambda routing for Slack with enrichment:
import json
import boto3
ce = boto3.client('ce')
def handler(event, context):
for record in event['Records']:
message = json.loads(record['Sns']['Message'])
anomaly = message['anomalyDetails']
# Enrich: get top cost center for the anomaly period
response = ce.get_cost_and_usage(
TimePeriod={
'Start': anomaly['anomalyStartDate'],
'End': anomaly['anomalyEndDate']
},
Granularity='DAILY',
Filter={
'Dimensions': {
'Key': 'SERVICE',
'Values': ['Amazon Bedrock']
}
},
GroupBy=[{'Type': 'TAG', 'Key': 'ai-cost-center'}],
Metrics=['UnblendedCost']
)
# Find the top cost center during the anomaly window
top_bu = max(
response['ResultsByTime'][-1]['Groups'],
key=lambda g: float(g['Metrics']['UnblendedCost']['Amount'])
)
slack_message = {
"text": f":alert: *Bedrock Spend Anomaly Detected*\n"
f"*Impact:* ${float(anomaly['totalImpact']['totalActualSpend']):.2f} "
f"(expected: ${float(anomaly['totalImpact']['totalExpectedSpend']):.2f})\n"
f"*Period:* {anomaly['anomalyStartDate']} → {anomaly['anomalyEndDate']}\n"
f"*Largest contributor:* {top_bu['Keys'][0]} "
f"(${float(top_bu['Metrics']['UnblendedCost']['Amount']):.2f})\n"
f"*Action required:* FinOps team to investigate within 24h"
}
# Post to Slack...
5.2 CUDOS for AI Chargeback Reports
CUDOS (Cost and Usage Dashboards Operations Solution) is the AWS Well-Architected Lab dashboard built on QuickSight + Athena + CUR. To add an AI chargeback panel:
- Install CUDOS following the Well-Architected Lab guide (creates the Athena views and QuickSight datasets)
- Add a custom Athena view for AI allocation:
-- CUDOS custom view: ai_chargeback_summary
CREATE OR REPLACE VIEW ai_chargeback_summary AS
SELECT
year,
month,
resource_tags_user_ai_cost_center AS cost_center,
resource_tags_user_ai_workload AS workload,
resource_tags_user_ai_env AS environment,
line_item_product_code,
SUM(line_item_unblended_cost) AS unblended_cost,
SUM(line_item_usage_amount) AS usage_amount,
line_item_usage_type
FROM ${table_name}
WHERE line_item_product_code IN ('AmazonBedrock', 'AmazonSageMaker')
AND line_item_line_item_type NOT IN ('Tax', 'Credit', 'Refund', 'Enterprise Discount Program Discount')
GROUP BY 1, 2, 3, 4, 5, 6, 9
- In QuickSight, create a calculated field for attribution coverage:
# Attribution Coverage %
ifelse(
isNull({cost_center}) OR {cost_center} = '',
0,
1
)
Aggregate this as a percentage to get your tag coverage KPI.
5.3 Custom Athena Query: Department Allocation with Shared Cost Split
This is the production query for a chargeback report that handles shared service costs:
-- Full AI spend allocation with split-charge for shared services
-- Runs monthly, parameterized by {billing_month} = 'YYYY-MM'
WITH
-- Step 1: All Bedrock and SageMaker spend, tagged and untagged
raw_ai_spend AS (
SELECT
bill_billing_period_start_date AS billing_period,
line_item_usage_account_id AS account_id,
line_item_product_code AS service,
resource_tags_user_ai_cost_center AS cost_center,
resource_tags_user_ai_workload AS workload,
resource_tags_user_ai_env AS env,
line_item_usage_type,
line_item_unblended_cost AS cost_usd,
line_item_usage_amount AS usage_amount
FROM your_cur_table
WHERE date_format(bill_billing_period_start_date, '%Y-%m') = '{billing_month}'
AND line_item_product_code IN ('AmazonBedrock', 'AmazonSageMaker')
AND line_item_line_item_type NOT IN ('Tax', 'Credit', 'Refund')
),
-- Step 2: Tag known platform accounts as SharedInfra
spend_classified AS (
SELECT
*,
CASE
WHEN account_id IN ('111111111111', '222222222222') -- platform accounts
THEN 'SharedInfra'
WHEN cost_center IS NULL OR cost_center = ''
THEN 'Untagged'
ELSE cost_center
END AS classified_cost_center
FROM raw_ai_spend
),
-- Step 3: Direct spend per cost center (excluding shared and untagged)
direct_spend AS (
SELECT
classified_cost_center AS cost_center,
SUM(cost_usd) AS direct_cost
FROM spend_classified
WHERE classified_cost_center NOT IN ('SharedInfra', 'Untagged')
GROUP BY 1
),
-- Step 4: Total direct spend (denominator for proportional allocation)
total_direct AS (
SELECT SUM(direct_cost) AS total FROM direct_spend
),
-- Step 5: Shared infra total to distribute
shared_total AS (
SELECT SUM(cost_usd) AS total
FROM spend_classified
WHERE classified_cost_center = 'SharedInfra'
),
-- Step 6: Untagged total to distribute
untagged_total AS (
SELECT SUM(cost_usd) AS total
FROM spend_classified
WHERE classified_cost_center = 'Untagged'
),
-- Step 7: Final allocation with shared and untagged distribution
final_allocation AS (
SELECT
d.cost_center,
d.direct_cost,
-- Proportional share of shared infra
COALESCE(st.total, 0) * (d.direct_cost / NULLIF(td.total, 0)) AS shared_allocated,
-- Proportional share of untagged spend
COALESCE(ut.total, 0) * (d.direct_cost / NULLIF(td.total, 0)) AS untagged_allocated
FROM direct_spend d
CROSS JOIN total_direct td
CROSS JOIN shared_total st
CROSS JOIN untagged_total ut
)
SELECT
'{billing_month}' AS billing_month,
cost_center,
ROUND(direct_cost, 2) AS direct_cost_usd,
ROUND(shared_allocated, 2) AS shared_infra_allocated_usd,
ROUND(untagged_allocated, 2) AS untagged_allocated_usd,
ROUND(direct_cost + shared_allocated + untagged_allocated, 2) AS total_allocated_usd
FROM final_allocation
ORDER BY total_allocated_usd DESC;
6. Governance: Who Approves Budgets, Escalation Paths, FinOps Council
6.1 FinOps Council Composition for AI Spend
A functional AI FinOps council requires four capabilities represented by distinct roles:
| Role | Responsibility | Typical Title |
|---|---|---|
| Financial controller | Owns the P&L treatment of AI spend; approves internal transfer pricing; signs off on chargeback methodology changes | VP Finance / Controller |
| Platform engineering lead | Owns the AI gateway infrastructure, tagging enforcement, and budget configuration in LiteLLM/Bedrock | Staff Engineer or Director, AI Platform |
| FinOps practitioner | Runs the monthly allocation reports, maintains CUR queries, owns anomaly alert routing | Cloud FinOps Analyst / FinOps Engineer |
| Business unit liaison (rotating) | Represents consuming teams; escalation path for disputed allocations; advocates for budget adjustments | VP Engineering or VP Product, rotating quarterly |
The council meets monthly (day 5–7 of the following month, after the billing cycle closes) with a standing agenda:
- Prior month allocation report — 15 min
- Anomaly review — any anomaly from prior month with root cause — 10 min
- Tag coverage KPIs — 5 min
- Budget adjustment requests — 10 min
- Methodology change proposals — as needed
6.2 Budget Approval Authority Matrix
Change Type | Approval Required
---------------------------------|------------------------------------------
New team budget < $5K/month | Engineering Manager + FinOps lead
New team budget $5K–$25K/month | VP Engineering + Finance BP
New team budget > $25K/month | CTO + CFO
Budget increase < 20% | Engineering Manager + FinOps lead
Budget increase 20%–50% | VP Engineering
Budget increase > 50% | Same as new budget at the resulting level
Emergency spend override | VP Engineering + FinOps lead (24h ratification)
Chargeback methodology change | FinOps council vote + CFO sign-off
Rate card revision | FinOps council + Finance sign-off, 30 days notice
6.3 Escalation Path for Budget Disputes
Disputes arise when a business unit believes they were over-allocated or when untagged spend is proportionally distributed to them. The escalation path:
-
Tier 1 (48h): BU engineer raises dispute with FinOps practitioner. FinOps pulls SpendLogs or CUR drill-down to verify allocation. If attribution error found → correction in next billing cycle + retroactive credit.
-
Tier 2 (5 business days): If Tier 1 unresolved, escalate to FinOps council BU liaison. Council reviews methodology, determines if the allocation formula is the issue vs. a data error.
-
Tier 3 (30 days): Structural dispute about transfer pricing or methodology → FinOps council formal review, potential methodology change for next quarter. CFO arbitrates if council deadlocks.
Retroactive credit mechanism:
-- Log retroactive credits for audit trail
INSERT INTO billing_adjustments (
billing_month,
cost_center,
adjustment_type,
original_charge_usd,
credit_usd,
reason,
approved_by,
applied_at
) VALUES (
'2026-05-01',
'fin-svc',
'ATTRIBUTION_ERROR',
1245.67,
312.41,
'Tag misconfiguration on trading-algo-prod key from 2026-05-12 to 2026-05-19; spend was misrouted to fin-svc instead of fs-trading',
'finops-council',
now()
);
7. Real-World Failure Modes
7.1 Tag Drift
What it looks like: Attribution coverage drops from 88% to 71% over three months without any new workloads being added. Investigation reveals that a key rotation cycle produced new LiteLLM virtual keys without copying the metadata from the old keys. Engineers copy the API key string into their applications, the metadata context is gone.
Root cause: LiteLLM virtual key rotation does not automatically inherit the metadata of the superseded key. Tag configuration lives on the key object, not the team object. When a key is regenerated, metadata must be explicitly re-specified.
Fix: Store authoritative tag configuration at the team level, not the key level. Use the team’s metadata as the source of truth and enforce it at call time via a custom callback:
# litellm custom callback to inject team metadata into every call
from litellm.integrations.custom_logger import CustomLogger
class TeamTagInjector(CustomLogger):
async def async_pre_call_hook(self, user_api_key_dict, cache, data, call_type):
team_id = user_api_key_dict.get("team_id")
if team_id and not data.get("metadata", {}).get("cost_center"):
team_metadata = await get_team_metadata(team_id) # your lookup
data.setdefault("metadata", {}).update({
"cost_center": team_metadata.get("cost_center"),
"workload": team_metadata.get("default_workload"),
"ai_team": team_id
})
return data
Prevention: Add tag coverage to the weekly engineering metrics dashboard. Alert when coverage drops below 80% within any rolling 7-day window.
7.2 Ungoverned Shared Model Endpoints
What it looks like: A single Bedrock Provisioned Throughput unit is shared across eight teams for cost efficiency. The endpoint has no per-team rate limiting. One team runs a batch job over a weekend, consuming 85% of the monthly token allocation in 48 hours. Other teams are throttled. The monthly chargeback attributes most of that month’s cost to the batch-running team, but they argue they were “using shared capacity and had no signal to stop.”
Root cause: Provisioned Throughput shared across teams without per-team metering or rate limiting at the application layer.
Fix: Route all shared endpoint traffic through LiteLLM with team-level TPM limits set to 1/N of total capacity (where N is the number of teams). The proxy enforces rate limits before traffic reaches the Bedrock endpoint.
# litellm_config.yaml — shared provisioned throughput with team limits
model_list:
- model_name: "shared-claude-sonnet-pt"
litellm_params:
model: "bedrock/arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-5-sonnet"
aws_region_name: "us-east-1"
model_info:
id: "shared-pt-endpoint"
# Per-team limits enforced at the proxy layer
# Each team object has tpm_limit set to 125000 (1M total / 8 teams)
# Teams can request temporary burst allowance via the FinOps council
7.3 Shadow AI Spend Escaping Attribution
What it looks like: At the monthly FinOps council meeting, the platform team reports $280K in attributed Bedrock spend. But the finance team’s consolidated AWS bill shows $410K for Amazon Bedrock. The $130K gap is coming from three engineering teams that created their own AWS IAM users, called Bedrock directly (bypassing the AI gateway), and applied no cost allocation tags.
Root cause: No enforcement layer requiring gateway usage. Engineers with direct AWS access can invoke Bedrock with no organizational visibility.
Fix — two-part:
Part 1: SCP (Service Control Policy) to deny direct Bedrock invocations from application accounts:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyDirectBedrockAccess",
"Effect": "Deny",
"Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:PrincipalAccount": "GATEWAY_ACCOUNT_ID"
}
}
}
]
}
Apply to the application OU. The gateway account (where LiteLLM runs) is exempted. All other accounts must route through the gateway.
Part 2: Detect bypass attempts with CloudTrail + EventBridge:
{
"source": ["aws.bedrock"],
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventName": ["InvokeModel", "InvokeModelWithResponseStream"],
"userIdentity": {
"accountId": [{"anything-but": "GATEWAY_ACCOUNT_ID"}]
}
}
}
Route EventBridge matches to a Lambda that logs the violation, notifies the FinOps Slack channel, and creates a JIRA ticket for the engineering manager.
7.4 Incomplete Chargeback at Month-End
What it looks like: The finance team needs all chargebacks journaled by the 5th of the month. Bedrock CUR data is available up to 3 days after month-end, but frequently has late-arriving line items that appear 5–7 days later. The FinOps team misses the close deadline or journals on incomplete data, then must issue corrections.
Fix: Use LiteLLM SpendLogs as the primary billing data source for chargeback (available same-day) and use CUR as the reconciliation source. Accept a small variance tolerance (< 2%) between LiteLLM spend and CUR; investigate any variance > 2%.
-- Monthly reconciliation: LiteLLM vs CUR
SELECT
'LiteLLM' AS source,
to_char(date_trunc('month', "startTime"), 'YYYY-MM') AS month,
SUM(spend) AS total_spend_usd
FROM "LiteLLM_SpendLogs"
WHERE date_trunc('month', "startTime") = date_trunc('month', now() - interval '1 month')
UNION ALL
SELECT
'CUR' AS source,
date_format(bill_billing_period_start_date, '%Y-%m') AS month,
SUM(line_item_unblended_cost) AS total_spend_usd
FROM aws_cost_and_usage
WHERE product_service_name = 'Amazon Bedrock'
AND date_format(bill_billing_period_start_date, '%Y-%m') = to_char(now() - interval '1 month', 'YYYY-MM')
AND line_item_line_item_type NOT IN ('Tax', 'Credit', 'Refund');
8. Metrics for a Healthy Chargeback Program
8.1 KPI Definitions and Targets
Attribution Coverage %
Definition: Dollar value of AI spend with a valid cost center tag / total AI spend, expressed as a percentage.
SELECT
to_char(date_trunc('month', "startTime"), 'YYYY-MM') AS month,
ROUND(
SUM(spend) FILTER (WHERE metadata->>'cost_center' IS NOT NULL
AND metadata->>'cost_center' != '')
/ NULLIF(SUM(spend), 0) * 100,
2
) AS attribution_coverage_pct,
ROUND(
SUM(spend) FILTER (WHERE metadata->>'cost_center' IS NULL
OR metadata->>'cost_center' = ''),
2
) AS unattributed_spend_usd
FROM "LiteLLM_SpendLogs"
WHERE "startTime" >= now() - interval '6 months'
GROUP BY 1
ORDER BY 1 DESC;
| Target | Threshold | Action |
|---|---|---|
| Green | ≥ 90% | No action required |
| Yellow | 80–89% | Root cause analysis; tag enforcement review |
| Red | < 80% | Freeze new workload onboarding until resolved |
Untagged Spend %
Same as 100% − Attribution Coverage %. Track separately as a lagging indicator of tag discipline degradation.
Target: < 5% by month 6 of program; < 2% at steady state.
Cost per Outcome Trend
The cost per unit of business outcome for the top three AI workloads, tracked weekly. This is the primary efficiency metric. Flat or declining = efficiency gains from caching, prompt optimization, or model tier optimization. Rising = regression.
Track as a percentage change from baseline (program launch) rather than absolute value, since absolute values vary by workload.
-- Cost per outcome trend: week-over-week change
WITH weekly_metrics AS (
SELECT
date_trunc('week', "startTime") AS week,
(metadata->>'workload') AS workload,
SUM(spend) AS spend_usd,
COUNT(*) AS api_calls,
SUM(total_tokens) AS tokens
FROM "LiteLLM_SpendLogs"
WHERE "startTime" >= now() - interval '12 weeks'
AND metadata->>'workload' IS NOT NULL
GROUP BY 1, 2
),
with_prev AS (
SELECT
*,
LAG(spend_usd / NULLIF(api_calls, 0)) OVER (PARTITION BY workload ORDER BY week)
AS prev_cost_per_call
FROM weekly_metrics
)
SELECT
week,
workload,
ROUND((spend_usd / NULLIF(api_calls, 0))::numeric, 6) AS cost_per_call_usd,
ROUND(((spend_usd / NULLIF(api_calls, 0) - prev_cost_per_call)
/ NULLIF(prev_cost_per_call, 0) * 100)::numeric, 2) AS wow_change_pct
FROM with_prev
ORDER BY workload, week DESC;
Budget Utilization Variance
Definition: How close was actual spend to the budget, across all teams. High variance (teams consistently under or over budget) indicates either bad forecasting or insufficient budget visibility.
Target: 80–110% utilization across > 80% of teams.
Teams consistently below 60% utilization signal either overprovisioned budgets (wasted financial planning) or blocked workloads. Teams consistently above 100% signal either growth that outpaced planning or an ungoverned workload.
SELECT
to_char(date_trunc('month', "startTime"), 'YYYY-MM') AS month,
"team_id",
SUM(spend) AS actual_spend,
t.max_budget AS budget,
ROUND((SUM(spend) / NULLIF(t.max_budget, 0) * 100)::numeric, 1) AS utilization_pct,
CASE
WHEN SUM(spend) / NULLIF(t.max_budget, 0) < 0.6 THEN 'UNDER_UTILIZED'
WHEN SUM(spend) / NULLIF(t.max_budget, 0) > 1.10 THEN 'OVER_BUDGET'
ELSE 'NORMAL'
END AS status
FROM "LiteLLM_SpendLogs" s
JOIN "LiteLLM_TeamTable" t USING (team_id)
GROUP BY 1, 2, t.max_budget
ORDER BY month DESC, utilization_pct DESC;
Anomaly Resolution Time
Definition: Time from anomaly alert trigger to root cause documented and spend corrective action taken.
Target: < 24 hours for alerts > $1K/day; < 4 hours for alerts > $10K/day.
Track in a simple table:
CREATE TABLE anomaly_log (
id SERIAL PRIMARY KEY,
alerted_at TIMESTAMP NOT NULL,
resolved_at TIMESTAMP,
anomaly_source TEXT, -- 'aws-cost-anomaly-detection' or 'litellm-budget-webhook'
cost_center TEXT,
impact_usd NUMERIC(12, 2),
root_cause TEXT,
corrective_action TEXT,
resolution_time_hours NUMERIC GENERATED ALWAYS AS (
EXTRACT(EPOCH FROM (resolved_at - alerted_at)) / 3600
) STORED
);
8.2 Monthly FinOps Dashboard Checklist
The following checklist is run by the FinOps practitioner at the start of each monthly reporting cycle (by day 3 of the following month):
- [ ] Pull LiteLLM SpendLogs for prior month; run chargeback query; export to finance template
- [ ] Pull CUR via Athena; reconcile with LiteLLM totals; flag any variance > 2%
- [ ] Calculate attribution coverage %; file in metrics tracking spreadsheet
- [ ] Run budget utilization report; identify teams > 100% or < 60%
- [ ] Check anomaly log; ensure all prior-month anomalies have root cause documented
- [ ] Export per-BU allocation to finance for journal entry by day 5
- [ ] Prepare council agenda: include any workloads with cost-per-call regression > 20% WoW
- [ ] Update rate card if any model pricing changed (Bedrock pricing updates are announced on the AWS pricing page; subscribe to pricing change notifications)
9. Implementation Sequencing
For organizations starting from zero, the recommended 6-month sequencing:
Month 1–2: Foundation
- Deploy LiteLLM proxy in AI platform account
- Implement virtual key issuance process (keys are created with mandatory metadata)
- Configure SpendLogs to PostgreSQL
- Begin showback reporting (monthly email to BU leaders)
- Target: 60% attribution coverage
Month 3–4: Enforcement
- Implement SCP blocking direct Bedrock access from application accounts
- Configure Cost Anomaly Detection with SNS routing
- Establish FinOps council with monthly cadence
- Begin shadow billing calculation (parallel to showback)
- Target: 80% attribution coverage
Month 5–6: Chargeback
- Enable chargeback for BUs > $50K/month (start with highest-spend teams)
- Implement budget approval workflow
- Deploy CUDOS dashboard for finance team self-service
- Publish internal rate card
- Target: 90% attribution coverage; full chargeback for Tier 1 BUs
Month 7+: Optimization
- Add cost-per-outcome tracking for top 3 workloads
- Quarterly rate card review process
- Expand chargeback to all BUs
- Begin outcome-based pricing experiments for willing BUs
- Target: > 95% attribution coverage; < 2% untagged spend
Appendix: Quick Reference
LiteLLM Environment Variables for Production Chargeback
# Database for SpendLogs
DATABASE_URL="postgresql://litellm:${DB_PASSWORD}@rds-host:5432/litellm_prod"
# Store all request metadata (enables cost_center, workload tags)
STORE_MODEL_IN_DB="True"
# Spend tracking
LITELLM_LOG="INFO"
# Redis for rate limiting enforcement
REDIS_URL="redis://elasticache-host:6379"
# Webhook for budget alerts
LITELLM_WEBHOOK_SECRET="${WEBHOOK_SECRET}"
# Master key (rotate quarterly; store in Secrets Manager)
LITELLM_MASTER_KEY="${MASTER_KEY}"
AWS Tag Naming Convention
Use a consistent prefix to distinguish AI tags from generic infrastructure tags:
ai-cost-center → {bu-code} # e.g., "fin-svc", "cx-support"
ai-workload → {kebab-case-name} # e.g., "algo-signal-extraction"
ai-env → prod | staging | dev
ai-team → {litellm-team-alias}
ai-model → {model-short-name} # e.g., "claude-3-5-sonnet"
ai-owner → {email-or-team-dl}
Enforce via AWS Config rule:
# AWS Config custom rule: check Bedrock invocations for required tags
required_tags = ['ai-cost-center', 'ai-workload', 'ai-env']
def evaluate_compliance(configuration_item, rule_parameters):
tags = configuration_item.get('tags', {})
missing = [t for t in required_tags if t not in tags or not tags[t]]
if missing:
return 'NON_COMPLIANT', f"Missing tags: {', '.join(missing)}"
return 'COMPLIANT', 'All required tags present'
Chargeback Methodology Version Control
Every change to the allocation formula must be versioned and documented. Store in a billing_methodology table:
CREATE TABLE billing_methodology (
version TEXT PRIMARY KEY, -- e.g., 'v2.3'
effective_from DATE NOT NULL,
effective_to DATE,
shared_cost_method TEXT NOT NULL, -- 'PROPORTIONAL' | 'FIXED_RATE' | 'EQUAL_SPLIT'
untagged_handling TEXT NOT NULL, -- 'PROPORTIONAL' | 'PLATFORM_ABSORBS' | 'DISCARD'
markup_pct NUMERIC(5, 2) DEFAULT 0,
approved_by TEXT NOT NULL,
change_summary TEXT,
created_at TIMESTAMP DEFAULT now()
);
When a chargeback report is generated, it records the methodology version used. This provides a complete audit trail for any historical period.