A JWT whose team sits in an org resolves the org on every request, and the
org row is cached for only DEFAULT_IN_MEMORY_TTL seconds while the team and
user rows ride the 60s management-object TTL. A few seconds into a database
outage the org lookup failed closed and that traffic got 503s while the same
request through a virtual key kept succeeding on its cached team.
get_org_object now also keeps a last-known copy of the org row under the
management-object TTL, and get_org_object_for_request serves that copy when
the database is unreachable, so JWT traffic degrades the same way the team
lookup does. A missing copy keeps the previous behaviour: fail closed unless
allow_requests_on_db_unavailable is set.
Two tests in the config/misc management suite were failing every run against
the Buildkite e2e stack, and one of them took the rest of the build with it.
test_add_allowed_ip_does_not_store_unrelated_config_value posted 127.0.0.1 to
/add/allowed_ip. That route sets the live general_settings["allowed_ips"] that
auth_utils._check_valid_ip reads before it persists anything, and the check is
exact string membership with no CIDR support, so from the moment the POST
returns only 127.0.0.1 can reach the proxy. The runner 403s on its very next
call, and the deferred /delete/allowed_ip sits behind the same auth dependency,
so the cleanup is locked out too and every later test in the build 403s. Build
254's first attempt lost 459 of its 465 failures to that one cascade.
There is no safe way to exercise the route against a shared proxy: nothing
reports the caller's address as the proxy sees it, so a test cannot allowlist
itself first. Move the claim to the route's own TestClient suite, where the
auth dependency is overridden and general_settings is per-test, and record the
route in the module docstring beside /cache/settings and the Vault override so
it is not re-added. save_config's end of the contract was already covered by
test_ProxyConfig_save_config_merges_changed_keys_without_copying_file_settings;
the new test covers the route's end, that what it hands save_config differs
from the loaded config in allowed_ips and nothing else.
The unrelated-key probe also only ever worked on one lane: max_parallel_requests
was added to tests/e2e/gateway/stage_mirror_ci_config.yml and never to the
Buildkite stack's config, where resolve() reports it as "unset" rather than
"config". That key is now unused, so drop it again.
test_config_update_persists_router_setting_to_get wrote router_settings.
num_retries, which both lanes declare in their config file, so the config-
ownership work correctly refuses it with a 400. Switch to retry_after, which is
declared by neither lane, is accepted by /config/update, and is reported back by
GET /router/settings. Verified against a live proxy: max_fallbacks also takes
the write but never reads back, so the read-back poll is what picks the key.
A batch upload naming a model on a LiteLLM-executed provider now checks
that the key may call that model before the upstream server is probed for
a Files API, matching the order batch create already uses. Only targets on
an executed provider are checked here, so provider-model uploads keep
their existing behavior.
File content reads and writes move out of the storage backend into
ManagedFileContentRepository, so the backend no longer queries Prisma
directly.
A second review pass found two more ways a write through the generic
config endpoint, which validates nothing, could strand the feature.
Dropping the enabled flag from a settings block the proxy had already
applied stopped the poller from reconciling it ever again, so the
callback served the old search tool forever. The poller now yields to
litellm_settings.callbacks only while it has applied nothing itself;
once it owns the callback it keeps reconciling.
A provider list written as a bare string was iterated one character at a
time, so interception matched no real provider - the same failure the
empty list already had. Anything that is not a non-empty list is now
dropped so the handler default applies.
The page also derives its toggle from whether the callback is registered
rather than from a stored flag, because a block can be live with no flag
in it at all, and the toggle is what an admin saves back.
- apply_db_row only clears runtime values for keys the row actually changed, so an env-resolved DB-owned setting survives a reload
- DELETE /config/field/delete refuses a key the config file owns instead of silently rewriting the row
- GET /config/field/info reports the declared value of a config-owned key, not the env-resolved secret
- SettingsStore gains a short-circuiting __bool__ so truthiness checks stop at the first key
- _initialize_jwt_auth resolves os.environ refs into a local mapping instead of mutating the shared general_settings dict
- rejected_writes compares against the resolved value, matching what __setitem__ accepts
- a stored value identical to the config template is no longer reported as shadowed
- the enterprise email-settings and coordination-redis writers go through reject_config_owned_writes
The first commit imported ReadOnly from typing, which only exists on Python 3.13 and up. CircleCI runs this suite on 3.12, so the module failed at import and the job stopped at collection before any of its tests ran. ReadOnly and TypedDict now come from typing_extensions, like the rest of the repo
A new test resolves the Vertex deployment's location from the suite's config with VERTEXAI_LOCATION set to a region, and fails if the vertex_location line is removed
The expected minimum backoff is now derived from litellm's INITIAL_RETRY_DELAY and MAX_RETRY_DELAY, so the test holds when those are overridden through the environment
Self-review found four ways the new settings page could take web search
interception down instead of configuring it.
A proxy that activates interception through litellm_settings.callbacks
stores no enabled flag, so the page reported it as off while it was
serving, and saving anything on that page persisted that answer and the
next poll removed the running callback. Reads now resolve the flag from
the callbacks list, and a stored block without an explicit flag no longer
touches the callback list at all.
An empty provider list is the page's own default, but the handler reads
it as "match no provider" rather than falling back to Bedrock, so
enabling the feature without naming a provider switched it on and
intercepted nothing. The empty list is now dropped so the handler default
applies.
The replacement logger is also built before the old one is removed, so a
loop ceiling the handler refuses no longer leaves the proxy with none and
retrying every poll, and a stored "false" string now reads as off rather
than as a truthy string.
A project max_budget of 0 was treated as unbudgeted by #41354, while key budgets block at 0 and null is the unlimited value. Drop the <= 0 skip so 0 blocks and null stays unlimited
The google_generate_content_endpoint_testing job went red on main when us-central1 ran out of shared gemini-2.5-flash-lite capacity for a few hours. The suite's proxy config now sends the Vertex deployment to the global endpoint and retries rate limit errors 5 times with exponential backoff, and a regression test pins that the config rides out 3 consecutive 429s
Web search interception could only be switched on by editing config.yaml
and restarting the proxy, so an admin had no way to turn it on, choose
which providers it covers, or pick which configured search tool runs the
searches without a redeploy.
Adds GET/PATCH /get|update/websearch_interception_settings backed by a
WebSearchInterceptionSettings model, and an Admin Settings panel that
reads and writes them. Config/database precedence comes from the existing
settings store, so a key the config file declares is still refused here.
The stored settings apply to a running proxy: the DB poll rebuilds the
WebSearchInterceptionLogger, removing the old instance before adding the
new one, because two instances with different params hash differently in
the callback dedup key and the first to short-circuit would win. A proxy
that activates interception the existing way, through
litellm_settings.callbacks with no stored params, is left untouched.
Responses API calls build the generation output only from response["choices"],
which Responses payloads do not carry, so Langfuse rendered a blank output.
Fold output[] into one assistant choice (output_text parts concatenated,
function_call and custom_tool_call items as tool_calls) and derive the finish
reason from status when choices are absent. Custom tool call input is now
redacted alongside function call arguments under turn_off_message_logging.
Carries the behavior of #41604 by @moshemorad (issue #41591) onto current
main with typed conversion and single-message output.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The key_exists chart test added a second direct DOM lookup for the Recharts bars, which exposes no role or label, and pushed testing-library/no-node-access over its budget (709 > 707). Both chart tests now go through one helper
reserve() stored fork_only_pid before noticing the runtime already ran under that pid, so a refused reservation still reserved the process: the next enter() cleared the runtime claim and children forked afterwards inherited a dead runtime and hung. Undo the reservation on the error path so the gate is exactly as it was.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>