Audit LLM API Costs Locally Before Adding a Proxy

Find what drove an AI API bill increase before changing models or adding a proxy. This guide compares two complete weeks and ranks cost changes by provider, model, processing mode and charge type.

You will prepare a small CSV and run a local Python script. It sends no API requests, needs no API key and does not collect prompts. It reviews past charges, not live spending limits.

What you need

  • Python 3.10 or later. Check with python --version, python3 --version or py -3 --version.
  • Read access to your billing reports and fourteen complete reporting days.
  • A private working folder and a copy of the original export.

1. Pick two complete weeks

Choose an end date your billing source considers complete. This example compares 3–9 October with 10–16 October 2026, matching the weekdays.

Record the source, account filters, currency, timezone and export time. Keep subscription allowances, direct API charges and cloud-marketplace bills separate. If reporting-day boundaries differ, audit providers separately unless you can normalize timestamped records correctly.

2. Prepare the CSV

The script does not import provider exports automatically. Map a copy into costs.csv with exactly these columns:

date,provider,model,mode,charge_type,currency,cost_usd

Use one row per day, provider, model, mode and charge type. Aggregate project or workspace subdivisions once. Never combine their totals with overlapping organization totals.

FieldRequired value
dateReporting day as YYYY-MM-DD.
provider, modelBilled provider and model. Use not_applicable for charges without a model.
modeBilled mode, or unknown. Do not assume Standard.
charge_typeA consistent category such as tokens, search or storage.
currencyUSD only. Do not relabel another currency.
cost_usdNon-negative dollar charges, not cents or token counts. Reconcile credits, refunds and tax separately.

Check source units before mapping. Anthropic’s Cost API reports decimal strings in cents: 12.5 cents becomes USD 0.125. That endpoint excludes Priority Tier costs. These rules do not automatically apply to dashboard CSVs or other providers.

Exclude prompts, responses, keys and personal identifiers. Spend metadata can still be sensitive; use non-sensitive grouping labels.

3. Try the fictional sample

Save this as sample-costs.csv. These are invented charges, not model prices or measured savings.

date,provider,model,mode,charge_type,currency,cost_usd
2026-10-03,demo,demo-model,standard,tokens,USD,0.10
2026-10-04,demo,demo-model,standard,tokens,USD,0.10
2026-10-05,demo,demo-model,standard,tokens,USD,0.10
2026-10-06,demo,demo-model,standard,tokens,USD,0.10
2026-10-07,demo,demo-model,standard,tokens,USD,0.10
2026-10-08,demo,demo-model,standard,tokens,USD,0.10
2026-10-09,demo,demo-model,standard,tokens,USD,0.10
2026-10-10,demo,demo-model,standard,tokens,USD,0.20
2026-10-11,demo,demo-model,standard,tokens,USD,0.20
2026-10-12,demo,demo-model,standard,tokens,USD,0.20
2026-10-13,demo,demo-model,standard,tokens,USD,0.20
2026-10-14,demo,demo-model,standard,tokens,USD,0.20
2026-10-15,demo,demo-model,standard,tokens,USD,0.20
2026-10-16,demo,demo-model,standard,tokens,USD,0.80

4. Run the audit

Save this as audit_costs.py in the same folder. It validates the CSV, uses decimal arithmetic and prints the largest dollar increases first. It makes no network calls and writes no files.

import csv
import sys
from collections import defaultdict
from datetime import date, timedelta
from decimal import Decimal, InvalidOperation

FIELDS = ["date", "provider", "model", "mode", "charge_type", "currency", "cost_usd"]

def audit(path, end_text):
    end = date.fromisoformat(end_text)
    first = end - timedelta(days=13)
    split = end - timedelta(days=6)
    expected = {first + timedelta(days=i) for i in range(14)}
    dates, keys = set(), set()
    totals = defaultdict(lambda: [Decimal("0"), Decimal("0")])
    with open(path, newline="", encoding="utf-8-sig") as handle:
        reader = csv.DictReader(handle)
        if reader.fieldnames != FIELDS:
            raise ValueError("Use exactly the seven documented CSV columns, in order.")
        for line, row in enumerate(reader, 2):
            if None in row or any(value is None for value in row.values()):
                raise ValueError(f"Line {line}: wrong number of columns.")
            row = {key: value.strip() for key, value in row.items()}
            if any(not value for value in row.values()):
                raise ValueError(f"Line {line}: blank fields are not allowed.")
            day = date.fromisoformat(row["date"])
            if day.isoformat() != row["date"]:
                raise ValueError(f"Line {line}: use YYYY-MM-DD dates.")
            if day not in expected:
                raise ValueError(f"Line {line}: date is outside the selected 14 days.")
            if row["currency"] != "USD":
                raise ValueError(f"Line {line}: this fixture accepts USD only.")
            amount = Decimal(row["cost_usd"])
            if not amount.is_finite() or amount < 0:
                raise ValueError(f"Line {line}: cost must be finite and non-negative.")
            group = tuple(row[k] for k in ["provider", "model", "mode", "charge_type"])
            unique = (day,) + group
            if unique in keys:
                raise ValueError(f"Line {line}: duplicate daily cost bucket.")
            keys.add(unique)
            dates.add(day)
            totals[group][int(day >= split)] += amount
    if dates != expected:
        raise ValueError("Missing dates: verify the export; add zero-cost rows only for confirmed zero days.")
    print(f"Previous: {first} to {split - timedelta(days=1)}")
    print(f"Current:  {split} to {end}")
    print("provider | model | mode | charge | previous USD | current USD | change USD")
    for group, (old, new) in sorted(totals.items(), key=lambda item: item[1][1] - item[1][0], reverse=True):
        print(" | ".join(group) + f" | {old:.6f} | {new:.6f} | {new-old:+.6f}")
    old = sum((values[0] for values in totals.values()), Decimal("0"))
    new = sum((values[1] for values in totals.values()), Decimal("0"))
    print(f"TOTAL USD: {old:.6f} -> {new:.6f} ({new-old:+.6f})")
    return old, new

if __name__ == "__main__":
    try:
        if len(sys.argv) != 3:
            raise ValueError("Usage: python audit_costs.py costs.csv YYYY-MM-DD")
        audit(sys.argv[1], sys.argv[2])
    except (ValueError, InvalidOperation, OSError) as exc:
        print(f"Audit stopped: {exc}", file=sys.stderr)
        raise SystemExit(2)

On Windows, run:

py -3 audit_costs.py sample-costs.csv 2026-10-16

On macOS or Linux, use python3 instead of py -3, or substitute your installed interpreter consistently. The sample should end with:

TOTAL USD: 0.700000 -> 2.000000 (+1.300000)

Then run the command against costs.csv, replacing the date with your chosen complete-period end date.

5. Reconcile, then investigate

Match each period’s total to the same filtered billing report. Explain differences such as excluded services, adjustments and rounding before changing application settings. If the source is estimated, label the audit estimated.

The script checks that all fourteen dates appear, not that every charge is present. Missing model or charge rows are treated as zero in the comparison. Confirm export completeness, and add zero-cost rows only for verified zero days.

Largest increaseCheck next
Model token costsRequest count, input length, cache writes/reads and output. Higher spend does not prove a price increase.
Fast or Priority usageActual billed mode and service-tier configuration.
Search, storage or executionTool counters, retention and service-specific prices.
More retry attemptsBounded retry logs against actual charges. Do not assume every failure was billed or free.

Check one representative request with the OpenAI, Claude or Gemini API cost calculator. Match the model, context band and mode; tool and other service charges may need separate calculations.

Avoid double-counting cache tokens. In the current OpenAI Responses format, cached and cache-write tokens are parts of total input. Claude reports uncached input, cache creation and reads separately. Follow the exact API schema, not similarly named fields.

Comparing with a local llama.cpp setup? Include hardware, electricity, administration and task quality. A smaller API bill alone does not prove lower overall cost.

Do you need a proxy?

Use this audit to identify which charge changed. Consider a gateway when you need shared routing, access controls or centrally managed limits across applications. Test its failure behavior and handling of prompts and credentials. A proxy estimate is not an invoice, and a configured budget is not necessarily an instantaneous hard cap.

Troubleshooting and cleanup

  • Wrong columns: check the header, order and malformed rows. Extra columns are rejected.
  • Duplicate bucket: check appended exports or omitted dimensions. Aggregate legitimate subdivisions once.
  • Missing dates: check freshness, pagination and verified zero-day coverage.
  • Currency or negative-value error: separate currencies and adjustments; never strip minus signs to pass validation.
  • Totals disagree: check units, account filters, excluded services and partial days.

The workflow changes no API keys, application settings or billing controls. To stop, stop using the working copy. Retain original exports securely and remove disposable copies under your normal retention policy.

FAQ

Can this stop overspending?

No. It analyzes past charges. Use provider or application controls for live enforcement.

Why leave token counts out of the CSV?

Start with billed changes, then inspect the affected bucket’s usage. Adding token data requires matching dates, models, account scope, geography and mode.

Does a lower total prove savings?

No. Compare workload volume and task quality too. Fewer jobs can lower the bill without making the application more efficient.

Sources and scope

Reviewed 19 October 2026. The Hacker News proxy discussion inspired the reader question, not the billing claims. The CSV format and script are an original offline workflow.

The fictional sample validates the workflow, not a real provider export, invoice or saving.

Leave a Comment

Scroll to Top

Discover more from Lachie's Lifestyle

Subscribe now to keep reading and get access to the full archive.

Continue reading