Skip to content

fix(proxy): attribute org spend for team-linked credentials minted without org_id - #34577

Merged
ryan-crabbe-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_org_spend_team_org_backfill
Jul 25, 2026
Merged

fix(proxy): attribute org spend for team-linked credentials minted without org_id#34577
ryan-crabbe-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_org_spend_team_org_backfill

Conversation

@ryan-crabbe-berri

@ryan-crabbe-berri ryan-crabbe-berri commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • CLI session tokens carry a real team_id but never an org_id
  • Their auth path skips the combined_view team join DB keys get
  • Spend from these tokens reaches the team but never the org
  • Org budget caps have nothing to trip on and get exceeded

How it solves it:

  • Backfill org_id from the fetched team at the centralized auth seam
  • Every credential type now leaves auth with the same complete identity
  • Spend writer and org budget check read the same derived org

Relevant issues

Linear ticket

Resolves LIT-4688

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

A customer ran an org with a hard budget cap and watched it get exceeded: 96% of their spend was logged with an empty organization_id even though every credential carried a real, org-linked team. Their traffic authenticates with CLI session tokens from /sso/cli/poll, which get_cli_jwt_auth_token mints with team_id set and org_id never set; at request time the token blob is decrypted and used as-is, so the combined_view SQL join that fills org from team for DB virtual keys never runs. Team spend and team budgets worked the whole time, which made the gap invisible

Proof against a live proxy on localhost:4141 (groq, real spend), same CLI session token, same org (max_budget $0.00001) and org-linked team both runs. Seed steps: POST /organization/new with the cap, POST /team/new with that organization_id, POST /user/new with that team, then mint the session token the same way /sso/cli/poll does

Before, at 7257d0f (base):

$ curl -s http://localhost:4141/v1/chat/completions -H "Authorization: Bearer $CLI_TOKEN" \
    -d '{"model":"qa-groq-llama","messages":[{"role":"user","content":"say BEFORE"}],"max_tokens":5}'
BEFORE

$ curl -s "http://localhost:4141/spend/logs?user_id=$CLI_USER" -H 'Authorization: Bearer sk-1234'
{'team': '0d9bd741-95dc-49d5-b6b0-6b01b20456c0', 'org': 'EMPTY', 'spend': 2.42e-05}

$ curl -s "http://localhost:4141/organization/info?organization_id=$ORG_ID" -H 'Authorization: Bearer sk-1234'
org spend: 0.0

After, at 22607bf (this PR):

$ curl -s http://localhost:4141/v1/chat/completions -H "Authorization: Bearer $CLI_TOKEN" \
    -d '{"model":"qa-groq-llama","messages":[{"role":"user","content":"say AFTER"}],"max_tokens":5}'
AFTER

$ curl -s "http://localhost:4141/spend/logs?user_id=$CLI_USER" -H 'Authorization: Bearer sk-1234'
{'team': '0d9bd741-95d', 'org': 'a4612ad4-808f-4f73-b80f-0746621186bb', 'spend': 2.42e-05}

$ curl -s "http://localhost:4141/organization/info?organization_id=$ORG_ID" -H 'Authorization: Bearer sk-1234'
2.42e-05

$ curl -s -w "\nHTTP %{http_code}" http://localhost:4141/v1/chat/completions -H "Authorization: Bearer $CLI_TOKEN" \
    -d '{"model":"qa-groq-llama","messages":[{"role":"user","content":"say AFTER2"}],"max_tokens":5}'
{"error":{"message":"Budget has been exceeded! Organization=a4612ad4-808f-4f73-b80f-0746621186bb Current cost: 2.42e-05, Max budget: 1e-05","type":"budget_exceeded","param":null,"code":"429"}}
HTTP 429

Restart durability, which is what the customer actually observed in production: on the base commit the org budget check reads an in-memory counter fed by nothing durable, so a single pod can throw one transient 429 at the threshold, and a restart (or any other pod) reseeds from the DB's 0.0 and serves traffic again. Reproduced both ways

base 7257d0fc89: call over cap -> 429 (in-memory counter), restart proxy, same call -> HTTP 200
this PR 22607bf801: call over cap -> 429, restart proxy, same call -> still HTTP 429

The fix makes the DB org spend real, so enforcement survives restarts and is consistent across pods

Type

🐛 Bug Fix

Changes

_run_centralized_common_checks now sets user_api_key_auth_obj.org_id from team_object.organization_id when the credential has no org of its own. The mutation is per-request only, on the UserAPIKeyAuth built for this request, never the cached key row, so a team moving to a different org is honored on the next auth once the team cache refreshes. A credential with an explicitly pinned org_id always wins

This is the same derivation combined_view already does in SQL for DB virtual keys, applied at the one seam every auth path flows through, so CLI session tokens, JWT team tokens, and any future credential type get the same complete identity that keys get. The org budget check's local team fallback keeps working as before; the difference is the spend writer now sees the same org the check sees

Tests: a parametrized regression test for the backfill (backfills from team, pinned key org wins, no org anywhere stays None), a team-fetch-failure test asserting the backfill fails safe, and a CLI session-token test that mints a real token via get_cli_jwt_auth_token, decrypts it like the auth path does, and asserts it leaves the centralized checks with the team's org. The backfill test and the session-token test both fail without the fix

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

…t org_id

Keys attached to an org-linked team but minted without an organization_id
produced spend that was never credited to the org: the spend writer reads
user_api_key_dict.org_id with no team fallback, while the org budget check
resolves the org from the team. The check therefore ran against a counter
fed by almost none of the org's traffic and never tripped.

Backfill org_id from the freshly fetched team object in
_run_centralized_common_checks, per request only, so the spend writer and
the budget check read the same org. A key with an explicitly pinned org_id
always wins, and the cached key row is never mutated, so moving a team to
a different org takes effect on the next auth once the team cache
refreshes.
CLI session tokens from /sso/cli/poll are minted with a real team_id but
no org_id, and their auth path decrypts the blob without the combined_view
team join that fills org for DB keys. Spend from these tokens reached the
team but never the org, so org budgets never tripped. The regression test
mints a real CLI token, runs it through the centralized checks, and
asserts the credential leaves auth with the team's org.
@greptile-apps

greptile-apps Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR completes request identity with the organization linked to its resolved team when the credential has no explicit organization, with regression coverage for CLI session tokens, pinned organizations, missing organizations, and failed team fetches

Confidence Score: 5/5

The PR appears safe to merge

No blocking failure remains

Important Files Changed

Filename Overview
litellm/proxy/auth/user_api_key_auth.py Backfills a missing request-scoped organization identity from the resolved team while preserving an explicitly assigned organization
tests/test_litellm/proxy/auth/test_user_api_key_auth.py Adds focused regression coverage for organization derivation across direct, CLI-session, and team-fetch-failure scenarios

Reviews (2): Last reviewed commit: "test(proxy): cover CLI session-token org..." | Re-trigger Greptile

Comment thread litellm/proxy/auth/user_api_key_auth.py
@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_org_spend_team_org_backfill (22607bf) with litellm_internal_staging (7263aa0)1

Open in CodSpeed

Footnotes

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

@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@ryan-crabbe-berri
ryan-crabbe-berri merged commit 579f41d into litellm_internal_staging Jul 25, 2026
81 of 82 checks passed
@ryan-crabbe-berri
ryan-crabbe-berri deleted the litellm_org_spend_team_org_backfill branch July 25, 2026 01:20
yuneng-berri pushed a commit that referenced this pull request Jul 26, 2026
…thout org_id (#34577)

* fix(proxy): attribute spend to org for team-linked keys minted without org_id

Keys attached to an org-linked team but minted without an organization_id
produced spend that was never credited to the org: the spend writer reads
user_api_key_dict.org_id with no team fallback, while the org budget check
resolves the org from the team. The check therefore ran against a counter
fed by almost none of the org's traffic and never tripped.

Backfill org_id from the freshly fetched team object in
_run_centralized_common_checks, per request only, so the spend writer and
the budget check read the same org. A key with an explicitly pinned org_id
always wins, and the cached key row is never mutated, so moving a team to
a different org takes effect on the next auth once the team cache
refreshes.

* test(proxy): cover CLI session-token org backfill from team

CLI session tokens from /sso/cli/poll are minted with a real team_id but
no org_id, and their auth path decrypts the blob without the combined_view
team join that fills org for DB keys. Spend from these tokens reached the
team but never the org, so org budgets never tripped. The regression test
mints a real CLI token, runs it through the centralized checks, and
asserts the credential leaves auth with the team's org.

(cherry picked from commit 579f41d)
Ericcwang23 pushed a commit to Ericcwang23/litellm that referenced this pull request Jul 27, 2026
…thout org_id (BerriAI#34577)

* fix(proxy): attribute spend to org for team-linked keys minted without org_id

Keys attached to an org-linked team but minted without an organization_id
produced spend that was never credited to the org: the spend writer reads
user_api_key_dict.org_id with no team fallback, while the org budget check
resolves the org from the team. The check therefore ran against a counter
fed by almost none of the org's traffic and never tripped.

Backfill org_id from the freshly fetched team object in
_run_centralized_common_checks, per request only, so the spend writer and
the budget check read the same org. A key with an explicitly pinned org_id
always wins, and the cached key row is never mutated, so moving a team to
a different org takes effect on the next auth once the team cache
refreshes.

* test(proxy): cover CLI session-token org backfill from team

CLI session tokens from /sso/cli/poll are minted with a real team_id but
no org_id, and their auth path decrypts the blob without the combined_view
team join that fills org for DB keys. Spend from these tokens reached the
team but never the org, so org budgets never tripped. The regression test
mints a real CLI token, runs it through the centralized checks, and
asserts the credential leaves auth with the team's org.
yuneng-berri added a commit that referenced this pull request Jul 28, 2026
…x-d8c02a

chore(release): backport #33565, #33840, #33841, #34121, #33261, #34325 and #34577 to stable/1.93.x and cut 1.93.1
ap-anton-r-susilo pushed a commit to ap-anton-r-susilo/litellm that referenced this pull request Jul 29, 2026
…thout org_id (BerriAI#34577)

* fix(proxy): attribute spend to org for team-linked keys minted without org_id

Keys attached to an org-linked team but minted without an organization_id
produced spend that was never credited to the org: the spend writer reads
user_api_key_dict.org_id with no team fallback, while the org budget check
resolves the org from the team. The check therefore ran against a counter
fed by almost none of the org's traffic and never tripped.

Backfill org_id from the freshly fetched team object in
_run_centralized_common_checks, per request only, so the spend writer and
the budget check read the same org. A key with an explicitly pinned org_id
always wins, and the cached key row is never mutated, so moving a team to
a different org takes effect on the next auth once the team cache
refreshes.

* test(proxy): cover CLI session-token org backfill from team

CLI session tokens from /sso/cli/poll are minted with a real team_id but
no org_id, and their auth path decrypts the blob without the combined_view
team join that fills org for DB keys. Spend from these tokens reached the
team but never the org, so org budgets never tripped. The regression test
mints a real CLI token, runs it through the centralized checks, and
asserts the credential leaves auth with the team's org.

(cherry picked from commit 579f41d)
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