Two spellings of one network share a prefix length, so the exemption wins the tie, then the higher limit, regardless of mapping order
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
/config/field/info used to answer from the LiteLLM_Config row, so "the field
400s" proved the row did not carry it. It now answers from the resolved
settings, and the CI stack config declares general_settings.max_parallel_requests,
so the endpoint returns that value and the old assertion could never hold.
The check that /add/allowed_ip writes only what the caller changed moves to
/config/list, which still reports stored_in_db off the row, and the field/info
call now asserts the ownership the endpoint reports: the config file owns the
key, so it reads back as source=config and editable=false.
Verified against a live proxy on an isolated Postgres rather than in CI, where
this check has never run: it waits on protected-environment approval.
The per-address-and-username allowance is now half the effective address allowance, rounded up, instead of a separate max_failed_login_attempts_per_user setting. A per-address override therefore raises or effectively removes both limits for that address, and no second override table is needed
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
_apply_general_settings_side_effects grew a fourth argument when the reload
started comparing the resolved pass-through list, and this dispatch test calls
it positionally, so it failed with a TypeError.
The key_management_routes group also contains /spend/logs, /team/daily/activity
and other routes whose handlers scope non-admin callers by user_id. A userless
service account key would have reached them unscoped, so the route check now
uses a dedicated two-route allowlist
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The reload only re-registered pass-through endpoints when the stored row
still carried the key, so deleting the row left the deleted routes serving
traffic until the process restarted.
It now compares the resolved list before and after the row is applied and
rebuilds on any difference, including a deletion that resolves back to the
config file's list or to nothing.
This matches what _apply_retention_settings already does with the retention
values, so the two reload effects no longer disagree about what counts as a
change.
The tests assert the proxy's registry of live pass-through routes, which is
what decides whether a request is routed upstream or falls through to the
auth error, rather than that the registration helper was called.
The two dashboard toggles under litellm_settings wrote through save_config,
so the refusal applied, but they mutated the litellm module global first: a
refused write still took effect in the running process until the next reload.
Both now check before they mutate.
/config/field/delete drops the stored key without touching the store, so a
deleted key kept reading back from the process. It now refreshes the store
like the other write paths.
/config/list reported source and editable for the general_settings rows but
not for the litellm_settings ones, so the dashboard would have shown a
config-declared toggle as editable.
Both write paths now go through the same refusal, so /config/field/update and
/config/update answer identically instead of each phrasing its own rule.
A successful write now applies to the SettingsStore, so the next read sees it.
Without this, /config/field/info reported a key the dashboard had just stored
as "not set" until the process reloaded from the database.
resolve() no longer takes a KeyRule it never reads; the store picks the row.
The matrix tests resolve through SettingsStore instead of calling resolve
directly, so the section and key in each case actually route a lookup.
ConfigFieldInfo and ConfigList type `source` as the FieldSource literal, and
the dashboard API types are regenerated for the two new fields.
Structured guardrail entries (dicts) are unhashable, so merging a carried list through _merge_guardrails_with_existing raised TypeError on the fallback path. Resolve the requested model alias through _check_and_merge_model_level_guardrails instead and add a regression test for structured request guardrails
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The precedence used to vary per key: some keys let a stored row win, some
let the file win, some merged the two. That meant an operator could not
answer "which value is live?" without knowing the key.
Now file presence decides ownership. A key the config file declares is
config-owned, whatever the database holds, and a key the file omits falls
back to the stored row. KeyRule no longer carries a RuleKind, only which
row the stored value lives in.
Writes to a config-owned key are refused at the two surfaces that reach
the database instead of being stored and silently ignored: save_config
and /config/field/update both 400 naming the key and the config file path.
Both read endpoints now report source and editable off the same
SettingsStore, so /config/field/info and /config/list can no longer
disagree inside one process.
Replaces the 786-case checked-in JSON fixture with cases generated from
the rule table, so the matrix tests no longer assert that resolve() agrees
with a snapshot of resolve().
BREAKING CHANGE: a dashboard or /config/field/update write to a setting
the config file declares now returns 400 instead of being stored. Remove
the key from the config file to let the database own it.
Pass-through requests inject the proxy span into upstream headers since #40669, which
replaced an explicit x-pass-traceparent with an unrelated trace and dropped its
x-pass-tracestate. Keep the caller's context when the carrier already names a
different trace, and keep the proxy child span for same-trace or missing headers.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The local rate-limit fallback path introduced in #40596 restores the request from a snapshot taken before the pre-call pass, so the guardrails resolved for the requested model were dropped when another deployment was selected, and the raw request-body disable_fallbacks field gated the fallback before the key-level disable_fallbacks override had been applied
Carry the requested model's merged guardrail list onto every fallback pass and read the effective disable_fallbacks value after the pre-call pass
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Titan embedding batch output carries the token count as a top-level inputTextTokenCount with no usage block, so the Bedrock batch cost parser recorded 0 tokens and 0 spend for every Titan embedding batch. Parse that field for embedding lines only and leave Converse and Anthropic shaped lines on their existing paths
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
get_provider_info is a passthrough over the cost map entry, so asserting supports_vision on named fireworks models pins a vendor capability rather than litellm behavior
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Fifty six of the deleted tests turn out to assert the output of litellm code rather than the catalog lookup itself, things like map_openai_params, get_supported_openai_params, should_fake_stream, transform_request bodies, cost_per_token arithmetic, get_llm_provider routing, and provider config dispatch. They only happen to read shipped entries as inputs, so they belong in the later rewrite that injects a local model_cost, not in this deletion
Each one is restored verbatim from origin/main along with the fixtures, helpers, constants and imports it needs, and tests/test_litellm/test_sambanova_model_metadata.py is restored wholesale
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
All three budget specs searched by typing into the search box and moved on
immediately. The search is debounced 300ms, and while the filtered query is in
flight react-query serves the previous page as placeholder data, which the list
hook reports as isLoading, which makes the table swap its whole body for
skeleton rows.
So the row assertion passed against the pre-search rows, and roughly 300ms later
the skeleton swap unmounted the row the spec had just opened the action menu on.
Playwright logged "element is not stable" twice and then "element was detached
from the DOM", and since the menu never reopened the click burned the full 15s
action timeout on all three attempts. Losing that race was pure timing: build
386 and build 387 of the UI suite ran the same commit 4b368bf066, and 386
passed where 387 failed on this spec plus "Delete a budget"
searchForBudget now waits for the GET that carries q=<budget id>, matching what
projectDetachment.spec.ts already does for a key search. That also gives the row
assertion something real to assert, since until now it could pass without the
search having filtered anything
The Python/Rust parity cases behind the ocr_backend fixture are back as they were on main:
the malformed-document matrix, Azure invalid options, native format for every provider and
the unknown Reducto model. They are the only check that the Python opt-out path and the
native path agree
test_native_failures_raise_the_public_exception_class drives every native failure kind
through litellm.ocr and litellm.aocr and pins the exception class callers catch. That class
is chosen in Python by route_host.map_failure, so no Rust test can cover it; bypassing the
mapping fails all 26 cases. The nested document edit and metadata failure tests run sync
again, since the sync path skips deployment hooks and dispatches success on the executor
legacy_callbacks.callbacks_needed now takes a Literal phase and ends its match with
assert_never, and setup imports from litellm.utils instead of mixing import styles