← Agent Frameworks 🕐 16 min read
Agent Frameworks

Enterprise AI Cost Chargeback and Showback Implementation (2026)

AI spend attribution is the single fastest-growing problem in enterprise FinOps.

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:

  1. Every dollar of AI spend traces to a cost center within one billing cycle
  2. Business unit leaders receive a monthly report they can act on without help from platform engineering
  3. Platform teams can answer “what is our cost per outcome?” for at least the top three AI workloads
  4. 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 prompt
  • OutputTokens — tokens in the completion
  • ImageCount — for image generation models
  • EmbeddingTokens — 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 USD
  • soft_budget — threshold that triggers alerts without blocking
  • tpm_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:

  1. Validate the billing methodology before going live with real transfers
  2. Give business units 1–2 quarters of visibility before they face budget impact
  3. 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:

  1. Install CUDOS following the Well-Architected Lab guide (creates the Athena views and QuickSight datasets)
  2. 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
  1. 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:

  1. Prior month allocation report — 15 min
  2. Anomaly review — any anomaly from prior month with root cause — 10 min
  3. Tag coverage KPIs — 5 min
  4. Budget adjustment requests — 10 min
  5. 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:

  1. 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.

  2. 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.

  3. 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.