Skip to content

fix(ui): include cache token columns in usage export - #32015

Merged
mateo-berri merged 1 commit into
litellm_internal_stagingfrom
litellm_fix_usage_export_cache_columns
Jul 3, 2026
Merged

fix(ui): include cache token columns in usage export#32015
mateo-berri merged 1 commit into
litellm_internal_stagingfrom
litellm_fix_usage_export_cache_columns

Conversation

@mateo-berri

@mateo-berri mateo-berri commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Reported by a customer via support

Linear ticket

Resolves LIT-4011

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Screenshots / Proof of Fix

Everything below ran against a real proxy and the real Anthropic API; no mocks anywhere. Setup: a fresh git worktree of this repo, a brand new venv (uv venv .venv-qa then uv pip install -e ".[proxy]" prisma), a dedicated Postgres database created only for this run (CREATE DATABASE litellm_qa_export_32015), and the proxy started on a randomly picked free port. Two chat completions were sent to anthropic/claude-haiku-4-5 with an ephemeral cache_control system block so the first call creates a cache entry and the second call reads it. After the daily spend flush landed, the actual export builders from the checked-out source (generateExportData in ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts, serialized with Papa.unparse, which is exactly what handleExportCSV downloads) were run over the live /user/daily/activity response, first at the base commit and then at this PR's commit, using the identical command

Pick a random free port and start the proxy; .env provides ANTHROPIC_API_KEY and the DATABASE_URL of the dedicated QA database (values never printed)

$ python3 -c "import socket; s=socket.socket(); s.bind(('127.0.0.1',0)); print(s.getsockname()[1])"
64023

$ cat qa_config.yaml
model_list:
  - model_name: anthropic-haiku-4-5
    litellm_params:
      model: anthropic/claude-haiku-4-5
      api_key: os.environ/ANTHROPIC_API_KEY

general_settings:
  master_key: sk-qa-1234

$ set -a; source .env; set +a; .venv-qa/bin/python litellm/proxy/proxy_cli.py --config qa_config.yaml --port 64023 > qa_proxy.log 2>&1 &
$ curl -s http://localhost:64023/health/liveliness
"I'm alive!"

Generate real cached traffic: the system block is ~9.5k tokens, well above the haiku cacheable minimum. First call creates the cache, second call reads it

$ python3 -c "import json; text=' '.join(f'Policy clause {i}: all reimbursement requests filed under schedule {i} must include itemized receipts, a cost center code, and written approval from the designated budget owner before payment processing may begin.' for i in range(220)); json.dump({'model': 'anthropic-haiku-4-5', 'max_tokens': 40, 'system': [{'type': 'text', 'text': text, 'cache_control': {'type': 'ephemeral'}}], 'messages': [{'role': 'user', 'content': 'In one short sentence, what do these clauses govern?'}]}, open('qa_payload.json', 'w'))"

$ curl -s http://localhost:64023/v1/chat/completions -H "Authorization: Bearer sk-qa-1234" -H "Content-Type: application/json" -d @qa_payload.json | jq .usage
{
  "completion_tokens": 28,
  "prompt_tokens": 9479,
  "total_tokens": 9507,
  "completion_tokens_details": {
    "reasoning_tokens": 0,
    "text_tokens": 28
  },
  "prompt_tokens_details": {
    "cached_tokens": 0,
    "text_tokens": 18,
    "cache_creation_tokens": 9461,
    "cache_creation_token_details": {
      "ephemeral_5m_input_tokens": 9461,
      "ephemeral_1h_input_tokens": 0
    }
  },
  "cache_creation_input_tokens": 9461,
  "cache_read_input_tokens": 0,
  "inference_geo": "not_available",
  "service_tier": "standard"
}

$ curl -s http://localhost:64023/v1/chat/completions -H "Authorization: Bearer sk-qa-1234" -H "Content-Type: application/json" -d @qa_payload.json | jq .usage
{
  "completion_tokens": 26,
  "prompt_tokens": 9479,
  "total_tokens": 9505,
  "completion_tokens_details": {
    "reasoning_tokens": 0,
    "text_tokens": 26
  },
  "prompt_tokens_details": {
    "cached_tokens": 9461,
    "text_tokens": 18,
    "cache_creation_tokens": 0,
    "cache_creation_token_details": {
      "ephemeral_5m_input_tokens": 0,
      "ephemeral_1h_input_tokens": 0
    }
  },
  "cache_creation_input_tokens": 0,
  "cache_read_input_tokens": 9461,
  "inference_geo": "not_available",
  "service_tier": "standard"
}

The backend daily activity API carries both cache token fields for the bucket (requests land in UTC date buckets, hence 2026-07-03), so the data the export modal receives is complete; only the row builders drop it

$ curl -s "http://localhost:64023/user/daily/activity?start_date=2026-07-01&end_date=2026-07-04" -H "Authorization: Bearer sk-qa-1234" | jq '.results[] | select(.metrics.api_requests > 0) | {date: .date, spend: .metrics.spend, prompt_tokens: .metrics.prompt_tokens, completion_tokens: .metrics.completion_tokens, cache_read_input_tokens: .metrics.cache_read_input_tokens, cache_creation_input_tokens: .metrics.cache_creation_input_tokens}'
{
  "date": "2026-07-03",
  "spend": 0.01307835,
  "prompt_tokens": 18958,
  "completion_tokens": 54,
  "cache_read_input_tokens": 9461,
  "cache_creation_input_tokens": 9461
}

The runner script imports the production builders from the checked-out source and prints the exact CSV the modal would download (the Usage page teams tab passes entity label "Team")

$ cat ui/litellm-dashboard/qa_export.ts
import Papa from "papaparse";
import { generateExportData } from "./src/components/EntityUsageExport/utils";

const port = process.env.QA_PORT;
const url = `http://localhost:${port}/user/daily/activity?start_date=2026-07-01&end_date=2026-07-04`;

const main = async () => {
  const res = await fetch(url, { headers: { Authorization: "Bearer sk-qa-1234" } });
  const spendData = await res.json();
  spendData.results = spendData.results.filter((day: any) => day.metrics.api_requests > 0);

  const dailyRows = generateExportData(spendData, "daily", "Team", {});
  console.log("===== CSV: scope 'daily' (what handleExportCSV downloads) =====");
  console.log(Papa.unparse(dailyRows));

  const modelRows = generateExportData(spendData, "daily_with_models", "Team", {});
  console.log("===== CSV: scope 'daily_with_models' =====");
  console.log(Papa.unparse(modelRows));
};

main();

Before, at the base commit: the daily CSV stops at "Completion Tokens" and the models CSV has no token detail at all, so the cache tokens are silently dropped

$ git checkout --detach origin/litellm_internal_staging
HEAD is now at 27069bd74f feat(ui): shadcn migration foundation: Tailwind v4, shadcn init, antd cascade fix (#31995)
$ cd ui/litellm-dashboard && npm ci && QA_PORT=64023 npx tsx qa_export.ts
===== CSV: scope 'daily' (what handleExportCSV downloads) =====
Date,Team,Team ID,Spend ($),Requests,Successful Requests,Failed Requests,Total Tokens,Prompt Tokens,Completion Tokens
2026-07-03,default_user_id,default_user_id,0.0131,2,2,0,19012,18958,54
===== CSV: scope 'daily_with_models' =====
Date,Team,Team ID,Model,Spend ($),Requests,Successful,Failed,Total Tokens
2026-07-03,default_user_id,default_user_id,anthropic/claude-haiku-4-5,0.0131,2,2,0,19012

Reconciling that export against published claude-haiku-4-5 prices (input 1e-06, output 5e-06 per token) fails; the exported columns cannot explain the exported spend

$ python3 -c "
prompt, completion = 18958, 54
naive = prompt * 1e-06 + completion * 5e-06
print(f'naive spend = prompt*1e-06 + completion*5e-06 = {naive:.6f}')
print('CSV Spend   = 0.0131')
print(f'delta       = {naive - 0.0131:+.6f}  (cannot be explained from the exported columns)')"
naive spend = prompt*1e-06 + completion*5e-06 = 0.019228
CSV Spend   = 0.0131
delta       = +0.006128  (cannot be explained from the exported columns)

After, at this PR's commit, rerunning the identical command: both cache columns appear in both scopes with the real token counts, and the models scope also gains prompt and completion tokens

$ git checkout --detach origin/litellm_fix_usage_export_cache_columns
HEAD is now at fbca54416a fix(ui): include cache token columns in usage export
$ cd ui/litellm-dashboard && QA_PORT=64023 npx tsx qa_export.ts
===== CSV: scope 'daily' (what handleExportCSV downloads) =====
Date,Team,Team ID,Spend ($),Requests,Successful Requests,Failed Requests,Total Tokens,Prompt Tokens,Completion Tokens,Cache Read Input Tokens,Cache Creation Input Tokens
2026-07-03,default_user_id,default_user_id,0.0131,2,2,0,19012,18958,54,9461,9461
===== CSV: scope 'daily_with_models' =====
Date,Team,Team ID,Model,Spend ($),Requests,Successful,Failed,Total Tokens,Prompt Tokens,Completion Tokens,Cache Read Input Tokens,Cache Creation Input Tokens
2026-07-03,default_user_id,default_user_id,anthropic/claude-haiku-4-5,0.0131,2,2,0,19012,18958,54,9461,9461

With the cache columns present the row reconciles exactly, to eight decimals, against the same price sheet (cache creation 1.25e-06, cache read 1e-07 per token)

$ python3 -c "
prompt, completion, cache_read, cache_creation = 18958, 54, 9461, 9461
regular_input = prompt - cache_read - cache_creation
spend = regular_input * 1e-06 + cache_creation * 1.25e-06 + cache_read * 1e-07 + completion * 5e-06
print(f'regular input tokens = {regular_input}')
print(f'reconstructed spend  = {spend:.8f}')
print('API bucket spend     = 0.01307835')
print(f'CSV Spend column     = 0.0131 (display rounded to 4 decimals; {spend:.4f} == 0.0131: {round(spend, 4) == 0.0131})')"
regular input tokens = 36
reconstructed spend  = 0.01307835
API bucket spend     = 0.01307835
CSV Spend column     = 0.0131 (display rounded to 4 decimals; 0.0131 == 0.0131: True)

Type

🐛 Bug Fix

Changes

The Export Data modal on the Usage page drops cache token metrics from both CSV and JSON exports. The daily activity API and the frontend aggregation both carry cache_read_input_tokens and cache_creation_input_tokens, but the three row builders in ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts omitted them from the exported rows. This made exported spend impossible to reconcile against model prices: a real export row showed Spend $0.011054 with Prompt Tokens 16306 (which includes cache tokens) and Completion Tokens 8, yet 16306 x $1/M plus 8 x $5/M works out to $0.016346. With the previously hidden fields (24 regular input tokens, 8141 cache creation tokens at $1.25/M, 8141 cache read tokens at $0.10/M) the row reconciles exactly to $0.01105435

This PR adds "Cache Read Input Tokens" and "Cache Creation Input Tokens" columns to all three export scopes. generateDailyData sources them from the entity metrics, and generateDailyWithKeysData accumulates them in its per-key aggregation before emitting the columns. generateDailyWithModelsData previously only emitted "Total Tokens"; its per-model aggregation now also accumulates prompt, completion, and both cache token fields and emits them as columns, so per-model rows become reconcilable like the other scopes. The JSON export shares these row builders, so it picks up the same columns automatically

Regression tests assert the exact numeric cache values per entity per day, per key (including aggregation across duplicate day entries for the same key), and per model (including summation across two keys that hit the same model), plus zero defaults when the backend omits the fields


Note

Low Risk
Dashboard-only export formatting with additive columns and unit tests; no API or billing logic changes.

Overview
Fixes Usage page CSV/JSON exports that already received cache_read_input_tokens and cache_creation_input_tokens from daily activity but dropped them in the row builders.

generateDailyData now emits Cache Read Input Tokens and Cache Creation Input Tokens from entity metrics (defaulting to 0 when absent). generateDailyWithKeysData tracks those fields in per-date/entity/key aggregation and includes them on each row. generateDailyWithModelsData now sums prompt, completion, and both cache token types from each model’s per-key breakdown and exports them as columns (previously model rows only had total tokens), so spend can be reconciled against cache-aware pricing.

JSON export uses the same builders, so it picks up the new columns automatically. Tests cover exact values, key-level aggregation across duplicate days, model-level summation across keys, and zero defaults when metrics are missing.

Reviewed by Cursor Bugbot for commit fbca544. Bugbot is set up for automated code reviews on this repo. Configure here.

@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a bug where cache_read_input_tokens and cache_creation_input_tokens were omitted from the Usage page's CSV and JSON exports, making spend totals impossible to reconcile against model pricing. The fix adds the two fields to all three export row builders (generateDailyData, generateDailyWithKeysData, generateDailyWithModelsData) and also back-fills Prompt Tokens and Completion Tokens columns that were missing from the per-model export scope.

  • generateDailyData reads cache fields directly from entity-level metrics with || 0 fallbacks, consistent with existing prompt/completion handling.
  • generateDailyWithKeysData extends the aggregation object type declaration and both the initialisation and accumulation branches to carry the two new fields.
  • generateDailyWithModelsData adds the four new camelCase accumulators to the per-model object and emits them in the output row; the per-key model metrics are the correct source since the function already cross-references entity keys with the model breakdown.
  • Regression tests cover exact numeric values for the daily, per-key (including multi-day aggregation), and per-model (including multi-key summation) cases, plus zero-default behaviour when the backend omits the fields.

Confidence Score: 5/5

Safe to merge — changes are purely additive column additions to frontend export utilities with no backend or auth-path impact.

All three export functions receive symmetric, consistent treatment: type declarations, initialisation, accumulation, and output rows are all updated together. The fallback || 0 pattern matches pre-existing handling of prompt_tokens and completion_tokens. The new regression tests cover the key correctness properties — per-entity per-day values, multi-day aggregation within the key export, multi-key summation within the model export, and zero defaults — and no existing assertions were weakened or removed.

No files require special attention.

Important Files Changed

Filename Overview
ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts Adds cache_read_input_tokens and cache_creation_input_tokens columns to all three export row builders; changes are additive, consistent with existing
ui/litellm-dashboard/src/components/EntityUsageExport/utils.test.ts New tests assert exact numeric cache token values for each export scope, including multi-day/multi-key aggregation and zero-default cases; no existing assertions weakened.

Reviews (1): Last reviewed commit: "fix(ui): include cache token columns in ..." | Re-trigger Greptile

@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes missing cache token columns in the Usage page export by adding cache_read_input_tokens and cache_creation_input_tokens to all three row builders (generateDailyData, generateDailyWithKeysData, generateDailyWithModelsData). As a side effect, generateDailyWithModelsData also gains Prompt Tokens and Completion Tokens columns that were previously absent, making per-model rows reconcilable against model pricing.

  • Adds "Cache Read Input Tokens" and "Cache Creation Input Tokens" columns to every export scope (daily, daily-with-keys, daily-with-models) in both CSV and JSON output.
  • Extends generateDailyWithModelsData to accumulate and emit promptTokens and completionTokens from the model-level API key breakdown, bringing it in line with the other two export scopes.
  • Adds regression tests that verify exact numeric values per entity per day, per key (including aggregation across duplicate day entries), and per model (including summation across multiple keys hitting the same model).

Confidence Score: 5/5

Safe to merge — the change is additive, touching only the export row builders in a single UI utility file with no server-side impact.

Both changed files are confined to the frontend export utility. The additions follow the exact same defensive || 0 pattern already used for prompt_tokens and completion_tokens, the TypeScript type for the aggregation map is updated to match, and the new tests cover exact numeric values for all three export scopes including multi-entry aggregation. No existing assertions were weakened or removed.

No files require special attention.

Important Files Changed

Filename Overview
ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts Adds cache_read_input_tokens and cache_creation_input_tokens to all three export row builders, plus prompt/completion tokens to generateDailyWithModelsData; logic is consistent and defensive across all paths.
ui/litellm-dashboard/src/components/EntityUsageExport/utils.test.ts Adds targeted regression tests for exact cache token values across all three export scopes, including aggregation cases and zero-default coverage; no existing assertions weakened.

Reviews (2): Last reviewed commit: "fix(ui): include cache token columns in ..." | Re-trigger Greptile

@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@yucheng-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit fbca544. Configure here.

@mateo-berri
mateo-berri merged commit 2633e8f into litellm_internal_staging Jul 3, 2026
127 checks passed
@mateo-berri
mateo-berri deleted the litellm_fix_usage_export_cache_columns branch July 3, 2026 03:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants