Skip to content

feat(auth): source request logging + spend identity from the resolved Principal - #31006

Open
yassin-berriai wants to merge 3 commits into
litellm_internal_stagingfrom
litellm_principal_logging_identity
Open

feat(auth): source request logging + spend identity from the resolved Principal#31006
yassin-berriai wants to merge 3 commits into
litellm_internal_stagingfrom
litellm_principal_logging_identity

Conversation

@yassin-berriai

@yassin-berriai yassin-berriai commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Linear ticket

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 unit tests on make test-unit
  • 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

CI (LiteLLM team)

  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Screenshots / Proof of Fix

Verified against a live proxy on localhost:4000 backed by Postgres, hitting the real Anthropic API, running this branch with PYTHONPATH pinned to the worktree so the resolver path and this change are the code under test. Identity is resolved once at the seam into request.state.principal; logging and spend now source caller identity off it.

Set up an organization, a team in it, a user on the team, and a team-scoped key:

ORG_ID  = 78153153-e28f-4a02-a68f-c7d04a35b0b4
TEAM_ID = c1014da1-8e61-4ae8-919b-98ef76fb62b8
USER_ID = a745550f-bf11-424e-817b-dd7e39faea53
key     = sk-... (team_id + user_id, model anthropic-haiku-4-5)

Two real chat completions on that key, the second carrying an end-user:

$ curl -s localhost:4000/v1/chat/completions -H "Authorization: Bearer sk-..." \
    -H "Content-Type: application/json" \
    -d '{"model":"anthropic-haiku-4-5","messages":[{"role":"user","content":"say hi in exactly 3 words"}],"max_tokens":20}'
# -> "Hi there friend"

$ curl -s localhost:4000/v1/chat/completions -H "Authorization: Bearer sk-..." \
    -H "Content-Type: application/json" \
    -d '{"model":"anthropic-haiku-4-5","messages":[{"role":"user","content":"reply with the single word ok"}],"max_tokens":10,"user":"customer-xyz"}'
# -> "ok"

The spend rows carry the identity resolved at the seam: the key's user, team, and org on both rows, and the end-user only on the request that sent one:

$ psql -c 'select substring(request_id,1,22) request_id, "user" user_id, team_id, organization_id,
           coalesce(end_user,'"'"'-'"'"') end_user from "LiteLLM_SpendLogs" order by "startTime" desc limit 2;'

 chatcmpl-2a5f5e0b-ba57 | a745550f-...-dd7e39faea53 | c1014da1-...-98ef76fb62b8 | 78153153-...-c7d04a35b0b4 | customer-xyz
 chatcmpl-14de440a-0fc2 | a745550f-...-dd7e39faea53 | c1014da1-...-98ef76fb62b8 | 78153153-...-c7d04a35b0b4 | -

No Principal projection at auth seam failed warnings and no cost-tracking errors in the proxy log, and both completions returned 200. Because the Principal is projected from the same resolved key object today, the values match the pre-change build, so there is no regression; the behavioral guarantee that identity now follows the Principal rather than the key is pinned by the unit tests, where a Principal whose ids differ from the key drives the logged metadata

Type

🆕 New Feature

Changes

This is Phase 1 of the internal Caller Identity design ("Resolve Once, Consume Everywhere"); it is the first downstream consumer wiring of the Principal value type introduced in Phase 0 (PR #30887). Phase 0 resolves one Principal per request at the auth seam and projects it onto request.state.principal, but nothing downstream reads it yet, so request logging and spend attribution still derive caller identity ad hoc from the UserAPIKeyAuth key object. That leaves identity in logs and spend potentially inconsistent with the identity resolved at the seam

get_sanitized_user_information_from_key and add_user_api_key_auth_to_request_metadata in litellm/proxy/litellm_pre_call_utils.py now take an explicit, strongly-typed optional principal: Optional[Principal]. When a Principal is present, the logged identity fields (user_api_key_user_id, user_api_key_user_email, user_api_key_team_id/user_api_key_team_alias, user_api_key_org_id/user_api_key_org_alias, user_api_key_project_id/user_api_key_project_alias, user_api_key_end_user_id) come from the Principal; non-identity fields (hash, alias, spend, budgets, request route, auth metadata) keep coming from the key object. When principal is None, behavior is byte-for-byte unchanged. add_litellm_data_to_request threads getattr(request.state, "principal", None) down to the chokepoint; the success spend path inherits the principal-sourced identity automatically because it reads the metadata that pre-call stuffs. Other callers that have no request principal handy pass principal=None and keep current behavior

Sourcing is principal-first with per-field key fallback, so a Principal that is present but missing a sub-model (for example no user) never drops an id the key actually carries; it falls back to the key for that field only. Tests in tests/test_litellm/proxy/test_litellm_pre_call_utils.py assert that with a Principal whose user, team, org, project, and end-user identifiers differ from the key object's, the written metadata carries the Principal's values; that with principal=None the metadata falls back to the key object's values; and that a Principal present but missing sub-models falls back per field rather than nulling those ids

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@codecov

codecov Bot commented Jun 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@yassin-berriai
yassin-berriai force-pushed the litellm_principal_logging_identity branch from ef8ce8b to 54ec649 Compare June 22, 2026 20:02
@yassin-berriai
yassin-berriai marked this pull request as ready for review June 22, 2026 20:08
@yassin-berriai

Copy link
Copy Markdown
Contributor Author

@greptileai

@greptile-apps

greptile-apps Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR (Phase 1 of "Resolve Once, Consume Everywhere") wires the Principal value type—resolved once per request at the auth seam in Phase 0—into request logging and spend attribution, replacing ad-hoc key-object reads for identity fields. The change is fully backward-compatible: the principal parameter defaults to None everywhere it is added, so callers without a principal (e.g. the MCP server) keep existing behavior unchanged.

  • get_sanitized_user_information_from_key and add_user_api_key_auth_to_request_metadata now accept Optional[Principal]; when present, identity fields (user_id, team_id, org_id, project_id, end_user_id) are sourced from the Principal with per-sub-model fallback to the key object.
  • add_headers_to_llm_call and add_headers_to_llm_call_by_model_group receive the same principal, so forwarded x-litellm-* headers now stay consistent with spend logs rather than diverging.
  • Five new unit tests exercise principal-sourced identity, full key fallback, per-field partial fallback, and header forwarding parity.

Confidence Score: 5/5

Safe to merge — all new parameters default to None so existing call sites are unaffected, and the new identity-sourcing path is exercised by five focused unit tests.

The change is purely additive: new optional parameters with None defaults, no mutation of existing call sites that don't opt in, and no modification to auth, budget, or rate-limit logic. The test suite covers the three meaningful states (principal present, absent, and partially populated), and live-proxy verification in the PR description confirms spend rows carry the expected identity. No correctness bugs were found in the changed paths.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/litellm_pre_call_utils.py Adds optional principal: Optional[Principal] to four methods and threads request.state.principal to both chokepoints in add_litellm_data_to_request. Per-sub-model fallback to key is safe; all signatures are backward-compatible.
tests/test_litellm/proxy/test_litellm_pre_call_utils.py Adds five new unit tests covering principal-sourced identity, key fallback without principal, per-field partial fallback, and header forwarding parity. Existing mock assertions updated to match the new principal=None default; coverage is not weakened.

Reviews (2): Last reviewed commit: "feat(auth): source request logging + spe..." | Re-trigger Greptile

@yassin-berriai
yassin-berriai force-pushed the litellm_principal_logging_identity branch from 54ec649 to 140575c Compare June 22, 2026 20:20
@yassin-berriai

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Closed the forwarded-header gap in 140575c: add_headers_to_llm_call now takes the same Optional[Principal] and threads it through add_headers_to_llm_call_by_model_group from add_litellm_data_to_request, so the x-litellm-* identity headers forwarded to the provider follow the Principal exactly like the logged identity, with per-field key fallback when the principal is absent. Two tests cover the principal-sourced headers and the key fallback.

On the embedded user_api_key_auth object: that one is intentional for Phase 1. The key object stays the carrier for budget, rate-limit, and policy state (and MCP access control), so it deliberately keeps key-sourced identity; reconciling identity reads off the embedded object belongs to Phase 2, when the authenticators normalize identity into the Principal and those consumers migrate to the flat principal-sourced fields. Tracking it there

@yassin-berriai

Copy link
Copy Markdown
Contributor Author

@greptileai

…itellm_principal_logging_identity

# Conflicts:
#	litellm/proxy/litellm_pre_call_utils.py
@codspeed-hq

codspeed-hq Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_principal_logging_identity (e161b32) with litellm_internal_staging (442fdc1)

Open in CodSpeed

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