Skip to content

feat(proxy): per-component response cost headers - #36965

Merged
mateo-berri merged 4 commits into
BerriAI:litellm_internal_stagingfrom
erensh27:feat/per-component-cost-headers
Aug 15, 2026
Merged

feat(proxy): per-component response cost headers#36965
mateo-berri merged 4 commits into
BerriAI:litellm_internal_stagingfrom
erensh27:feat/per-component-cost-headers

Conversation

@erensh27

@erensh27 erensh27 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Callers only see a total request cost, never its component split
  • Attributing spend needs hand-copied pricing tables that drift per provider

How it solves it:

  • Emits six x-litellm-response-cost-* component headers next to the total
  • Input, cache read, cache creation, output, and tool usage sum exactly to the total; reasoning is a subset of output
  • Reads the cost breakdown the proxy already computes per request
  • Omits component headers cleanly when no breakdown was computed
  • Existing cost, discount, and margin headers are unchanged

User Flow

Before: a platform team fronting all LLM traffic through the LiteLLM proxy wants to show each internal app where its spend goes, but callers only ever see one total, so they rebuild the split by hand from their own pricing tables

  1. An app sends POST https://litellm-domain/v1/chat/completions with a prompt-cached context on a reasoning model such as gpt-5.4-nano
  2. The 200 response carries x-litellm-response-cost: 7.4e-06 and the body's usage block carries token counts (prompt_tokens, completion_tokens, cached_tokens, reasoning_tokens)
  3. To split the total they multiply those token counts by per-model rates copied into their own pricing table, which drifts: cache-write premiums, reasoning rates, and built-in tool charges differ per provider, and tool charges never appear in usage at all
  4. When their hand-computed components fail to sum to the header total, they cannot tell which component is wrong

After: the same response states the split directly and it sums to the total

  1. The app sends the same POST https://litellm-domain/v1/chat/completions with the same prompt-cached context
  2. The 200 response carries the same x-litellm-response-cost total and now also x-litellm-response-cost-input, -output, -cache-read, -cache-creation, -reasoning, and -tool-usage
  3. Input plus cache read plus cache creation plus output plus tool usage equals the total, reasoning being a subset of output, so the team attributes spend per component with no local pricing table

Relevant issues

Closes #36875

Linear ticket

Resolves LIT-5617

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 received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Live-proxy legs with real OpenAI and Anthropic spend, no mocks. Before leg at commit c9917cb (this PR's merge base), after leg re-proven at commit 05beb7a (this PR's behavioral tip). Each leg booted a bootstrapped worktree with .venv/bin/python litellm/proxy/proxy_cli.py --config qa_config.yaml --port <port> and hit all three unified endpoints: /v1/chat/completions and /v1/responses with gpt-5.4-nano, /v1/messages with claude-haiku-4-5 carrying a ~6k-token cache_control: ephemeral system block sent twice to exercise cache creation then cache read. An earlier eight-scenario after leg at 9079e4c surfaced that the input header still contained the cache costs nested inside it (Cursor Bugbot flagged the same double-count); 05beb7a fixed that by subtracting the cache components from the input header, the same split the Admin UI's cost breakdown viewer shows, and the leg below re-proves the final contract

Before leg (c9917cb, port 41387): component headers absent everywhere

curl -sD /tmp/h_a1.txt -o /tmp/b_a1.json -X POST http://localhost:41387/v1/chat/completions \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d @body_a.json
grep -i '^x-litellm-response-cost' /tmp/h_a1.txt

A1 and A2 (identical, 200 OK, prompt_tokens=1177, completion_tokens=4, answer "391"):

x-litellm-response-cost: 0.00024040000000000002
x-litellm-response-cost-original: 0.00024040000000000002
x-litellm-response-cost-discount-amount: 0.0
x-litellm-response-cost-margin-amount: 0.0
x-litellm-response-cost-margin-percent: 0.0

B (/v1/responses, 200 OK, input_tokens=12, output_tokens=5):

x-litellm-response-cost: 8.65e-06
x-litellm-response-cost-original: 8.65e-06
x-litellm-response-cost-discount-amount: 0.0
x-litellm-response-cost-margin-amount: 0.0
x-litellm-response-cost-margin-percent: 0.0

C1 and C2 (/v1/messages; C1 cache_creation_input_tokens=6242, C2 cache_read_input_tokens=6242):

== c1 ==
x-litellm-response-cost: 0.0078345
x-litellm-response-cost-original: 0.0078345
x-litellm-response-cost-discount-amount: 0.0
x-litellm-response-cost-margin-amount: 0.0
x-litellm-response-cost-margin-percent: 0.0
== c2 ==
x-litellm-response-cost: 0.0006562
x-litellm-response-cost-original: 0.0006562
x-litellm-response-cost-discount-amount: 0.0
x-litellm-response-cost-margin-amount: 0.0
x-litellm-response-cost-margin-percent: 0.0

Across all five before-leg responses, zero occurrences of -input, -output, -cache-read, -cache-creation, -reasoning, or -tool-usage

After leg (05beb7a, port 46746): six component headers present and additive

A1 (chat completions, gpt-5.4-nano, prompt_tokens=1417, completion_tokens=4, answer "391"; A2 byte-identical):

curl -sD /tmp/ha2_a1.txt -o /tmp/ba2_a1.json -X POST http://localhost:46746/v1/chat/completions \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d @body_a.json
grep -i '^x-litellm-response-cost' /tmp/ha2_a1.txt
x-litellm-response-cost: 0.0002884
x-litellm-response-cost-original: 0.0002884
x-litellm-response-cost-discount-amount: 0.0
x-litellm-response-cost-margin-amount: 0.0
x-litellm-response-cost-margin-percent: 0.0
x-litellm-response-cost-input: 0.0002834
x-litellm-response-cost-output: 5e-06
x-litellm-response-cost-tool-usage: 0.0

B (/v1/responses, input_tokens=12, output_tokens=5):

x-litellm-response-cost: 8.65e-06
x-litellm-response-cost-input: 2.4e-06
x-litellm-response-cost-output: 6.25e-06
x-litellm-response-cost-tool-usage: 0.0

C1 (/v1/messages, cold cache, cache_creation_input_tokens=6242, input_tokens=12) then C2 (warm, cache_read_input_tokens=6242):

== c1 ==
x-litellm-response-cost: 0.0078345
x-litellm-response-cost-input: 1.1999999999999858e-05
x-litellm-response-cost-output: 2e-05
x-litellm-response-cost-cache-creation: 0.0078025
x-litellm-response-cost-tool-usage: 0.0
== c2 ==
x-litellm-response-cost: 0.0006562
x-litellm-response-cost-input: 1.1999999999999966e-05
x-litellm-response-cost-output: 2e-05
x-litellm-response-cost-cache-read: 0.0006242
x-litellm-response-cost-tool-usage: 0.0

C1n/C2n (same shape, fresh nonce guaranteeing a cold cache; cache_creation then cache_read of 6267 tokens) behaved identically: creation 0.00783375, read 0.0006267, input 1.2e-05 on both

Br (/v1/responses with "reasoning": {"effort": "high"}, input_tokens=25, output_tokens=175, reasoning_tokens=168):

x-litellm-response-cost: 0.00022375
x-litellm-response-cost-input: 4.9999999999999996e-06
x-litellm-response-cost-output: 0.00021875
x-litellm-response-cost-reasoning: 0.00021
x-litellm-response-cost-tool-usage: 0.0

Sum invariant across all eight after-leg responses

Python check over the captured header files (rel_tol 1e-9, absent header = 0). Additive = input + cache-read + cache-creation + output + tool-usage; nested = input + output + tool-usage (the pre-fix contract)

Scenario Total Additive holds Nested holds reasoning<=output
A1 0.0002884 True True True
A2 0.0002884 True True True
B 0.00000865 True True True
C1 0.0078345 True False True
C2 0.0006562 True False True
C1n 0.0078658 True False True
C2n 0.0006587 True False True
Br 0.0002238 True True True

On every cache scenario the input header is exactly the 12 uncached tokens x 1e-06 = 1.2e-05, cache creation = tokens x 1.25e-06, and cache read = tokens x 1e-07, matching Haiku 4.5 pricing to the digit (rel_tol 1e-9)

Component semantics: _store_cost_breakdown_in_logging_obj in cost_calculator.py records the full prompt cost (cache pricing already applied) as input_cost, so emitting it unchanged would double-count the cache once the cache headers exist. The header path therefore subtracts cache read and cache creation from input before emitting, the exact split the Admin UI's cost breakdown viewer already shows, delivering the additive contract issue #36875 asked for: input + cache-read + cache-creation + output + tool-usage = total, reasoning a subset of output. Spend-log storage is untouched

QA surprises:

  • Stored input nests cache costs; headers now subtract them
  • Nested formula now fails on cache scenarios, by design
  • Input header carries float noise (1.19999...e-05), subtraction artifact
  • No specified scenario produced reasoning tokens; added Br
  • OpenAI auto-cache never hit on repeat prompts
  • .env master key is double-quoted; strip quotes
  • Harness reaped backgrounded proxies twice; detached Popen fixed it

Type

🆕 New Feature

Caveats (if any)

  • Component headers appear on non-streaming responses only (headers leave before a stream's usage exists)
  • Reasoning is a subset of output, so exclude it when summing components to the total
  • Cache and reasoning headers appear only when those costs are nonzero
  • The input header is the uncached input cost; spend logs keep storing the full prompt cost (cache included) as input_cost, so the header equals stored input minus the cache components

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

- Extract input_cost, output_cost, cache_read_cost, cache_creation_cost, reasoning_cost, and tool_usage_cost from logging object cost breakdown
- Populate x-litellm-response-cost-* component headers in ProxyBaseLLMRequestProcessing.get_custom_headers
- Ensure headers are omitted when cost breakdown is absent or values are None
- Add comprehensive test suite covering component headers, math invariants, caching, reasoning, and discounts/margins
@CLAassistant

CLAassistant commented Aug 14, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@greptile-apps

greptile-apps Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds per-component cost headers to non-streaming proxy responses while preserving existing total, discount, and margin headers

  • Introduces a named cost-breakdown result with input, output, cache, reasoning, and tool-usage components
  • Converts nested cached-input costs into an additive header split
  • Adds coverage for component values, absent breakdowns, cache costs, discounts, and margins

Confidence Score: 5/5

The PR appears safe to merge

No blocking failure remains

Important Files Changed

Filename Overview
litellm/proxy/common_request_processing.py Adds component cost extraction and response headers, with previous direct callers migrated safely to named-field access
tests/test_litellm/proxy/test_common_request_processing.py Adds component-header coverage and updates all helper callers to the expanded named return contract

Reviews (4): Last reviewed commit: "fix(proxy): emit uncached input cost so ..." | Re-trigger Greptile

Comment thread litellm/proxy/common_request_processing.py Outdated
Comment thread litellm/proxy/common_request_processing.py
@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.00000% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/proxy/common_request_processing.py 95.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing erensh27:feat/per-component-cost-headers (05beb7a) with litellm_internal_staging (29fe342)1

Open in CodSpeed

Footnotes

  1. No successful run was found on litellm_internal_staging (0f2566e) during the generation of this report, so 29fe342 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

Comment thread litellm/proxy/common_request_processing.py
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-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!

1 issue from previous review remains unresolved.

Fix All in Cursor

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

Reviewed by Cursor Bugbot for commit 7562445. Configure here.

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-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 05beb7a. Configure here.

@mateo-berri mateo-berri 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.

LGTM

@mateo-berri
mateo-berri merged commit cba2bea into BerriAI:litellm_internal_staging Aug 15, 2026
75 checks passed
Duxl-Ai pushed a commit to Duxl-Ai/litellm that referenced this pull request Aug 20, 2026
Seven live e2e tests covering cost-tracking regressions that currently ship
unnoticed: cache-write tokens billed at the cache-creation rate (BerriAI#34046),
per-component cost_breakdown on the spend row (BerriAI#31686), cache reads billed at
the cache-read discount on streamed calls (BerriAI#34812), cache tokens surviving the
anthropic-messages to Responses bridge (BerriAI#34957), priority-tier rates applied to
input, output and reasoning (BerriAI#35923, BerriAI#35925), the per-component response cost
headers summing to the total (BerriAI#36965), and cost injected into the final usage
frame of an /openai passthrough stream (BerriAI#36503).

Every test registers its own deployment with a distinct custom rate per
component, so a component billed at the wrong rate cannot pass. The shared
helpers in cost_rows.py encode the one thing the two surfaces disagree on: the
spend row's input_cost is gross of cache while the response's cost-input header
is net of it.
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.

[Feature]: per-component response cost headers (input, output, cache read, cache write, reasoning, tool usage)

3 participants