Commit graph

261 commits

Author SHA1 Message Date
yuneng-jiang
666f6b01b4
fix: enforce disable_custom_api_keys from general_settings (#42437)
* fix: enforce disable_custom_api_keys from general_settings

The gate in _check_custom_key_allowed read the persisted UI settings row
through get_ui_settings_cached, which had two consequences.

A config-file general_settings.disable_custom_api_keys was never enforced,
because the gate only ever looked at the stored ui_settings row. POST
/key/generate with a custom key value returned 200 even with the flag set
to true in config.yaml.

The read went through a DualCache with a 600s TTL, which is per worker
without Redis, and only the worker that served PATCH /update/ui_settings
refreshed it. A gate flipped through the UI was then a coin flip across
workers for up to ten minutes.

Both go away by routing the flag the way the other runtime UI flags are
already routed. Adding it to _RUNTIME_GENERAL_SETTINGS_FLAGS and to the
settings rules' _UI_SETTINGS_FIELDS makes SettingsStore resolve it from the
ui_settings row with the config file winning, and every pod re-reads it on
its own settings sync rather than holding a private cached copy. The two
lists have to stay in step: a flag in one and not the other resolves
against the wrong stored row and silently never reaches a reader, so there
is a test for that invariant.

Writes to the ui_settings table did not publish on the config-sync channel,
so other pods only discovered a change on their next periodic reload. Adding
litellm_uisettings to _CONFIG_SYNCED_TABLE_NAMES puts it on the same pubsub
path model and SSO config writes already use, which cuts cross-pod
propagation from tens of seconds to a few.

The value reaching the gate is run through coerce_bool first. Resolution
hands back the raw YAML value, so a quoted "true" in config.yaml is a str
and the old `is True` check let custom keys straight through.

* test: assert both directions of the coerced config value

* test: assert the runtime flags read back instead of inspecting the registry
2026-09-22 13:28:31 -07:00
devin-ai-integration[bot]
569ccaece9
fix(proxy): make the lazy OpenAPI snapshot byte-identical on every Python version (#42519)
Python 3.13+ strips the common indentation of docstrings at compile time and 3.12
keeps it, and the 429 error description in ERROR_RESPONSES came straight from
RateLimitError.__doc__, so regenerating litellm/proxy/_lazy_openapi_snapshot.json
on a 3.13+ venv produced a one-line diff that the check-ui-api-types job (Python
3.12) rejected. Run the docstring through inspect.cleandoc before it lands in the
spec, regenerate the snapshot once, and pin the behavior with a test

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-09-22 12:21:20 -07:00
devin-ai-integration[bot]
5035c458fb
feat(proxy): admin-only /debug/report sharing the bug report environment (#42440)
* feat(proxy): add admin-only /debug/report sharing the bug report environment fields

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* feat(proxy): add verbose=true to /debug/report listing every config key with typed values

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy): inject auth into /debug/report through Annotated to keep the B008 budget flat

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy): bound the verbose config walk, drop nested-list recursion from the safe renderer, regenerate schema.d.ts

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy): count pass-through and mcp header maps plus operator-named budget maps in verbose /debug/report, single-exit scalar renderers

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* chore(ui): regenerate schema.d.ts after dropping the verbose query from /debug/report

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: ryan <ryan@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-22 12:05:23 -07:00
devin-ai-integration[bot]
b673ee61e7
feat(arize): per-team success and error sampling rates for the Arize AX callback (#42383)
* feat(arize): per-team success and error sampling rates for the Arize AX callback

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(arize): fail open on invalid sampling rates and type the new sampling code

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(arize): type the sampling test helpers and parametrized fixture

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-22 11:21:40 -05:00
devin-ai-integration[bot]
8b33da7bb3
feat(proxy): opt-in litellm_call_id in JSON error bodies (#42391)
* feat(proxy): opt-in litellm_call_id in JSON error bodies

Add general_settings.include_call_id_in_error_body. When true, the value
already on the x-litellm-call-id response header is copied into JSON error
bodies: as error.litellm_call_id on the OpenAI-shaped routes, /v1/messages,
and streaming first-chunk errors, and as a top-level litellm_call_id on
pass-through routes. Off by default, so error bodies stay byte-identical
unless an admin opts in

* chore(proxy): drop helper docstring and restore lazy OpenAPI snapshot

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-09-21 19:17:18 -07:00
kerry
41d6acaa97 test(proxy): isolate the agent read-through singleton between unknown-agent tests
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-22 01:34:31 +00:00
Mateo Wang
5417abd586
Merge pull request #42011 from BerriAI/litellm_scrub_default_master_key
docs: stop advertising sk-1234 as the master key in shipped configs and examples
2026-09-19 19:05:31 -07:00
Mateo Wang
b6dd3d932c
Merge pull request #42019 from BerriAI/litellm_master_key_boot_enforcement
feat(proxy)!: refuse to start with an unset, empty, or publicly known master key
2026-09-19 19:04:02 -07:00
ryan-crabbe-berri
a6c51ba3de fix(proxy): never treat plaintext that base64-decodes to nothing as a ciphertext during the master key migration
A string such as "*" or "..." has no base64 characters, so it decoded to no bytes and read as an empty plaintext under any key. The migration would have counted it and overwritten it with a ciphertext of the empty string. Also read from the writer database instead of a read replica, report a database error during the migration instead of crashing the boot, skip columns the connected schema lacks across every schema on the search path, cap the JSON walk depth for the recursion detector, and move the boot wiring into one tested function.
2026-09-19 17:53:17 -07:00
ryan-crabbe-berri
38d776bd2b feat(proxy): re-encrypt stored secrets at boot from LITELLM_MIGRATE_FROM_MASTER_KEY so an unsafe key can be replaced while the proxy refuses to start
Rotating through POST /key/regenerate needs a running proxy, which a refused boot does not have. The refusal now counts the stored values that decrypt under the unsafe key. When there are none it only asks for a new key. When there are some it also asks for LITELLM_MIGRATE_FROM_MASTER_KEY, and the next boot with a safe key re-encrypts them and logs that the variable can be deleted. Leaving the variable set afterwards is a no-op with one notice.
2026-09-19 17:32:58 -07:00
yucheng-berri
2ec5c2c7cd
Merge pull request #41740 from BerriAI/litellm_otel_v2_langfuse_llm_spans_only
feat(otel v2): opt-in llm_only span scope for Langfuse destinations and the operator Langfuse exporter
2026-09-19 17:24:50 -07:00
yucheng
f63782f678 fix(otel v2): reject a langfuse_span_scope that conflicts with another callback entry on the same team or key
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-19 23:16:54 +00:00
ryan-crabbe-berri
c8e0f2ddb4 docs: stop advertising sk-1234 as the master key in shipped configs and examples
Shipped proxy configs now read general_settings.master_key from
os.environ/LITELLM_MASTER_KEY, the .env examples ship a blank value with
the openssl generate command above it, and READMEs, the missing env vars
page and Admin UI code snippets show a generate command or the
<your-master-key> placeholder instead of the literal sk-1234

The two CircleCI docker runs that mount proxy_server_config.yaml and
oai_misc_config.yaml now pass LITELLM_MASTER_KEY so their runtime key is
unchanged
2026-09-19 12:59:48 -07:00
mateo-berri
5f6ffdc333 test: drop the docstring that restated the payload test's name 2026-09-18 22:48:42 -07:00
mateo-berri
ddac683ec6 fix(exceptions): keep internal_server_error as the public type of an upstream 500
PR #40243 started carrying the upstream error body on InternalServerError so the Responses response.failed event can report the provider's code and message, and openai's APIError.__init__ took the body's type along with it. The proxy then answered an OpenAI-compatible upstream 500 with type server_error while a 502 and a 503 kept internal_server_error, and the integration contract in test_observed_routing.py went red. Pin the type the way RateLimitError pins throttling_error, keeping the body.
2026-09-18 22:28:58 -07:00
Mateo Wang
c1de8665ff
Merge pull request #34267 from BerriAI/litellm_claude_code_gateway_protocol
feat(proxy): serve the Claude Code gateway protocol under /claude_code_gateway
2026-09-18 20:49:48 -07:00
mateo-berri
59f7a00cf6 fix(claude_code_gateway): scope the protobuf body skip to the OTLP routes and match the metrics middleware on the route path 2026-09-18 17:23:59 -07:00
mateo-berri
87ac68709c fix(claude_code_gateway): single-use device codes across replicas, protobuf telemetry, CLI user route access 2026-09-18 16:05:16 -07:00
ryan-crabbe-berri
a5f6ce7bb1 Merge remote-tracking branch 'origin/main' into litellm_lit_3269_project_spend_tracking
# Conflicts:
#	tests/test_litellm/proxy/db/test_db_spend_update_writer.py
2026-09-18 14:22:07 -07:00
Yassin Kortam
84ae0805ba
Merge pull request #41620 from BerriAI/litellm_team_member_temp_budget_increase
feat(proxy): temporary budget increase for team members
2026-09-18 11:38:34 -07:00
yucheng
755890b59a fix(otel v2): scope-aware additive dedupe, reject langfuse_span_scope off langfuse_otel, render the scope as a select
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-18 07:53:32 +00:00
yucheng
72e847288a feat(otel v2): opt-in llm_only span scope for Langfuse destinations and the operator Langfuse exporter
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-18 02:47:29 +00:00
yassin
504b464417 test(proxy): assert the persisted member budget row as a whole instead of field absence
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-18 02:12:12 +00:00
yassin
507219e26f fix(proxy): resolve temporary budget grants against the live team default instead of a snapshot
A temporary-only member update no longer clones the team default budget into the private row. The row stores just the temp pair and auth, spend admission and reservation add the active increase to the current shared default, so a later lowering of the default reaches members with an active grant

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-18 02:09:09 +00:00
yassin
3926e74e9d test(proxy): lock plain member updates to the merge-patch contract without a team default snapshot
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-18 01:28:49 +00:00
kerry
84d4d17bf8 Merge remote-tracking branch 'origin/main' into litellm_remove_brittle_price_pinning_tests
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

# Conflicts:
#	tests/test_litellm/llms/bedrock/chat/test_converse_transformation.py
2026-09-18 00:29:52 +00:00
kerry
c4620170ca test: delete assertions that pin vendor cost map facts
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-18 00:28:49 +00:00
Devin AI
569ef3ea25 merge: resolve conflict with main in member budget seeding
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-17 23:42:41 +00:00
ryan-crabbe-berri
e5aa10a1ea
Merge pull request #41686 from BerriAI/litellm_member_budget_clone_reset_and_audit
fix(team): keep a forked member budget's reset window and audit bulk member budget writes
2026-09-17 16:32:31 -07:00
Devin AI
9d5f65c26c merge: resolve conflict with main, move temp budget patch fields to shared member_budget_patch
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-17 23:04:25 +00:00
ryan-crabbe-berri
f04f0258f7 refactor(team): model the bulk budget audit payload as frozen types 2026-09-17 15:41:06 -07:00
kerry
9eb6fbc572 test: read cost-map keys the implementation resolves and isolate the tariff test's model_cost copy
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-17 22:32:39 +00:00
ryan
bde5593523 Merge remote-tracking branch 'origin/main' into litellm_lit_3269_project_spend_tracking
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

# Conflicts:
#	litellm/proxy/common_utils/reset_budget_job.py
2026-09-17 22:29:16 +00:00
ryan-crabbe-berri
909a30d6a1 fix(team): keep a forked member budget's reset window and audit bulk member budget writes
Forking a shared budget row rebuilt budget_reset_at from the duration, so editing
an unrelated limit restarted the member's window while their spend carried over:
a tpm bump quietly handed them a fresh period. The fork now inherits the source
row's deadline, and only recomputes when the patch actually sets budget_duration.

The bulk member budget route now writes one audit entry per call, a team-scoped
'updated' row carrying every written member's limits before and after, matching
what /team/member_add already records for membership changes. It honors the
litellm-changed-by header like the other audited team routes.
2026-09-17 15:17:00 -07:00
kerry
ccff1fa95f test: derive the remaining cost-map pins from the catalog entry
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-17 21:49:16 +00:00
yassin
664688f337 fix(proxy): do not carry a zero team default cap onto a new member budget row
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-17 21:00:31 +00:00
yassin
e73b8d49da fix(proxy): seed a new member budget row from the team default cap
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-17 20:49:58 +00:00
yassin
97c50acc19 fix(proxy): persist a temp budget pair for members without a private budget row
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-17 20:30:29 +00:00
ryan-crabbe-berri
fbbddb922e
Merge pull request #41488 from BerriAI/litellm_bound_enduser_reset_invalidation
fix(budgets): page end-user cache invalidation after a budget reset
2026-09-17 13:21:23 -07:00
yucheng-berri
672f43fd54
Merge pull request #41356 from BerriAI/litellm_lit7836_call_id_endpoint_logs
fix(proxy): carry litellm_call_id through endpoint specific error logs and failure responses
2026-09-16 16:43:12 -07:00
ryan-crabbe-berri
0d8b46b88c refactor(proxy): trim the invalidation docstrings and inject the page read failure
Cuts the new docstrings back to the parts a reader cannot get from the code,
and fixes a stale reference: the walk this one is modelled on is
_reset_windows_for, not _reset_windows_for_source.

The truncation test reached in and replaced MockTable.find_many. The mock takes
a scheduled read failure instead, the way it already takes canned rows.
2026-09-16 16:41:13 -07:00
ryan-crabbe-berri
14fbd623d7 fix(proxy): clear both cache partitions and carry the walk position as a value
UserApiKeyCache's batch delete ran the two partitions in sequence, so a Redis
failure on the hashed token partition returned before the ordinary management
keys were touched. Both partitions are attempted now and the first failure is
re-raised for the caller to report.

The customer walk kept its position in two locals it reassigned each page. It
now mirrors the window walk in the same file: a page helper returns where the
walk goes next, and the driver rebinds one value.
2026-09-16 16:28:16 -07:00
ryan-crabbe-berri
cdf0142f4a fix(proxy): isolate each cache and each page in the budget reset invalidation
Greptile review follow-ups on the paged end-user cache invalidation.

UserApiKeyCache keeps hashed token keys in a second in-memory partition, and
routes delete_cache / async_delete_cache there. It inherited the new batch
delete unchanged, so a budget cascade cleared the main partition and left the
key object sitting on its pre-reset spend. Override it the way
async_set_cache_pipeline already partitions its entries.

The spend counters and the management cache shared one exception handler, so a
Redis failure on the counters returned before the management cache was touched
at all. Each cache gets its own await and its own handler now.

A failed page read returned the same empty tuple that ends the walk normally,
so a truncated pass was reported as a complete one. The window is advanced by
then and no later tick comes back for the customers past that page, so the walk
now says it was cut short and the service log carries it.
2026-09-16 16:14:08 -07:00
ryan-crabbe-berri
7ba47a5b6e fix(budgets): page end-user cache invalidation after a budget reset
The budget-tier reset read every customer linked to an expiring tier into
one result set before the write, then invalidated their caches one key at
a time. Both of those scale with the customer count, so a large enough
deployment can OOM the proxy pod on the read, and the tail of the
population sits on a stale spend counter while the per-key invalidations
drain

PR #40639 moved the reset write itself to a link-based UPDATE, so that
pre-commit read no longer feeds the write. It only fed cache invalidation
and the service-logging counts, which means it can move after the commit.
This replaces it with a keyset walk over litellm_endusertable ordered by
user_id, taking RESET_BUDGET_JOB_BATCH_SIZE rows per page, the same shape
_reset_windows_for_source already uses, with no per-run page cap for the
same reason that walk has none: the cursor cannot survive the run, so a
cap would restart at the first customer on every tick and never reach the
tail

Each page's counter and cache keys now go out as one batched delete
through a new DualCache.async_delete_cache_keys, which drops the
in-memory entries and chunks the Redis DELETE at
DEFAULT_MAX_REDIS_BATCH_CACHE_SIZE

num_endusers_found and num_endusers_updated now report the customers
whose caches were invalidated after the commit rather than the rows read
before it, so both read 0 when the cascade write fails
2026-09-16 12:12:18 -07:00
yassin
6cf35ed71b feat(proxy): expose lifetime total_spend on virtual keys
Adds a persistent total_spend column to LiteLLM_VerificationToken and LiteLLM_DeletedVerificationToken, incremented in the same write as spend and left alone by budget resets. Surfaces it on /key/info, /key/list and the Admin UI Virtual Keys table and key detail view

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-16 09:42:20 +00:00
yucheng
581c613f66 fix(proxy): keep litellm_call_id on shaped errors and list_batches failure hook
Already shaped ProxyException and HTTPException errors passing through the moderations, audio speech, Anthropic Messages, and handle_exception_on_proxy paths now answer with the x-litellm-call-id header the route logged under, without overwriting a header the exception was raised with. The GET /v1/batches failure hook receives the resolved request data so the spend log request_id matches the response header and the error log

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-16 03:02:50 +00:00
yucheng
1a7ca04cc5 fix(proxy): carry litellm_call_id through endpoint specific error logs and failure responses
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-16 02:29:26 +00:00
ryan
b20f1422eb fix(proxy): carry project_id through key metadata enrichment and drop docstrings
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-16 02:28:38 +00:00
ryan
1a749d84bd fix(proxy): track project spend and enforce project budgets additively
Project-scoped keys never wrote spend to LiteLLM_ProjectTable, so
/project/info stayed at 0 and project budgets could not block. Wire the
PROJECT entity through the spend queue, redis buffer, and db writer,
reserve and increment a spend:project counter, reseed it from the
project row, reset project spend in the budget cascade, and read the
live counter in the project max budget check. Team member budgets keep
gating project-scoped keys alongside the project budget

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-16 01:56:00 +00:00
Yassin Kortam
e36626174b
Merge pull request #41297 from BerriAI/litellm_return_400_on_lone_surrogate_input
fix(proxy): return 400 instead of 500 for lone surrogate escapes in request body
2026-09-15 15:04:08 -07:00