Skip to content

fix(proxy): look up hashed key names with two spend log rows per key - #43656

Merged
mateo-berri merged 5 commits into
mainfrom
litellm_spend_log_alias_per_key_probes
Sep 30, 2026
Merged

mateo-berri merged 5 commits into
mainfrom
litellm_spend_log_alias_per_key_probes

Conversation

@devin-ai-integration

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

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

How it solves it:

  • Look up only the oldest and newest named row per key
  • Each lookup reads at most 100 rows from its end of the window, so cost stays flat as traffic grows
  • The newest-row lookup starts where the oldest-row lookup stopped, so a key with under 200 rows in the window is read once, not twice
  • The lookup transaction turns bitmap scans off for its own statements, so the planner walks the (api_key, "startTime") index instead of reading every row of a busy key when the table's statistics or visibility map are stale

User Flow

Before: an admin's daily BI export gets every CLI session key back with no name, only an owner, so that spend can't be tied to the session it came from

  1. Their job sends GET https://litellm-domain/user/daily/activity/aggregated?start_date=2026-09-24&end_date=2026-09-24 with an admin key
  2. It gets a 200 back after more than 5 seconds
  3. Every CLI session key under breakdown.api_keys shows key_alias as null, with user_id and user_email filled in from its daily spend rows
  4. Running it again right away returns the same nulls

The Admin UI Usage page, Key Activity tab, at /ui/?page=new_usage on the same data as the After screenshot, labels the same keys by their owner's email since the alias is missing, and 4 of them key-hash-<digest> (boxed in red)

pr43656-59d6c462d4-lit8924-tip-before-key-activity-boxed.png

After: the same export comes back fast with every CLI session key named

  1. Their job sends the same GET https://litellm-domain/user/daily/activity/aggregated?start_date=2026-09-24&end_date=2026-09-24
  2. It gets a 200 back in under a second
  3. Every CLI session key shows its key_alias, user_id, and user_email, e.g. cli-session-cli.user13@example.com and cli.user13@example.com

The same Key Activity tab now shows every key by its alias (boxed in red)

pr43656-59d6c462d4-lit8924-tip-after-key-activity-boxed.png

Linear ticket

Resolves LIT-8924

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/unit/<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)

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

Last updated: 54da4e5. QA, /live-pr-risk, and /caveats ran on 59d6c46, the last commit with a product diff; the commit since adds the /audit cells below and touches no product code

Audit round 1 of 3

Merge base e7460f1 and head 59d6c46, each booted as a 2-worker proxy on its own database, ran the same 72 cells from tests/integration/spend/ (run ids base 8ca7f467a4654f5197c704d3f8fcdb45, head run 1 2a9b0e74d6c74d018fd50fa9c0f308e8, head run 2 f5f8b671c29f4289a5e11b9515855737, seeds 4106601/4106601). Base passed 70, head passed 72 then 72 with the same collected and passed selections, 0 skips and no retries. The 2 cells red on base are the intended behavior changes: 100 nameless + named + 100 nameless (middle-only name), shipped deliberately: the second Low caveat in this PR's Caveats section, called deliberate by mateo-berri in #43656 (comment), where Greptile withdrew its finding; A + 100 nameless + B + 100 nameless + A (rename and back), shipped deliberately: the first Low caveat in this PR's Caveats section, called deliberate by mateo-berri in #43656 (comment), where Greptile withdrew its finding. Every inventory row is verified by a cell below; the latency differential is evidenced by the clone-data legs above rather than by a cell, since a deterministic cell reproducing it would seed hundreds of thousands of rows against a 5 s statement timeout. The rig is the CircleCI shape: owned Postgres and Redis per leg, the scripted upstream, a 2-worker proxy in production mode with a real license, nothing mocked inside the proxy. Runs 1 to 3 of this round failed on the cells' own fixtures, on a convergence predicate, and on the rig's 60-connection Postgres under three concurrent legs, and none of them touched the product

Matrix, one row per cell
# Section Scenario Test node id Base Head run 1 Head run 2 Verdict
1 Happy alias named only by a spend log is reported on every daily activity route [user_daily_activity] 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] passed passed passed PASS
2 Happy alias named only by a spend log is reported on every daily activity route [user_daily_activity_aggregated] 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] passed passed passed PASS
3 Happy alias named only by a spend log is reported on every daily activity route [team_daily_activity] 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] passed passed passed PASS
4 Happy alias named only by a spend log is reported on every daily activity route [team_daily_activity_aggregated] 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] passed passed passed PASS
5 Happy alias named only by a spend log is reported on every daily activity route [tag_daily_activity] 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] passed passed passed PASS
6 Happy alias named only by a spend log is reported on every daily activity route [organization_daily_activity] 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] passed passed passed PASS
7 Happy alias named only by a spend log is reported on every daily activity route [customer_daily_activity] 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] passed passed passed PASS
8 Happy alias named only by a spend log is reported on every daily activity route [end_user_daily_activity] 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] passed passed passed PASS
9 Happy alias named only by a spend log is reported on every daily activity route [agent_daily_activity] 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] passed passed passed PASS
10 Happy alias on an edge of the window is reported whatever surrounds it [named_between_50_and_50_nameless] 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] passed passed passed PASS
11 Happy alias on an edge of the window is reported whatever surrounds it [oldest_named_150_nameless_newer] 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] passed passed passed PASS
12 Happy alias on an edge of the window is reported whatever surrounds it [newest_named_150_nameless_older] 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] passed passed passed PASS
13 Happy alias on an edge of the window is reported whatever surrounds it [both_edges_named_150_nameless_between] 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] passed passed passed PASS
14 Happy alias on an edge of the window is reported whatever surrounds it [100_nameless_named_99_nameless] 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] passed passed passed PASS
15 Happy alias on an edge of the window is reported whatever surrounds it [99_nameless_named_100_nameless] 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] passed passed passed PASS
16 Happy team named only by a spend log is reported next to the daily owner [team_id_column] 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] passed passed passed PASS
17 Happy team named only by a spend log is reported next to the daily owner [team_id_in_metadata] 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] passed passed passed PASS
18 Happy user named by a spend log beats the owner the daily rows name [user_column] 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] passed passed passed PASS
19 Happy user named by a spend log beats the owner the daily rows name [user_id_in_metadata] 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] passed passed passed PASS
20 Happy hashed jwt digest is named by its spend log tests/integration/spend/test_daily_activity_key_alias_probes.py::test_hashed_jwt_digest_is_named_by_its_spend_log passed passed passed PASS
21 Happy key purged from the key tables is reported with the alias its spend logs name 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 passed passed passed PASS
22 Behavior change alias named only in the middle of two hundred nameless rows is not picked up 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 failed passed passed FAIL as a behavior change (alias on base, none on head), shipped deliberately: the second Low caveat in this PR's Caveats section, called deliberate by mateo-berri in #43656 (comment), where Greptile withdrew its finding
23 Behavior change key renamed and renamed back is reported with the alias on both edges tests/integration/spend/test_daily_activity_key_alias_probes.py::test_key_renamed_and_renamed_back_is_reported_with_the_alias_on_both_edges failed passed passed FAIL as a behavior change (none on base, alias A on head), shipped deliberately: the first Low caveat in this PR's Caveats section, called deliberate by mateo-berri in #43656 (comment), where Greptile withdrew its finding
24 Edge spend log names the key only from one day before to two days after the read [second_before_the_window] 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] passed passed passed PASS
25 Edge spend log names the key only from one day before to two days after the read [first_second_after_the_window] 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] passed passed passed PASS
26 Edge spend log names the key only from one day before to two days after the read [first_second_of_the_window] 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] passed passed passed PASS
27 Edge spend log names the key only from one day before to two days after the read [last_second_of_the_window] 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] passed passed passed PASS
28 Edge two aliases on the two edges leave the key unnamed tests/integration/spend/test_daily_activity_key_alias_probes.py::test_two_aliases_on_the_two_edges_leave_the_key_unnamed passed passed passed PASS
29 Edge rows without a usable alias do not hide the named row after them [empty_string_alias] 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] passed passed passed PASS
30 Edge rows without a usable alias do not hide the named row after them [array_then_string_metadata] 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] passed passed passed PASS
31 Edge alias found once is served from the cache for the same window only tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_found_once_is_served_from_the_cache_for_the_same_window_only passed passed passed PASS
32 Edge alias logged after a cached miss shows once the miss expires tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_logged_after_a_cached_miss_shows_once_the_miss_expires passed passed passed PASS
33 Sad alias of an unexpected shape is reported as postgres renders it [json_int] tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_of_an_unexpected_shape_is_reported_as_postgres_renders_it[json_int] passed passed passed PASS
34 Sad alias of an unexpected shape is reported as postgres renders it [json_list] tests/integration/spend/test_daily_activity_key_alias_probes.py::test_alias_of_an_unexpected_shape_is_reported_as_postgres_renders_it[json_list] passed passed passed PASS
35 Sad alias of an unexpected shape is reported as postgres renders it [five_kb_string] 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] passed passed passed PASS
36 Sad key missing from the key tables is reported with the one user its daily spend names [user_daily_activity] 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] passed passed passed PASS
37 Sad key missing from the key tables is reported with the one user its daily spend names [user_daily_activity_aggregated] 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] passed passed passed PASS
38 Sad key missing from the key tables is reported with the one user its daily spend names [team_daily_activity] 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] passed passed passed PASS
39 Sad key missing from the key tables is reported with the one user its daily spend names [team_daily_activity_aggregated] 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] passed passed passed PASS
40 Sad key missing from the key tables is reported with the one user its daily spend names [tag_daily_activity] 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] passed passed passed PASS
41 Sad key missing from the key tables is reported with the one user its daily spend names [organization_daily_activity] 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] passed passed passed PASS
42 Sad key missing from the key tables is reported with the one user its daily spend names [customer_daily_activity] 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] passed passed passed PASS
43 Sad key missing from the key tables is reported with the one user its daily spend names [end_user_daily_activity] 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] passed passed passed PASS
44 Sad key missing from the key tables is reported with the one user its daily spend names [agent_daily_activity] 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] passed passed passed PASS
45 Sad key whose daily spend names two users is reported with no owner tests/integration/spend/test_daily_activity_key_owner.py::test_key_whose_daily_spend_names_two_users_is_reported_with_no_owner passed passed passed PASS
46 Sad daily spend rows naming no user do not hide the one user the others name [blank_user] 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] passed passed passed PASS
47 Sad daily spend rows naming no user do not hide the one user the others name [null_user] 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] passed passed passed PASS
48 Sad key whose daily spend names no user at all is reported with no owner tests/integration/spend/test_daily_activity_key_owner.py::test_key_whose_daily_spend_names_no_user_at_all_is_reported_with_no_owner passed passed passed PASS
49 Sad owner the user table does not hold is reported by id with no email tests/integration/spend/test_daily_activity_key_owner.py::test_owner_the_user_table_does_not_hold_is_reported_by_id_with_no_email passed passed passed PASS
50 Sad live key keeps its own user when its daily spend names another tests/integration/spend/test_daily_activity_key_owner.py::test_live_key_keeps_its_own_user_when_its_daily_spend_names_another passed passed passed PASS
51 Sad live key with no user is not given the user its daily spend names tests/integration/spend/test_daily_activity_key_owner.py::test_live_key_with_no_user_is_not_given_the_user_its_daily_spend_names passed passed passed PASS
52 Sad deleted key keeps its own user when its daily spend names another tests/integration/spend/test_daily_activity_key_owner.py::test_deleted_key_keeps_its_own_user_when_its_daily_spend_names_another passed passed passed PASS
53 Sad deleted key with no user keeps its alias and gains the one user its daily spend names 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 passed passed passed PASS
54 Sad key named only by a spend log alias keeps that alias and gains the one user its daily spend names 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 passed passed passed PASS
55 Chaos alias lookup gives up while spend logs are locked and answers once they are not 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 passed passed passed PASS
56 Chaos concurrent reads over every route all name a fresh key tests/integration/spend/test_daily_activity_key_alias_probes.py::test_concurrent_reads_over_every_route_all_name_a_fresh_key passed passed passed PASS
57 Chaos user reading a key shared with another user is shown no owner and nothing of the other user 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 passed passed passed PASS
58 Chaos user reading a key only they spent with is shown themselves as its owner 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 passed passed passed PASS
59 Chaos user reading a key only another user spent with is shown nothing of it 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 passed passed passed PASS
60 Chaos invalid key is refused without naming the owner tests/integration/spend/test_daily_activity_key_owner_faults.py::test_invalid_key_is_refused_without_naming_the_owner passed passed passed PASS
61 Chaos five kilobyte key is reported with the one user its daily spend names tests/integration/spend/test_daily_activity_key_owner_faults.py::test_five_kilobyte_key_is_reported_with_the_one_user_its_daily_spend_names passed passed passed PASS
62 Chaos key with no daily spend is reported as no activity tests/integration/spend/test_daily_activity_key_owner_faults.py::test_key_with_no_daily_spend_is_reported_as_no_activity passed passed passed PASS
63 Chaos every key of a team is reported with its own user tests/integration/spend/test_daily_activity_key_owner_faults.py::test_every_key_of_a_team_is_reported_with_its_own_user passed passed passed PASS
64 Chaos reading the same activity twice gives the same answer tests/integration/spend/test_daily_activity_key_owner_faults.py::test_reading_the_same_activity_twice_gives_the_same_answer passed passed passed PASS
65 Chaos key stops being reported with an owner once a second user spends with it 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 passed passed passed PASS
66 Chaos owner lookup gives up while daily user spend is locked and answers once it is not 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 passed passed passed PASS
67 Chaos owner is reported while a worker is killed and after it is replaced tests/integration/spend/test_daily_activity_key_owner_faults.py::test_owner_is_reported_while_a_worker_is_killed_and_after_it_is_replaced passed passed passed PASS
68 Chaos owner is reported again after the proxy restarts tests/integration/spend/test_daily_activity_key_owner_faults.py::test_owner_is_reported_again_after_the_proxy_restarts passed passed passed PASS
69 Chaos key used on every unified endpoint is reported with its own alias and user 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 passed passed passed PASS
70 Chaos cli session spend is reported with the user and team of the session tests/integration/spend/test_daily_activity_key_owner_traffic.py::test_cli_session_spend_is_reported_with_the_user_and_team_of_the_session passed passed passed PASS
71 Chaos usage ai chat hands the model the usage summary without any key owner tests/integration/spend/test_daily_activity_key_owner_traffic.py::test_usage_ai_chat_hands_the_model_the_usage_summary_without_any_key_owner passed passed passed PASS
72 Chaos owner is reported on every route while a burst of requests waits on the provider 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 passed passed passed PASS

Shared setup: a local proxy on PostgreSQL 18.6 with every migration applied at boot, including the (api_key, "startTime") index, and one gpt-5.4-nano deployment in the config. A key with alias cli-session-template made three real gpt-5.4-nano calls, and the seed below clones its spend log row into 186 hashed CLI session keys that are no longer in the key table (23 keys with 4,500 rows each and 163 with 300 each), plus 250k rows from other keys. Each row carries about 3 KB of metadata like real spend logs, 402,403 rows in total. The endpoint reads spend that is already logged, so the proof itself makes no LLM call

seed.sql, run as psql -v key_from=1 -v hashed_keys=23 -v rows_per_key=4500 -v background_rows=250000 -f seed.sql, then again with key_from=24 hashed_keys=186 rows_per_key=300 background_rows=0 (the rig itself was seeded in three smaller passes with the same totals)
\set ON_ERROR_STOP on
-- usage: psql -v key_from=1 -v rows_per_key=N -v background_rows=M -v hashed_keys=K -f seed.sql
CREATE EXTENSION IF NOT EXISTS pgcrypto;

CREATE TEMP TABLE template AS
SELECT * FROM "LiteLLM_SpendLogs" WHERE api_key = (SELECT token FROM "LiteLLM_VerificationToken" WHERE key_alias = 'cli-session-template') LIMIT 1;

CREATE TEMP TABLE cli_keys AS
SELECT k,
       encode(digest('cli-session-token-' || k, 'sha256'), 'hex') AS digest,
       'u-cli-' || lpad(k::text, 2, '0') AS user_id,
       'cli.user' || lpad(k::text, 2, '0') || '@example.com' AS email
FROM generate_series(:key_from, :hashed_keys) AS k;

INSERT INTO "LiteLLM_UserTable" (user_id, user_email, user_role, spend, models, created_at, updated_at)
SELECT user_id, email, 'internal_user', 0, '{}', now(), now() FROM cli_keys
ON CONFLICT (user_id) DO NOTHING;

CREATE TEMP TABLE planned AS
SELECT c.digest AS api_key, c.user_id, 'cli-session-' || c.email AS alias,
       timestamp '2026-09-23 06:00' + random() * interval '60 hours' AS start_time
FROM cli_keys c, generate_series(1, :rows_per_key)
UNION ALL
SELECT encode(digest('background-' || (g % 400), 'sha256'), 'hex'), 'u-bg-' || (g % 400), 'svc-key-' || (g % 400),
       timestamp '2026-09-20' + random() * interval '8 days'
FROM generate_series(1, :background_rows) AS g;

INSERT INTO "LiteLLM_SpendLogs" (
    request_id, call_type, api_key, spend, total_tokens, prompt_tokens, completion_tokens,
    "startTime", "endTime", "completionStartTime", model, model_id, model_group, custom_llm_provider,
    api_base, "user", metadata, cache_hit, cache_key, request_tags, team_id, end_user,
    requester_ip_address, messages, response, proxy_server_request, session_id, status
)
SELECT gen_random_uuid()::text, t.call_type, p.api_key, t.spend, t.total_tokens, t.prompt_tokens, t.completion_tokens,
       p.start_time, p.start_time + interval '2 seconds', p.start_time + interval '1 second', t.model, t.model_id,
       t.model_group, t.custom_llm_provider, t.api_base, p.user_id,
       t.metadata
         || jsonb_build_object(
              'user_api_key', p.api_key,
              'user_api_key_alias', p.alias,
              'user_api_key_user_id', p.user_id,
              'headers_blob', encode(gen_random_bytes(1000), 'hex') || encode(gen_random_bytes(1000), 'hex') || encode(gen_random_bytes(1000), 'hex')
            ),
       t.cache_hit, t.cache_key, t.request_tags, t.team_id, t.end_user,
       t.requester_ip_address, t.messages, t.response, t.proxy_server_request, gen_random_uuid()::text, t.status
FROM planned p CROSS JOIN template t
ORDER BY p.start_time;

INSERT INTO "LiteLLM_DailyUserSpend" (
    id, user_id, date, api_key, model, model_group, custom_llm_provider, prompt_tokens, completion_tokens,
    spend, created_at, updated_at, api_requests, successful_requests, failed_requests
)
SELECT gen_random_uuid()::text, c.user_id, d.day, c.digest, 'openai/gpt-5.4-nano', 'gpt-5.4-nano', 'openai',
       14 * :rows_per_key / 3, 8 * :rows_per_key / 3, 0.0000128 * :rows_per_key / 3, now(), now(),
       :rows_per_key / 3, :rows_per_key / 3, 0
FROM cli_keys c CROSS JOIN (VALUES ('2026-09-23'), ('2026-09-24'), ('2026-09-25')) AS d(day)
ON CONFLICT DO NOTHING;

ANALYZE "LiteLLM_SpendLogs";
ANALYZE "LiteLLM_DailyUserSpend";

The count line in each run comes from this jq filter over the response

[.results[].breakdown.api_keys // {} | to_entries[] | select(.key | test("^[0-9a-f]{64}$"))] | unique_by(.key)
| "hashed keys: \(length), null key_alias: \(map(select(.value.metadata.key_alias == null)) | length), null user_email: \(map(select(.value.metadata.user_email == null)) | length)"

Both runs use the same topology: two proxy processes, instance A and instance B, on two ports with two workers each and one shared Postgres. There is no Redis in the rig, since the lookup keeps its cache inside each worker. The same request goes to A, then B, then to each of them again. The Before run is a worktree at the merge base e7460f1 and the After run is one at the tip 59d6c46

Before (e7460f1)

  1. Instance A on port 35782: curl -sS -o before_a1.json -w 'HTTP %{http_code} in %{time_total}s\n' -H "Authorization: Bearer $LITELLM_MASTER_KEY" "http://localhost:35782/user/daily/activity/aggregated?start_date=2026-09-24&end_date=2026-09-24", then jq -r -f count_nulls.jq before_a1.json

    HTTP 200 in 5.334904s
    hashed keys: 186, null key_alias: 186, null user_email: 0
    
  2. Instance B on port 50376, the same curl and jq

    HTTP 200 in 5.442001s
    hashed keys: 186, null key_alias: 186, null user_email: 0
    
  3. Instance A again

    HTTP 200 in 0.118122s
    hashed keys: 186, null key_alias: 186, null user_email: 0
    
  4. Instance B again

    HTTP 200 in 0.069921s
    hashed keys: 186, null key_alias: 186, null user_email: 0
    

On this merge base the owner comes from the key's daily spend rows through #43642, which is why user_email is no longer null there, while key_alias still is

After (59d6c46)

  1. Instance A on port 52726, the same curl and jq

    HTTP 200 in 0.359019s
    hashed keys: 186, null key_alias: 0, null user_email: 0
    
  2. Instance B on port 37802

    HTTP 200 in 0.100293s
    hashed keys: 186, null key_alias: 0, null user_email: 0
    
  3. Instance A again

    HTTP 200 in 0.200060s
    hashed keys: 186, null key_alias: 0, null user_email: 0
    
  4. Instance B again

    HTTP 200 in 0.116820s
    hashed keys: 186, null key_alias: 0, null user_email: 0
    
  5. A sample key entry from instance B's last response

    "19ba7d358be7453326f8bd07afbbb986f7651bb864181bd99d2307ddac2a5aa6": {"metadata": {"key_alias": "cli-session-cli.user13@example.com", "team_id": null, "user_id": "u-cli-13", "user_email": "cli.user13@example.com", "key_exists": false}}

Spend per key is the same in every response of both runs, only the names differ

Admin UI, Key Activity tab

The before and after screenshots are under User Flow above. Both come from instance A of the same two-instance rig, signed in as the proxy admin, on the page's default date range of 2026-09-22 to 2026-09-29

  1. Open http://localhost:/ui/login and sign in
  2. Go to http://localhost:/ui/?page=new_usage
  3. Click the Key Activity tab
  4. Scroll down to the list of keys

Before (e7460f1, instance A on port 40046), the page says "Showing 191 of 191 keys", the keys are labeled by their owner's email since the alias is missing, 4 of them are labeled key-hash-<digest>, and the request behind the page took 5.4 seconds. After (59d6c46, instance A on port 32321), the page says "Showing 191 of 191 keys", the keys are labeled by their alias cli-session-<email>, none of them is labeled key-hash-<digest>, and that request took 0.9 seconds (rows)

Type

🐛 Bug Fix

Caveats (if any)

Low

  • A key is named when its oldest and newest named rows agree
    • Before, every named row in the window had to agree
    • So a key whose alias or owner changed and changed back is now named, where it used to come back null
    • The full-row check is what timed out, and an index on the metadata alias would mean a heavy boot migration on the largest table
  • An alias, owner, or team that only rows in the middle of the window carry is not picked up
    • That takes both edge rows missing the field, e.g. an owner set and then removed inside the window
    • Before, any row in the window could supply it
    • Reading each field from its own row makes every lookup read all 100 rows instead of stopping at the first named one
  • Each lookup reads at most 100 rows from its end of the window
    • A key whose only named rows sit more than 100 rows in from both ends comes back with null names, and that miss is cached the same way as before
    • That takes a key that had no alias, owner, or team, gained one, and lost it again inside the window, with over 100 requests on each side
    • When only one end finds a name, that name is used, even if a different name sits past the other end's 100 rows
    • Before, those rows were found only when the full read finished inside the 5 second timeout
    • Without the cap, busy keys with no named rows took 10 to 23 seconds, a cap of 250 took 0.85 to 1.63 seconds, and 100 takes 0.18 to 0.26
    • Before a vacuum, a lookup can touch up to three times the cap per key, since each end reads up to 100 rows and the boundary lookup walks up to 100 index entries that each check their heap page while the visibility map is stale, which is the bound test_recover_key_metadata_from_spend_logs_walks_a_bounded_number_of_nameless_rows_per_key asserts
  • The lookup transaction runs SET LOCAL enable_bitmapscan = off before its query
    • Without it, the planner sometimes reads every row of a busy key. On a table whose visibility map was just cleared, 23 keys with 4,500 rows each took 1,235 ms reading all 4,500 rows per key against 173 ms reading 100 index entries per key with it. The planner expects fewer rows per key than the 100 the boundary lookup needs, so it believes both plans read everything and picks by total cost, and stale statistics or a cleared visibility map tip it
    • The setting is user-settable, so any role can set it, and SET LOCAL lasts for the transaction alone, like the SET LOCAL statement_timeout line that shipped before this PR, so it works under PgBouncer transaction pooling the same way
    • A Postgres-compatible backend that rejects the setting fails the lookup, and the miss is cached like a timeout. Guarding it with a backend check would add a round trip per lookup for a backend the proxy does not run on
    • Rewrites that avoid the setting planned worse: an aggregate or window function over the 100-row subquery sorts every row of the key, EXISTS ... OFFSET 100 without an ORDER BY is not bound to the window's oldest rows, and skipping the newest-row lookup when the oldest one finds nothing misses a key named only at its newest end
    • The new lookup returns the same names as bea6f49 on every window tried, 11 and 3 wide over the proof rig's database, 10 and 3 wide over the merged tree's database, and 141 over a scratch schema of keys built to break it, with no row differing
  • A database without the (api_key, "startTime") index from migration 20260823000000_add_spend_logs_api_key_starttime_index walks the "startTime" index with an api_key filter instead
    • Every schema since v1.103.0 has that index, so only a database that skipped a shipped migration lacks it, and a backport to a line older than v1.103.0 has to carry that migration
    • Without it, keys with named rows still come back in 74 to 98 ms against the merge base's 5.9 to 7.7 seconds, 23 busy keys in 171 to 502 ms against 4.5 to 4.7 seconds, and 100 keys with 100 nameless rows each in 3.7 to 5.2 seconds against 0.4, so such a batch can hit the 5 second timeout and cache its miss as before
    • Checking the catalog for the index on every lookup would add a query per call to guard a database the migrations already fixed
  • A batch of many nameless short keys can still exceed the 5 second statement timeout, and the miss is cached the same way as before
    • 750 keys with 100 nameless rows each take 3.0 seconds with a custom plan (8.4 on the first cold run) and 10 to 12 seconds once the prepared statement switches to a generic plan, where bea6f49 took 5.6 to 6.2 and 12.5 to 14.6, and the merge base 2.7 to 2.9 and 18.7 to 22.8, so this tip is the fastest of the three in both plan modes
    • 100 such keys take 0.4 to 0.5 seconds, where bea6f49 took 0.8 to 0.9 and the merge base 0.4
  • The proof rig's spend log rows are clones of three real rows, so every row of a key carries the same metadata shape and size, where real logs vary per row
    • The lookup selects rows by key and time alone, so row content does not change which rows it reads
  • The non-required misc / Run tests job is red on 5 tests, 4 in tests/unit/interactions/test_openapi_compliance.py and tests/unit/test_utils.py::test_aaamodel_prices_and_context_window_json_is_valid
  • No CircleCI pipeline ran at this tip: it is deferred while a release is being cut, since the release needs every runner. The last one, 90626 at bea6f49, is red with 31 failed tests, and its one rerun from failed still has 31

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

/live-pr-risk passes at 59d6c46. It ran on two proxy instances sharing one Postgres database, 2 workers each, with requests alternating between them and no mocks. All 144 requests returned 200 on the merge base, the tip, and the tip merged into main, and every instance and round returned the same body for the same side. The only values that differ between the sides are key names and owners (rows)

  • Breaking

    • A key whose owner is only on a row in the middle of the window no longer gets that owner on GET /user/daily/activity/aggregated
  • Backward incompatible

    • A key whose alias changed and changed back is now named
      • Key 47fc8df03190 came back with key_alias null before and "alias-a" after (rows)
      • Before, every named row in the window had to agree, and now only the oldest and newest named rows do
      • Not fixed in this PR. It is the first Low caveat above, and mateo-berri called it deliberate in this thread, where Greptile withdrew the finding
    • Each lookup stops after 100 rows from its end of the window, so a key named only past both caps comes back null
      • This run did not drive that case live. test_recover_key_metadata_from_spend_logs_walks_a_bounded_number_of_nameless_rows_per_key covers the bound on rows read, and the null result is read from the SQL
      • Not fixed in this PR. It is the third Low caveat above, and mateo-berri called it deliberate in this thread, where Greptile withdrew the finding
  • Regression risk

    • A database without the (api_key, "startTime") index from migration 20260823000000_add_spend_logs_api_key_starttime_index would sort each key's rows without it
      • The rig's database has that index, so this run could not observe the lookup without it, and the old full read filtered on the same two columns
      • Exercising it takes a database where that migration was skipped
  • Dependency graph

    • The changed lookup runs only inside the key naming step of the daily activity routes, after the key table, the deleted key table, and the CLI session and double hashed key lookups all miss
    • Verified live on all three sides
      • GET /user/daily/activity and GET /user/daily/activity/aggregated
      • GET /team/daily/activity and GET /team/daily/activity/aggregated
      • GET /tag/daily/activity, GET /organization/daily/activity, GET /customer/daily/activity, GET /end_user/daily/activity, and GET /agent/daily/activity
      • POST /usage/ai/chat, which returned the same tool call and the same totals on every side (rows)
    • Verified live on the merge base and the tip
      • The Admin UI Usage page, Key Activity tab (rows)
    • Not reached by the change, read from the code
      • GET /gateway/daily/activity reads a table with no key column and imports nothing from the daily activity module, and the Usage page called it on both sides (rows)
      • The CloudZero and FOCUS exports call fill_missing_api_key_aliases, which uses none of the changed code, and this run did not drive them live
    • The lookup sends nothing to a provider or vendor, and the diff reads no new header or body field
  • Not verified

    • A key named only past both 100 row caps
    • The CloudZero and FOCUS exports
    • A database without the (api_key, "startTime") index
  • The merged tree is 2c12d4e31d, which is 59d6c46 merged with main at ffb15f9

  • 59d6c46 passes /live-pr-risk


Note

Medium Risk
Changes spend metadata SQL and semantics on daily activity paths (including deliberate tradeoffs: names only on window edges within the 100-row cap, aliases when oldest/newest named rows agree). Mis-tuned probes or planner settings could still yield null names or slow batches on very large miss sets.

Overview
Replaces the spend-log fallback that named missing API keys so daily activity routes stop timing out on busy hashed keys and can return key_alias (and related fields) again.

The old query aggregated every LiteLLM_SpendLogs row per key in the window. The new path probes each digest with at most SPEND_LOG_KEY_METADATA_ROWS_PER_PROBE (100) rows from the oldest and newest ends, picks the first named row on each side, and merges alias/team/user only when those edge values agree (with updated _unanimous null handling). The newest-side scan starts after the oldest probe’s stop time so keys with fewer than ~200 rows are not scanned twice. The lookup transaction also runs SET LOCAL enable_bitmapscan = off alongside the existing statement timeout so Postgres tends to use the (api_key, "startTime") index.

Tests add PostgreSQL-backed unit coverage for row bounds and edge cases, integration probes for daily activity naming/cache/timeout behavior, and a traffic test that purged keys still get aliases from spend logs.

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

The spend-log fallback for keys missing from the key table read every row per key to check that all named rows agreed, which passed the 5s statement timeout on busy keys even with the (api_key, startTime) index. Probe only the oldest and newest named row per key, so the lookup stays two index reads per key however much the key logged.
@devin-ai-integration

devin-ai-integration Bot commented Sep 29, 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

@greptile-apps

greptile-apps Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[High risk] Changes how the proxy looks up API key metadata from spend logs.

The PR appears safe to merge; no outstanding finding or new actionable issue was identified.

Findings

  1. P1 Conflicting rows go unnoticed ▶
  2. P1 Interior owner is lost ▶

Summary

The PR bounds spend-log metadata recovery to oldest and newest edge probes so daily activity can name hashed keys without scanning every row. It also adds PostgreSQL and integration coverage for probe limits, caching, concurrency, and purged keys.

Reviews (8) · Last reviewed commit: "test(integration): add spend log alias p..."

Comment on lines +57 to +58
ORDER BY "startTime" {direction}
LIMIT 1

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.

P1 Conflicting rows go unnoticed

If a key’s owner, team, or alias changes and then changes back within the window, the oldest and newest named spend logs agree while a row between them does not. The two LIMIT 1 lookups miss that conflict, so recovery can report a misleading owner or label in daily-activity data. The previous all-row comparison withheld fields with conflicting values. Logs tied at an endpoint timestamp can also make the selected value arbitrary.

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.

Deliberate, see the Low caveat: the all-row check is what timed out, and a mid-window flip back still names one of that key's real owners

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.

That’s fair. The behavior is deliberate and explicitly documented in the Low caveat: the endpoint now treats agreement between the oldest and newest named rows as sufficient, and a flip-back still resolves to a real owner/alias rather than inventing metadata. Given the timeout/performance goal and the accepted semantics, I’m withdrawing this concern.

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@codspeed

codspeed Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_spend_log_alias_per_key_probes (54da4e5) with main (8afabe8)

Open in CodSpeed

@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 mateo-berri added run-ci and removed run-ci labels Sep 29, 2026
@codecov

codecov Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

Comment on lines +70 to +72
FROM unnest($1::text[]) AS keys(digest)
CROSS JOIN LATERAL ({_named_spend_log_edge_row_sql("ASC")}) first_row
CROSS JOIN LATERAL ({_named_spend_log_edge_row_sql("DESC")}) last_row

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.

P1 Interior owner is lost

When both edge rows have aliases but no owner, this lookup ignores an owner recorded between them. Daily activity then returns a null user ID and email.

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.

Covering interior rows needs a probe per field, and a field missing on every row reads them all again, the timeout this PR removes

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.

That’s fair. Recovering interior values independently would require additional per-field probes, and a field absent from every row would still force a full scan—reintroducing the timeout this change is intended to eliminate. Given the documented trade-off of using only the oldest/newest named rows and the flat-cost requirement, I’m withdrawing the finding; no change is needed for this PR.

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

Comment thread litellm/proxy/spend_tracking/key_metadata_recovery.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.

Stale Bugbot comment from a previous run.

The newest-row probe now starts at the row where the oldest-row probe gave up, so a key with under 200 rows in the window is read once instead of twice, and the lookup transaction turns bitmap scans off so the planner walks the (api_key, startTime) index instead of every row of a busy key when statistics or the visibility map are stale.
@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

@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

@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 54da4e5. 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 61a73c5 into main Sep 30, 2026
102 of 103 checks passed
@mateo-berri
mateo-berri deleted the litellm_spend_log_alias_per_key_probes branch September 30, 2026 03:21
mateo-berri added a commit that referenced this pull request Sep 30, 2026
…tion_stable_1_103_x

fix(proxy): backport #40541, #43642, and #43656 to stable/1.103.x for v1.103.2
stvnksslr pushed a commit to stvnksslr/litellm that referenced this pull request Oct 1, 2026
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant