Skip to content

fix(proxy): backport #40541, #43642, and #43656 to stable/1.103.x for v1.103.2 - #43897

Merged
mateo-berri merged 7 commits into
stable/1.103.xfrom
litellm_backport_usage_attribution_stable_1_103_x
Sep 30, 2026
Merged

mateo-berri merged 7 commits into
stable/1.103.xfrom
litellm_backport_usage_attribution_stable_1_103_x

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

How it solves it:

  • Cherry-picks the three PRs onto stable/1.103.x, one commit per PR, original authors kept
  • Bumps the line to 1.103.2 so it can be cut as v1.103.2

User Flow

Before: a BI job pulling daily usage from a v1.103.0 gateway gets hashed CLI keys with no owner, so its per-user cost report is dirty

  1. A developer runs litellm-proxy login, gets a session token, and sends chat requests through the gateway all day
  2. The BI job sends GET https://litellm-domain/user/daily/activity/aggregated?start_date=2026-09-28&end_date=2026-09-28 with an admin key
  3. The 200 response lists that developer's spend in results[].breakdown.api_keys under a 64-character hash, and on a day where that hash has more spend logs than a 5-second lookup can scan, its "key_alias" and "user_email" come back null while the gateway log says canceling statement due to statement timeout
  4. The BI report shows the day's spend under the hash, attributable to nobody

After: the same pull names the user behind every CLI session key, old hashes included

  1. The admin upgrades the gateway to v1.103.2 and restarts it
  2. The developer runs litellm-proxy login, gets a session token, and sends chat requests through the gateway all day
  3. The BI job sends the same GET https://litellm-domain/user/daily/activity/aggregated?start_date=2026-09-28&end_date=2026-09-28 with an admin key
  4. New sessions are listed under cli-session-<user_id> with key_alias and user_email filled, and hashes written before the upgrade come back with user_email from the user their spend rows name and key_alias from their spend logs, however many spend logs the hash has that day
  5. The BI report attributes the day's spend to the developer

Backport checklist

Notes for the reviewer:

Pre-Submission checklist

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

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • 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)

Screenshots / Proof of Fix

Last updated: fe87252. The integration accounting run below is at fe87252; QA and /live-pr-risk ran on 1bbc9ee, the last commit with a product diff (fe87252 only bumps the litellm-enterprise version and pin)

Two live proxies booted from this worktree, no mocks, real gpt-5.6 calls through OpenAI. Before on :4131 from the line's tip a32e70a (a detached worktree at that commit, put first on the import path), After on :4577 from this PR's tip 1bbc9ee. Each runs --num_workers 2, both share one Postgres 18 database (no Redis), one gpt-5.6 deployment, PROXY_BATCH_WRITE_AT=5. The developer cli-user-1 was created with POST /user/new, and the CLI session token was minted with ExperimentalUIJWTToken.get_cli_jwt_auth_token(user_info), the same function GET /sso/cli/poll/{key_id} hands litellm-proxy login (SSO is not wired locally). To match the customer's volume, the day's spend logs under that developer's hash were seeded to 800,000 rows, every one a clone of a real chat completion's spend row with its full 3.3 KB metadata as the proxy writes it (so it is TOASTed out of line like production rows); the line's whole-window scan over them took 46 s and 111 s in two timed passes right after seeding, far past the hardcoded 5 s statement timeout, which is the customer's condition. Both proxies were restarted after seeding so no in-memory key metadata cache carries over, both worktrees read the same .env (the enterprise license is what makes /organization/daily/activity answer 200), and both legs ran the same commands in the same order. The Before leg was re-run after the base worktree got that .env, which is why its timestamp is later than the After leg's

Integration accounting group, merge base vs head

Merge base a32e70a and head fe87252, each booted as its own proxy on its own database (litellm_slp427_audit_base on :42526 with Redis :35707 and the scripted upstream on :28361, litellm_slp427_audit_head on :39003 with Redis :50663 and the upstream on :45689, PostgreSQL 18 with every migration applied at boot), ran the same 85 cells of tests/integration/run.py accounting (the pricing and spend groups of tests/integration/contracts.json) with --seed 4106601 --order-seed 0. The rig is the CircleCI shape from .circleci/scripts/run_integration.sh: the proxy is python -m integration._support.proxy --config tests/integration/proxy_config.yaml --num_workers 1 --use_prisma_db_push --enforce_prisma_migration_check in production mode with STORE_MODEL_IN_DB=True, LITELLM_LOCAL_MODEL_COST_MAP=True and router_settings.num_retries set to 0 over POST /config/update before the run, the base tree first on the import path for its leg, nothing mocked inside the proxy beyond the shim's route entitlement that CI grants too. Base (started 2026-09-30T22:46:41Z): 57 failed, 28 passed, 71 warnings in 550.74 s, 28 green and 57 red of 85 collected. Head (started 2026-09-30T22:56:33Z): 85 passed, 71 warnings in 702.08 s, 85 of 85 collected, no skip and no retry (-p no:pytest-retry -p no:rerunfailures). The 57 cells red on base are the behavior the three picks change and nothing else: 22 assert the owner that #43642 recovers from the daily spend rows, 30 are the alias-probe cells that assert that same owner next to the alias (every key_metadata() expectation in test_daily_activity_key_alias_probes.py carries it, so those cells are red on base with the alias itself already recovered where the cell expects it), 2 are the two alias window edges #43656 changes deliberately (its two Low caveats, called deliberate by mateo-berri in #43656 (comment) and #43656 (comment)), 1 is the CLI session alias #40541 writes, and 2 are the team-next-to-owner cells whose base outcome the inventory left open. Two earlier runs were retired, not counted, and neither touched the product: a base leg that booted the proxy through proxy_cli.py rather than CI's shim, so POST /key/regenerate answered as unlicensed (that cell is green on the base leg above) and /organization/daily/activity answered 403 before reaching the owner assertion, every other cell with the outcome above; and a head run started next to that base leg on a loaded box, where one cell's owned proxy missed the 70 s readiness deadline of tests/integration/_support/process.py and the other 84 cells passed, after which the head run above ran alone on a fresh database. A second head run with the same seeds (started 2026-09-30T23:11:45Z) was stopped at 52% with 45 cells passed and none failed when the full /audit was waived for this PR: it backports three PRs that already passed their review gates on main, so the runs above are recorded as the integration proof they are, not as an audit verdict. Cherry-pick fidelity: litellm/proxy/spend_tracking/key_metadata_recovery.py at this head differs from main's merge of #43656 only by two main-only refactors this line never took (the find_many_in chunking helper from #42629 and a predicate pulled into _user_id_needing_details), and the enterprise wheel built from this head is litellm_enterprise-0.1.69.post1-py3-none-any.whl carrying _get_key_alias in check_batch_cost.py and get_logged_api_key in managed_files.py

Matrix, one row per cell
# Cell (test node id) Pick Expected base Base a32e70a Head fe87252 Verdict
1 tests/integration/pricing/test_configured_prices.py::test_custom_price_is_reported_and_charged untouched or same behavior on both green green green PASS
2 tests/integration/pricing/test_configured_prices.py::test_default_prices_survive_nullable_sibling_and_reload untouched or same behavior on both green green green PASS
3 tests/integration/pricing/test_configured_prices.py::test_loaded_router_preserves_cached_defaults_during_real_requests untouched or same behavior on both green green green PASS
4 tests/integration/pricing/test_off_peak_pricing.py::test_closed_off_peak_window_bills_standard_rates untouched or same behavior on both green green green PASS
5 tests/integration/pricing/test_off_peak_pricing.py::test_open_off_peak_window_bills_off_peak_rates untouched or same behavior on both green green green PASS
6 tests/integration/pricing/test_price_precedence.py::test_generated_zero_null_and_omitted_prices_follow_independent_arithmetic untouched or same behavior on both green green green PASS
7 tests/integration/pricing/test_price_precedence.py::test_same_upstream_aliases_keep_distinct_prices_after_reload untouched or same behavior on both green green green PASS
8 tests/integration/spend/test_cache_and_quota.py::test_different_system_messages_do_not_share_a_cached_response untouched or same behavior on both green green green PASS
9 tests/integration/spend/test_cache_and_quota.py::test_generated_cache_sequences_preserve_content_usage_and_zero_hit_cost untouched or same behavior on both green green green PASS
10 tests/integration/spend/test_cache_and_quota.py::test_key_budget_at_boundary_blocks_provider_then_explicit_reset_restores untouched or same behavior on both green green green PASS
11 tests/integration/spend/test_cache_and_quota.py::test_repeated_hits_keep_response_identity_and_create_distinct_zero_cost_rows untouched or same behavior on both green green green PASS
12 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_found_once_is_served_from_the_cache_for_the_same_window_only untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
13 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_logged_after_a_cached_miss_shows_once_the_miss_expires untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
14 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_lookup_gives_up_while_spend_logs_are_locked_and_answers_once_they_are_not untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
15 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_named_only_by_a_spend_log_is_reported_on_every_daily_activity_route[agent_daily_activity] untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
16 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_named_only_by_a_spend_log_is_reported_on_every_daily_activity_route[customer_daily_activity] untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
17 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_named_only_by_a_spend_log_is_reported_on_every_daily_activity_route[end_user_daily_activity] untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
18 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_named_only_by_a_spend_log_is_reported_on_every_daily_activity_route[organization_daily_activity] untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
19 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_named_only_by_a_spend_log_is_reported_on_every_daily_activity_route[tag_daily_activity] untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
20 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_named_only_by_a_spend_log_is_reported_on_every_daily_activity_route[team_daily_activity] untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
21 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_named_only_by_a_spend_log_is_reported_on_every_daily_activity_route[team_daily_activity_aggregated] untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
22 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_named_only_by_a_spend_log_is_reported_on_every_daily_activity_route[user_daily_activity] untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
23 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_named_only_by_a_spend_log_is_reported_on_every_daily_activity_route[user_daily_activity_aggregated] untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
24 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_named_only_in_the_middle_of_two_hundred_nameless_rows_is_not_picked_up #43656 red (deliberate behavior change, Low caveat) red green PASS
25 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_of_an_unexpected_shape_is_reported_as_postgres_renders_it[five_kb_string] untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
26 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_of_an_unexpected_shape_is_reported_as_postgres_renders_it[json_int] untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
27 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_of_an_unexpected_shape_is_reported_as_postgres_renders_it[json_list] untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
28 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_on_an_edge_of_the_window_is_reported_whatever_surrounds_it[100_nameless_named_99_nameless] untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
29 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_on_an_edge_of_the_window_is_reported_whatever_surrounds_it[99_nameless_named_100_nameless] untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
30 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_on_an_edge_of_the_window_is_reported_whatever_surrounds_it[both_edges_named_150_nameless_between] untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
31 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_on_an_edge_of_the_window_is_reported_whatever_surrounds_it[named_between_50_and_50_nameless] untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
32 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_on_an_edge_of_the_window_is_reported_whatever_surrounds_it[newest_named_150_nameless_older] untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
33 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_on_an_edge_of_the_window_is_reported_whatever_surrounds_it[oldest_named_150_nameless_newer] untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
34 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_concurrent_reads_over_every_route_all_name_a_fresh_key untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
35 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_hashed_jwt_digest_is_named_by_its_spend_log untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
36 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_key_renamed_and_renamed_back_is_reported_with_the_alias_on_both_edges #43656 red (deliberate behavior change, Low caveat) red green PASS
37 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_rows_without_a_usable_alias_do_not_hide_the_named_row_after_them[array_then_string_metadata] untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
38 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_rows_without_a_usable_alias_do_not_hide_the_named_row_after_them[empty_string_alias] untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
39 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_spend_log_names_the_key_only_from_one_day_before_to_two_days_after_the_read[first_second_after_the_window] untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
40 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_spend_log_names_the_key_only_from_one_day_before_to_two_days_after_the_read[first_second_of_the_window] untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
41 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_spend_log_names_the_key_only_from_one_day_before_to_two_days_after_the_read[last_second_of_the_window] untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
42 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_spend_log_names_the_key_only_from_one_day_before_to_two_days_after_the_read[second_before_the_window] untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
43 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_team_named_only_by_a_spend_log_is_reported_next_to_the_daily_owner[team_id_column] #43642 + #43656 red or green (whole-window alias scan on the line, no daily owner) red green PASS
44 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_team_named_only_by_a_spend_log_is_reported_next_to_the_daily_owner[team_id_in_metadata] #43642 + #43656 red or green (whole-window alias scan on the line, no daily owner) red green PASS
45 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_two_aliases_on_the_two_edges_leave_the_key_unnamed untouched or same behavior on both red (#43642: the cell also asserts the daily-row owner next to the alias, which the base cannot recover) red green PASS
46 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_user_named_by_a_spend_log_beats_the_owner_the_daily_rows_name[user_column] #43642 + #43656 red or green (whole-window alias scan on the line, no daily owner) green green PASS
47 tests/integration/spend/test_daily_activity_key_alias_probes.py::test_user_named_by_a_spend_log_beats_the_owner_the_daily_rows_name[user_id_in_metadata] #43642 + #43656 red or green (whole-window alias scan on the line, no daily owner) green green PASS
48 tests/integration/spend/test_daily_activity_key_owner.py::test_daily_spend_rows_naming_no_user_do_not_hide_the_one_user_the_others_name[blank_user] #43642 red (no owner recovery on the line) red green PASS
49 tests/integration/spend/test_daily_activity_key_owner.py::test_daily_spend_rows_naming_no_user_do_not_hide_the_one_user_the_others_name[null_user] #43642 red (no owner recovery on the line) red green PASS
50 tests/integration/spend/test_daily_activity_key_owner.py::test_deleted_key_keeps_its_own_user_when_its_daily_spend_names_another untouched or same behavior on both green green green PASS
51 tests/integration/spend/test_daily_activity_key_owner.py::test_deleted_key_with_no_user_keeps_its_alias_and_gains_the_one_user_its_daily_spend_names #43642 red (no owner recovery on the line) red green PASS
52 tests/integration/spend/test_daily_activity_key_owner.py::test_key_missing_from_the_key_tables_is_reported_with_the_one_user_its_daily_spend_names[agent_daily_activity] #43642 red (no owner recovery on the line) red green PASS
53 tests/integration/spend/test_daily_activity_key_owner.py::test_key_missing_from_the_key_tables_is_reported_with_the_one_user_its_daily_spend_names[customer_daily_activity] #43642 red (no owner recovery on the line) red green PASS
54 tests/integration/spend/test_daily_activity_key_owner.py::test_key_missing_from_the_key_tables_is_reported_with_the_one_user_its_daily_spend_names[end_user_daily_activity] #43642 red (no owner recovery on the line) red green PASS
55 tests/integration/spend/test_daily_activity_key_owner.py::test_key_missing_from_the_key_tables_is_reported_with_the_one_user_its_daily_spend_names[organization_daily_activity] #43642 red (no owner recovery on the line) red green PASS
56 tests/integration/spend/test_daily_activity_key_owner.py::test_key_missing_from_the_key_tables_is_reported_with_the_one_user_its_daily_spend_names[tag_daily_activity] #43642 red (no owner recovery on the line) red green PASS
57 tests/integration/spend/test_daily_activity_key_owner.py::test_key_missing_from_the_key_tables_is_reported_with_the_one_user_its_daily_spend_names[team_daily_activity] #43642 red (no owner recovery on the line) red green PASS
58 tests/integration/spend/test_daily_activity_key_owner.py::test_key_missing_from_the_key_tables_is_reported_with_the_one_user_its_daily_spend_names[team_daily_activity_aggregated] #43642 red (no owner recovery on the line) red green PASS
59 tests/integration/spend/test_daily_activity_key_owner.py::test_key_missing_from_the_key_tables_is_reported_with_the_one_user_its_daily_spend_names[user_daily_activity] #43642 red (no owner recovery on the line) red green PASS
60 tests/integration/spend/test_daily_activity_key_owner.py::test_key_missing_from_the_key_tables_is_reported_with_the_one_user_its_daily_spend_names[user_daily_activity_aggregated] #43642 red (no owner recovery on the line) red green PASS
61 tests/integration/spend/test_daily_activity_key_owner.py::test_key_named_only_by_a_spend_log_alias_keeps_that_alias_and_gains_the_one_user_its_daily_spend_names #43642 red (no owner recovery on the line) red green PASS
62 tests/integration/spend/test_daily_activity_key_owner.py::test_key_whose_daily_spend_names_no_user_at_all_is_reported_with_no_owner untouched or same behavior on both green green green PASS
63 tests/integration/spend/test_daily_activity_key_owner.py::test_key_whose_daily_spend_names_two_users_is_reported_with_no_owner untouched or same behavior on both green green green PASS
64 tests/integration/spend/test_daily_activity_key_owner.py::test_live_key_keeps_its_own_user_when_its_daily_spend_names_another untouched or same behavior on both green green green PASS
65 tests/integration/spend/test_daily_activity_key_owner.py::test_live_key_with_no_user_is_not_given_the_user_its_daily_spend_names untouched or same behavior on both green green green PASS
66 tests/integration/spend/test_daily_activity_key_owner.py::test_owner_the_user_table_does_not_hold_is_reported_by_id_with_no_email #43642 red (no owner recovery on the line) red green PASS
67 tests/integration/spend/test_daily_activity_key_owner_faults.py::test_every_key_of_a_team_is_reported_with_its_own_user #43642 red (no owner recovery on the line) red green PASS
68 tests/integration/spend/test_daily_activity_key_owner_faults.py::test_five_kilobyte_key_is_reported_with_the_one_user_its_daily_spend_names #43642 red (no owner recovery on the line) red green PASS
69 tests/integration/spend/test_daily_activity_key_owner_faults.py::test_invalid_key_is_refused_without_naming_the_owner untouched or same behavior on both green green green PASS
70 tests/integration/spend/test_daily_activity_key_owner_faults.py::test_key_stops_being_reported_with_an_owner_once_a_second_user_spends_with_it #43642 red (no owner recovery on the line) red green PASS
71 tests/integration/spend/test_daily_activity_key_owner_faults.py::test_key_with_no_daily_spend_is_reported_as_no_activity untouched or same behavior on both green green green PASS
72 tests/integration/spend/test_daily_activity_key_owner_faults.py::test_owner_is_reported_again_after_the_proxy_restarts #43642 red (no owner recovery on the line) red green PASS
73 tests/integration/spend/test_daily_activity_key_owner_faults.py::test_owner_is_reported_while_a_worker_is_killed_and_after_it_is_replaced #43642 red (no owner recovery on the line) red green PASS
74 tests/integration/spend/test_daily_activity_key_owner_faults.py::test_owner_lookup_gives_up_while_daily_user_spend_is_locked_and_answers_once_it_is_not #43642 red (no owner recovery on the line) red green PASS
75 tests/integration/spend/test_daily_activity_key_owner_faults.py::test_reading_the_same_activity_twice_gives_the_same_answer untouched or same behavior on both green green green PASS
76 tests/integration/spend/test_daily_activity_key_owner_faults.py::test_user_reading_a_key_only_another_user_spent_with_is_shown_nothing_of_it untouched or same behavior on both green green green PASS
77 tests/integration/spend/test_daily_activity_key_owner_faults.py::test_user_reading_a_key_only_they_spent_with_is_shown_themselves_as_its_owner #43642 red (no owner recovery on the line) red green PASS
78 tests/integration/spend/test_daily_activity_key_owner_faults.py::test_user_reading_a_key_shared_with_another_user_is_shown_no_owner_and_nothing_of_the_other_user untouched or same behavior on both green green green PASS
79 tests/integration/spend/test_daily_activity_key_owner_traffic.py::test_cli_session_spend_is_reported_with_the_user_and_team_of_the_session #40541 red (session spend written under the hash) red green PASS
80 tests/integration/spend/test_daily_activity_key_owner_traffic.py::test_key_purged_from_the_key_tables_is_reported_with_the_alias_its_spend_logs_name untouched or same behavior on both green green green PASS
81 tests/integration/spend/test_daily_activity_key_owner_traffic.py::test_key_used_on_every_unified_endpoint_is_reported_with_its_own_alias_and_user untouched or same behavior on both green green green PASS
82 tests/integration/spend/test_daily_activity_key_owner_traffic.py::test_owner_is_reported_on_every_route_while_a_burst_of_requests_waits_on_the_provider #43642 red (no owner recovery on the line) red green PASS
83 tests/integration/spend/test_daily_activity_key_owner_traffic.py::test_usage_ai_chat_hands_the_model_the_usage_summary_without_any_key_owner untouched or same behavior on both green green green PASS
84 tests/integration/spend/test_filtered_ledger.py::test_rotated_keys_users_and_model_groups_preserve_success_failure_cache_ledger untouched or same behavior on both green green green PASS
85 tests/integration/spend/test_team_member_spend_flush.py::test_fractional_member_spend_lands_after_a_whole_number_flush_on_the_same_connection untouched or same behavior on both green green green PASS
### Before (a32e70a, :4131)
## before leg on :4131, 2026-09-30T21:49:00Z
$ curl -s http://localhost:4131/health/readiness | jq -c '{status, db}'
{"status":"healthy","db":"connected"}
$ curl -s 'http://localhost:4131/user/info?user_id=cli-user-1' -H 'Authorization: Bearer $LITELLM_MASTER_KEY' | jq -c '{user_id: .user_info.user_id, user_email: .user_info.user_email, keys: (.keys | length)}'
{"user_id":"cli-user-1","user_email":"cli-user-1@example.com","keys":0}
$ curl -s -D chat.h http://localhost:4131/v1/chat/completions -H 'Authorization: Bearer $CLI_TOKEN' -H 'Content-Type: application/json' -d '{"model":"gpt-5.6","messages":[{"role":"user","content":"Say hi"}]}' | jq -c '{model, content: .choices[0].message.content}'
{"model":"gpt-5.6","content":"Hi!"}
$ curl -s -D messages.h http://localhost:4131/v1/messages -H 'Authorization: Bearer $CLI_TOKEN' -H 'anthropic-version: 2023-06-01' -H 'Content-Type: application/json' -d '{"model":"gpt-5.6","max_tokens":32,"messages":[{"role":"user","content":"Say hi"}]}' | jq -c '{model, text: .content[0].text}'
{"model":"gpt-5.6","text":"Hi!"}
$ curl -s -D responses.h http://localhost:4131/v1/responses -H 'Authorization: Bearer $CLI_TOKEN' -H 'Content-Type: application/json' -d '{"model":"gpt-5.6","input":"Say hi"}' | jq -c '{model, text: .output[-1].content[0].text}'
{"model":"gpt-5.6","text":"Hi!"}
$ sleep 12  # PROXY_BATCH_WRITE_AT=5, let the spend writer flush
$ curl -s "http://localhost:4131/spend/logs?request_id=$(grep -i x-litellm-call-id chat.h | cut -d' ' -f2)" -H 'Authorization: Bearer $LITELLM_MASTER_KEY' | jq -c '.[0] | {call_type, api_key, user}'
{"call_type":"acompletion","api_key":"eb71a299ac70b764b90f1488bdd2d7ef9aaca220c171b1757618193a97eff198","user":"cli-user-1"}
$ curl -s "http://localhost:4131/spend/logs?request_id=$(grep -i x-litellm-call-id messages.h | cut -d' ' -f2)" -H 'Authorization: Bearer $LITELLM_MASTER_KEY' | jq -c '.[0] | {call_type, api_key, user}'
{"call_type":"anthropic_messages","api_key":"eb71a299ac70b764b90f1488bdd2d7ef9aaca220c171b1757618193a97eff198","user":"cli-user-1"}
$ curl -s "http://localhost:4131/spend/logs?request_id=$(grep -i x-litellm-call-id responses.h | cut -d' ' -f2)" -H 'Authorization: Bearer $LITELLM_MASTER_KEY' | jq -c '.[0] | {call_type, api_key, user}'
{"call_type":"aresponses","api_key":"eb71a299ac70b764b90f1488bdd2d7ef9aaca220c171b1757618193a97eff198","user":"cli-user-1"}
$ curl -s -o aggregated.json -w 'HTTP %{http_code} in %{time_total}s
' "http://localhost:4131/user/daily/activity/aggregated?start_date=2026-09-30&end_date=2026-09-30" -H 'Authorization: Bearer $LITELLM_MASTER_KEY'; jq '.results[].breakdown.api_keys | map_values({metadata, api_requests: .metrics.api_requests, spend: .metrics.spend})' aggregated.json
HTTP 200 in 5.200489s
{
  "eb71a299ac70b764b90f1488bdd2d7ef9aaca220c171b1757618193a97eff198": {
    "metadata": {
      "key_alias": null,
      "team_id": null,
      "user_id": null,
      "user_email": null,
      "key_exists": false
    },
    "api_requests": 13,
    "spend": 0.001876
  },
  "cli-session-cli-user-1": {
    "metadata": {
      "key_alias": null,
      "team_id": null,
      "user_id": null,
      "user_email": null,
      "key_exists": false
    },
    "api_requests": 11,
    "spend": 0.001572
  }
}
$ curl -s "http://localhost:4131/user/daily/activity/aggregated?start_date=2026-09-30&end_date=2026-09-30" -H 'Authorization: Bearer $CLI_TOKEN' | jq -c '.results[].breakdown.api_keys | keys'  # the developer's own view through the session token
["cli-session-cli-user-1","eb71a299ac70b764b90f1488bdd2d7ef9aaca220c171b1757618193a97eff198"]
## pass-through
$ curl -s -D passthrough.h http://localhost:4131/openai/v1/chat/completions -H 'Authorization: Bearer $CLI_TOKEN' -H 'Content-Type: application/json' -d '{"model":"gpt-5.6","messages":[{"role":"user","content":"Say hi"}]}' | jq -c '{model, content: .choices[0].message.content}'
{"model":"gpt-5.6-sol","content":"Hi!"}
$ sleep 12
$ curl -s "http://localhost:4131/spend/logs?request_id=$(grep -i x-litellm-call-id passthrough.h | cut -d' ' -f2)" -H 'Authorization: Bearer $LITELLM_MASTER_KEY' | jq -c '.[0] | {call_type, api_key, user}'
{"call_type":"pass_through_endpoint","api_key":"eb71a299ac70b764b90f1488bdd2d7ef9aaca220c171b1757618193a97eff198","user":"cli-user-1"}
## other daily activity routes
GET /user/daily/activity -> 200 0.062945s keys=[{"key":"cli-session-","alias":null,"user":null,"email":null},{"key":"eb71a299ac70","alias":null,"user":null,"email":null}]
GET /team/daily/activity -> 200 0.020319s keys=[{"key":"cli-session-","alias":null,"user":null,"email":null},{"key":"eb71a299ac70","alias":null,"user":null,"email":null}]
GET /team/daily/activity/aggregated -> 200 0.072524s keys=[{"key":"cli-session-","alias":null,"user":null,"email":null},{"key":"eb71a299ac70","alias":null,"user":null,"email":null}]
GET /tag/daily/activity -> 200 0.030992s keys=[{"key":"cli-session-","alias":null,"user":null,"email":null},{"key":"eb71a299ac70","alias":null,"user":null,"email":null}]
GET /organization/daily/activity -> 200 0.070272s keys=[]
GET /customer/daily/activity -> 200 0.013530s keys=[]
GET /end_user/daily/activity -> 200 0.012483s keys=[]
GET /agent/daily/activity -> 200 0.133894s keys=[]
## done 2026-09-30T21:49:43Z

After (1bbc9ee, :4577)

## after leg on :4577, 2026-09-30T21:46:36Z
$ curl -s http://localhost:4577/health/readiness | jq -c '{status, db}'
{"status":"healthy","db":"connected"}
$ curl -s 'http://localhost:4577/user/info?user_id=cli-user-1' -H 'Authorization: Bearer $LITELLM_MASTER_KEY' | jq -c '{user_id: .user_info.user_id, user_email: .user_info.user_email, keys: (.keys | length)}'
{"user_id":"cli-user-1","user_email":"cli-user-1@example.com","keys":0}
$ curl -s -D chat.h http://localhost:4577/v1/chat/completions -H 'Authorization: Bearer $CLI_TOKEN' -H 'Content-Type: application/json' -d '{"model":"gpt-5.6","messages":[{"role":"user","content":"Say hi"}]}' | jq -c '{model, content: .choices[0].message.content}'
{"model":"gpt-5.6","content":"Hi!"}
$ curl -s -D messages.h http://localhost:4577/v1/messages -H 'Authorization: Bearer $CLI_TOKEN' -H 'anthropic-version: 2023-06-01' -H 'Content-Type: application/json' -d '{"model":"gpt-5.6","max_tokens":32,"messages":[{"role":"user","content":"Say hi"}]}' | jq -c '{model, text: .content[0].text}'
{"model":"gpt-5.6","text":"Hi!"}
$ curl -s -D responses.h http://localhost:4577/v1/responses -H 'Authorization: Bearer $CLI_TOKEN' -H 'Content-Type: application/json' -d '{"model":"gpt-5.6","input":"Say hi"}' | jq -c '{model, text: .output[-1].content[0].text}'
{"model":"gpt-5.6","text":"Hi!"}
$ sleep 12  # PROXY_BATCH_WRITE_AT=5, let the spend writer flush
$ curl -s "http://localhost:4577/spend/logs?request_id=$(grep -i x-litellm-call-id chat.h | cut -d' ' -f2)" -H 'Authorization: Bearer $LITELLM_MASTER_KEY' | jq -c '.[0] | {call_type, api_key, user}'
{"call_type":"acompletion","api_key":"cli-session-cli-user-1","user":"cli-user-1"}
$ curl -s "http://localhost:4577/spend/logs?request_id=$(grep -i x-litellm-call-id messages.h | cut -d' ' -f2)" -H 'Authorization: Bearer $LITELLM_MASTER_KEY' | jq -c '.[0] | {call_type, api_key, user}'
{"call_type":"anthropic_messages","api_key":"cli-session-cli-user-1","user":"cli-user-1"}
$ curl -s "http://localhost:4577/spend/logs?request_id=$(grep -i x-litellm-call-id responses.h | cut -d' ' -f2)" -H 'Authorization: Bearer $LITELLM_MASTER_KEY' | jq -c '.[0] | {call_type, api_key, user}'
{"call_type":"aresponses","api_key":"cli-session-cli-user-1","user":"cli-user-1"}
$ curl -s -o aggregated.json -w 'HTTP %{http_code} in %{time_total}s
' "http://localhost:4577/user/daily/activity/aggregated?start_date=2026-09-30&end_date=2026-09-30" -H 'Authorization: Bearer $LITELLM_MASTER_KEY'; jq '.results[].breakdown.api_keys | map_values({metadata, api_requests: .metrics.api_requests, spend: .metrics.spend})' aggregated.json
HTTP 200 in 0.274113s
{
  "eb71a299ac70b764b90f1488bdd2d7ef9aaca220c171b1757618193a97eff198": {
    "metadata": {
      "key_alias": "cli-session-cli-user-1",
      "team_id": null,
      "user_id": "cli-user-1",
      "user_email": "cli-user-1@example.com",
      "key_exists": false
    },
    "api_requests": 10,
    "spend": 0.0014400000000000003
  },
  "cli-session-cli-user-1": {
    "metadata": {
      "key_alias": "cli-session-cli-user-1",
      "team_id": null,
      "user_id": "cli-user-1",
      "user_email": "cli-user-1@example.com",
      "key_exists": false
    },
    "api_requests": 10,
    "spend": 0.00144
  }
}
$ curl -s "http://localhost:4577/user/daily/activity/aggregated?start_date=2026-09-30&end_date=2026-09-30" -H 'Authorization: Bearer $CLI_TOKEN' | jq -c '.results[].breakdown.api_keys | keys'  # the developer's own view through the session token
["cli-session-cli-user-1","eb71a299ac70b764b90f1488bdd2d7ef9aaca220c171b1757618193a97eff198"]
## pass-through
$ curl -s -D passthrough.h http://localhost:4577/openai/v1/chat/completions -H 'Authorization: Bearer $CLI_TOKEN' -H 'Content-Type: application/json' -d '{"model":"gpt-5.6","messages":[{"role":"user","content":"Say hi"}]}' | jq -c '{model, content: .choices[0].message.content}'
{"model":"gpt-5.6-sol","content":"Hi!"}
$ sleep 12
$ curl -s "http://localhost:4577/spend/logs?request_id=$(grep -i x-litellm-call-id passthrough.h | cut -d' ' -f2)" -H 'Authorization: Bearer $LITELLM_MASTER_KEY' | jq -c '.[0] | {call_type, api_key, user}'
{"call_type":"pass_through_endpoint","api_key":"cli-session-cli-user-1","user":"cli-user-1"}
## other daily activity routes
GET /user/daily/activity -> 200 0.053606s keys=[{"key":"cli-session-","alias":"cli-session-cli-user-1","user":"cli-user-1","email":"cli-user-1@example.com"},{"key":"eb71a299ac70","alias":"cli-session-cli-user-1","user":"cli-user-1","email":"cli-user-1@example.com"}]
GET /team/daily/activity -> 200 0.061237s keys=[{"key":"cli-session-","alias":"cli-session-cli-user-1","user":"cli-user-1","email":"cli-user-1@example.com"},{"key":"eb71a299ac70","alias":"cli-session-cli-user-1","user":"cli-user-1","email":"cli-user-1@example.com"}]
GET /team/daily/activity/aggregated -> 200 0.140613s keys=[{"key":"cli-session-","alias":"cli-session-cli-user-1","user":"cli-user-1","email":"cli-user-1@example.com"},{"key":"eb71a299ac70","alias":"cli-session-cli-user-1","user":"cli-user-1","email":"cli-user-1@example.com"}]
GET /tag/daily/activity -> 200 0.028155s keys=[{"key":"cli-session-","alias":"cli-session-cli-user-1","user":"cli-user-1","email":"cli-user-1@example.com"},{"key":"eb71a299ac70","alias":"cli-session-cli-user-1","user":"cli-user-1","email":"cli-user-1@example.com"}]
GET /organization/daily/activity -> 200 0.062971s keys=[]
GET /customer/daily/activity -> 200 0.026831s keys=[]
GET /end_user/daily/activity -> 200 0.008558s keys=[]
GET /agent/daily/activity -> 200 0.053366s keys=[]
## done 2026-09-30T21:47:07Z

Observations

  • Old hash and new alias appear as two keys; left alone
  • key_exists false for session keys both legs; left alone
  • Symptom needs high spend-log volume; smaller tables never null
  • Pass-through spend rows keyed by the alias after; PR causes

Type

🐛 Bug Fix

Caveats (if any)

Medium, fixed by fe87252

  • fix(spend): attribute CLI session spend to the per-user cli-session alias instead of the hashed session token #40541 changes two enterprise/ files (check_batch_cost.py writes the batch's spend under the session alias, managed_files.py does the same for managed-file spend). The Docker image is fine either way, since the Dockerfile installs litellm-enterprise from the workspace source (uv sync --frozen ... --no-editable after COPY . .). A pip install litellm[proxy]==1.103.2 resolved litellm-enterprise==0.1.69 from PyPI instead, a wheel published from main on 2026-09-20, before fix(spend): attribute CLI session spend to the per-user cli-session alias instead of the hashed session token #40541 landed there on 2026-09-24, so a pip-installed proxy would still write enterprise batch and managed-file spend under the hash and only name it through the recovery. fe87252 bumps the line to litellm-enterprise 0.1.69.post1 (both enterprise/pyproject.toml version lines, the pin in pyproject.toml, the uv.lock entry; uv lock --check passes) the way c4b58deaacf did for stable/1.88.x, so the release publishes a wheel built from this branch and the pip install pins it. Main's own bump 0.1.71 is not reused because it names main's tree, not the line's

Low

  • rust-wheel (4 sync/async ordering failures in tests/test_litellm_rust/messages/test_callbacks.py) is red on the line's own latest run at the merge base a32e70a (run 36755425882) and on the line's last merge chore(release): sync stable/1.103.x to v1.103.1 #43824 (run 36652970017); this branch's runs (36783173067 at fe87252, fired by the pyproject.toml and uv.lock push paths) fail the same four, and the picks touch no Rust path. Left alone: fixing the line's Rust build here would widen a three-PR backport into unrelated build tooling, and the check is not required on stable/**
  • codecov/patch on backport lines is a known coverage-upload gap, not actionable here. Left alone: the shards that would fix it live in the CircleCI config, which this line does not run for PRs
  • A hash named only in spend logs past both 100-row probes (oldest and newest of the day) still comes back null, the Low caveat fix(proxy): look up hashed key names with two spend log rows per key #43656 shipped with on main, unchanged here. Left alone: widening the probes brings back the whole-window scan this backport exists to remove, and a backport carries main's behavior, not a new one
  • A hash whose alias changed and changed back within a day is named from its newest rows, also fix(proxy): look up hashed key names with two spend log rows per key #43656's Low caveat on main, unchanged here. Left alone for the same reason: the line should match main, and a rename-and-rename-back inside one day is an edge nobody has reported

Review bot verdicts at fe87252

Bot Verdict Open findings
Greptile 5/5 after the 22:21Z re-run one P2, false positive, see below
Cursor Bugbot clean at fe87252 (23:16Z) none
  • Greptile's P2 at tests/test_litellm/proxy/spend_tracking/test_key_metadata_recovery.py:610 ("Real database in mock-only tests") is a false positive: pytest-postgresql starts a throwaway local PostgreSQL process, nothing leaves the machine, and the same fixtures at the same line are already on main through fix(proxy): look up hashed key names with two spend log rows per key #43656, where Greptile raised the identical finding on 2026-09-29 and withdrew it on that rebuttal. Rebutted in-thread here, no code change; Greptile's re-run after that reply finished at 22:21Z with no counter and moved the score to 5/5, the thread stays open as its record
  • Bugbot reviewed 1bbc9ee clean at 20:01Z and fe87252 clean at 23:16Z ("found no new issues")

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

Risk report

/live-pr-risk at 1bbc9ee against the line's tip a32e70a, the same two-worker proxies and shared Postgres as the proof above, no mocks. The picks are byte-for-byte main's product code at 61a73c5 except for three conflict resolutions that keep the line's own helpers: _details_for_user_ids calls Prisma find_many(where={"user_id": {"in": [...]}}) because the chunked find_many_in helper (#42629) never landed on the line, attach_user_details keeps the inline comprehension main later split into _user_id_needing_details, and the pagination in common_daily_activity.py keeps getattr(prisma_client.db, table_name) where main names a TableActions local. All three are the same behavior in different spelling

Breaking

None observed. Every route below answered HTTP 200 on both builds and the aggregated body differs only in the recovered key_alias, user_id, and user_email values and in the alias the new session rows sit under

Backward incompatible

The same five changes main shipped with #40541, #43642, and #43656, now on the line

Regression risk

  • stable/1.103.x is still at a32e70a, this branch's base, and the line's last merge chore(release): sync stable/1.103.x to v1.103.1 #43824 shares only pyproject.toml and uv.lock (version lines) with the picks, so git merge-tree of the line and this tip is clean and the merged tree is this tip
  • The (api_key, "startTime") index the probes rely on is already on the line (20260823000000_add_spend_logs_api_key_starttime_index), and the QA database has it, so a database that skipped that migration was not observed
  • No new outbound call: the diff adds database reads only, so no forwarding recorder was needed

Dependency graph

The picked symbols have the same caller files on the line as on main (get_logged_api_key in 11 files, the recovery helpers only in common_daily_activity.py, fill_missing_api_key_aliases in the CloudZero and FOCUS exports), checked with a grep of both trees

Dependent Status
POST /v1/chat/completions, /v1/messages, /v1/responses with a session token verified-live, both legs
POST /openai/v1/chat/completions pass-through with a session token verified-live, both legs
GET /spend/logs?request_id= verified-live, both legs
GET /user/daily/activity/aggregated as admin and as the developer verified-live, both legs
GET /user/daily/activity, /team/daily/activity{,/aggregated}, /tag, /organization, /customer, /end_user, /agent verified-live, both legs, all HTTP 200, nulls before and named after
Usage page Key Activity column (activity_metrics.tsx) tested (activity_metrics.test.tsx, 60 passed)
Prometheus, websearch interception, semantic text index, managed files, executed batches, batch cost tested (the picks' unit tests), not driven live
CloudZero and FOCUS exports not driven live, untouched helper

Not verified

  • The Prometheus label and third-party loggers for session requests (no callbacks in the QA config)

  • The enterprise callers (managed_files.py, check_batch_cost.py) and the batch, skill, and websearch spend paths

  • A database without the (api_key, "startTime") index

  • The Admin UI Usage page against these proxies; the dashboard column change is covered by its Vitest suite

  • 1bbc9ee passes /live-pr-risk

mateo-berri and others added 5 commits September 30, 2026 11:43
…lias instead of the hashed session token (#40541)

Backport of #40541 to stable/1.103.x.
Cherry-picked from 25fb781 (main).
Hand-ported prerequisite for the #43642 backport to stable/1.103.x.
Taken from dd636373229b1ab3bfb2ed5b9b17b0ebd68b3ac7 (#43288, main), scratch_database only.
…ribution (#43642)

Backport of #43642 to stable/1.103.x.
Cherry-picked from f5a1c9f (main).
@devin-ai-integration

devin-ai-integration Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@CLAassistant

CLAassistant commented Sep 30, 2026 •

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 all sign our Contributor License Agreement before we can accept your contribution.
2 out of 3 committers have signed the CLA.

✅ yuneng-berri
✅ mateo-berri
❌ devin-ai-integration[bot]
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[High risk] Backport of multiple proxy fixes affecting API key logging and spend tracking.

The PR appears safe to merge based on the reviewed changes and previous threads.

Findings

  1. P2 Real database in mock-only tests ▶

Summary

This backport attributes CLI-session spend to a stable per-user alias, recovers identity for historical keys in daily activity, and prepares the 1.103.2 release.

  • Adds bounded spend-log lookups and daily-spend owner recovery.
  • Adds integration and unit coverage, dashboard owner display, and a matching enterprise package version and proxy pin.

Reviews (6) · Last reviewed commit: "chore(release): bump litellm-enterprise ..."

Comment thread litellm/proxy/spend_tracking/key_metadata_recovery.py
Comment thread litellm/proxy/spend_tracking/key_metadata_recovery.py
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

1 similar comment
@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.

Stale Bugbot comment from a previous run.

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

Comment on lines +609 to +610
_spend_logs_postgresql_proc: Final = factories.postgresql_proc()
_spend_logs_postgresql: Final = factories.postgresql("_spend_logs_postgresql_proc")

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.

P2 Real database in mock-only tests The new fixtures start a PostgreSQL server, and the added tests execute SQL against it. Repository instructions require tests under tests/test_litellm/ to use mocks rather than make real network calls. This requirement must be satisfied before merging; move these tests to an allowed integration-test location or remove their database connection.

Rule Used: What: prevent any tests from being added here that make real network calls - only mock tests can be added to this folder. Exception: tests/e2e folder, this folder can contain network calls. Why: ensure it runs correctly on github ci/cd + for all d... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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.

pytest-postgresql starts a throwaway local PostgreSQL process for the test session. Nothing leaves the machine, so this is not a real network call

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.

You're right that pytest-postgresql is local and isolated; however, repository policy still says tests/test_litellm can only contain mocked tests.

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.

Same fixtures merged on main in #43656, where Greptile withdrew this finding on 2026-09-29. Recording it as a false positive, no code change

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

1 similar comment
@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 fe87252. 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 f146256 into stable/1.103.x Sep 30, 2026
14 of 15 checks passed
@mateo-berri
mateo-berri deleted the litellm_backport_usage_attribution_stable_1_103_x branch September 30, 2026 23:31
doonga pushed a commit to greyrock-labs/home-ops that referenced this pull request Oct 2, 2026
…03.2) (#328)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/berriai/litellm](https://images.chainguard.dev/directory/image/wolfi-base/overview) ([source](https://github.com/BerriAI/litellm)) | patch | `v1.103.1` → `v1.103.2` |

---

### Release Notes

<details>
<summary>BerriAI/litellm (ghcr.io/berriai/litellm)</summary>

### [`v1.103.2`](https://github.com/BerriAI/litellm/releases/tag/v1.103.2)

[Compare Source](BerriAI/litellm@v1.103.1...v1.103.2)

#### Verify Docker Image Signature

All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](BerriAI/litellm@0112e53).

**Verify using the pinned commit hash (recommended):**

A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \
  ghcr.io/berriai/litellm:v1.103.2
```

**Verify using the release tag (convenience):**

Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules:

```bash
cosign verify \
  --key https://raw.githubusercontent.com/BerriAI/litellm/v1.103.2/cosign.pub \
  ghcr.io/berriai/litellm:v1.103.2
```

Expected output:

```
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key
```

***

#### What's Changed

- chore(release): sync stable/1.103.x to v1.103.1 by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;43824](BerriAI/litellm#43824)
- fix(proxy): backport [#&#8203;40541](BerriAI/litellm#40541), [#&#8203;43642](BerriAI/litellm#43642), and [#&#8203;43656](BerriAI/litellm#43656) to stable/1.103.x for v1.103.2 by [@&#8203;devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#&#8203;43897](BerriAI/litellm#43897)
- fix(anthropic): backport [#&#8203;42152](BerriAI/litellm#42152) and [#&#8203;42288](BerriAI/litellm#42288) to stable/1.103.x by [@&#8203;devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#&#8203;43662](BerriAI/litellm#43662)
- fix(proxy): backport [#&#8203;43962](BerriAI/litellm#43962) to stable/1.103.x by [@&#8203;yuneng-berri](https://github.com/yuneng-berri) in [#&#8203;43984](BerriAI/litellm#43984)

**Full Changelog**: <BerriAI/litellm@v1.103.1...v1.103.2>

</details>

---

### Configuration

📅 **Schedule**: (in timezone America/New_York)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMTUuMTMiLCJ1cGRhdGVkSW5WZXIiOiI0NC4xMTUuMTMiLCJ0YXJnZXRCcmFuY2giOiJtYWluIiwibGFiZWxzIjpbInJlbm92YXRlL2NvbnRhaW5lciIsInR5cGUvcGF0Y2giXX0=-->

Reviewed-on: https://git.greyrock.io/todd/home-ops/pulls/328
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.

3 participants