The ModelGroupInfo splat let a supported_reasoning_efforts value left in a deployment's
model_info seed the group, so it narrowed the group from whichever deployment was read
first and was silently ignored on every other one. The field is derived from the group's
deployments, so start it unset and let the intersection fill it in.
Also correct two docstring claims that did not match the code: the anthropic chat path
gates xhigh and max on the output_config path only, and the mode signal separates an
unknown deployment from a known non-reasoning one only while that deployment carries no
model_info of its own.
get_model_info answers supports_reasoning None both for a model absent from
the map, which the router registers under a synthesized entry, and for a
mapped model that simply is not a reasoning model. Reading both as "adds no
levels" let one custom deployment wipe every level its mapped siblings agreed
on.
The synthesized entry carries no mode, which every real map entry for a
routable model does, so an unset flag with no mode now resolves to unknown and
never narrows its group. A group that genuinely shares no level still
advertises none, and the dashboard drops the effort control for it instead of
offering levels routing would refuse.
The chat-completions gate only ever owned xhigh. Widening it to max and ultra
made gpt-5.6 answer 400 on requests litellm itself converts to /v1/responses,
where max is valid, because the gate runs before the bridge decision. No map
entry asserts either flag, so the widened gate could only ever reject.
An empty per-group intersection now falls back to the capability-blind level
list in the dashboard, matching what the picker showed before the field
existed, and ModelGroupInfo tolerates whatever shape an operator writes under
supported_reasoning_efforts instead of failing the whole /model_group/info
response.
/v1/chat/completions rejects reasoning_effort=max on every gpt-5.6 snapshot with
"Unsupported value: 'reasoning_effort' does not support 'max' with this model.
Supported values are: 'none', 'low', 'medium', 'high', and 'xhigh'", so the 16
gpt-5.6 entries that asserted supports_max_reasoning_effort had model groups
advertising a level routing would always get a 400 for.
The flag reads as chat-surface capability everywhere else in the map: before
this branch no openai or azure entry carried it at all, only anthropic-family
ones whose chat endpoint really does take max. Dropping it lines the gpt-5.6
advertisement up with what the endpoint accepts and lets the chat gate refuse
the level with our own error instead of forwarding it into a provider 400.
/v1/responses does accept max for gpt-5.6 and is unaffected: the responses
config gates none alone, so the cursor thinking-max variant keeps resolving.
AzureOpenAIGPT5Config raises UnsupportedParamsError on reasoning_effort='none'
unless the model map flags it, while OpenAI never refuses the level, so a single
opt-out polarity advertised none for 61 azure deployments that reject it. Defer
to _supports_factory when the azure flag is absent so the advertised list and the
request gate agree on every azure gpt-5 model in the map.
Also wrap the widened reasoning_effort Literal in main.py, which ruff format
flagged over the line limit.
The claude branch called the anthropic SDK's `Anthropic().count_tokens`, which the
SDK removed, so every claude call raised AttributeError. Counting now goes through
litellm's own token_counter, which handles anthropic models offline and drops the
SDK dependency entirely.
Hiding that was a swallowed error: `except Exception: Exception("Anthropic import
failed please run `pip install anthropic`")` built the exception without raising
it, so an environment missing the SDK fell through to the unguarded
`from anthropic import ...` on the next line and got a bare ModuleNotFoundError
instead of the install hint.
That was the codebase's last PLW0133, so the rule graduates from the ratcheted
budget into ruff.toml where it hard-fails, and editors get the diagnostic inline.
Add an endpoint-level regression test asserting can_user_make_model_call
receives the litellm_params after health_check_params are merged in, so the
merge-before-auth ordering cannot silently regress and let a request smuggle
a field past authorization.
* fix(ui): boot the UI image as an arbitrary uid by anchoring nginx writes under /tmp
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* test(ui): type the arbitrary-uid image test 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>
Regenerate model_prices_and_context_window.schema.json and add the flag to
the inline validator schema in test_utils.py so the new cost-map key passes
validate-model-prices-json and the JSON-valid test.
* fix(anthropic): reconcile enum with declared type in output_format schema
Anthropic cross-validates `enum` against `type` in structured outputs: every
enum value must match a single declared type. A union `type` array, or an enum
value whose JSON type differs from a scalar `type`, is rejected with
"Invalid schema: Enum value 'low' does not match declared type '['string','null']'"
filter_anthropic_output_schema had no enum/type reconciliation, so both keys
reached Anthropic untouched. Drop the conflicting `type` -- `enum` is the
tighter constraint, and an enum with no `type` is accepted
The drop is conditional: `type` is only removed when it is a union array, or
when some enum value does not match the scalar type. A matching enum plus
scalar `type` is left exactly as-is, so existing behaviour is unchanged
Pydantic emits the failing shape for Optional[SomeEnum], so this affects any
caller with a nullable enum field on the native output_format path. vertex_ai
is unaffected because it is forced onto the permissive tool-use path
Fixes#37881
* refactor(anthropic): make enum/type reconciliation immutable and precisely typed
Address review: the predicate registry was a mutable `dict[str, Any]`, and the
reconciliation removed `type` by mutating the built result with `pop`
- registry is now `Final[Mapping[str, Callable[[Any], bool]]]` wrapped in
`MappingProxyType`, so predicate signatures are statically checked and the
table cannot be mutated
- the conflict decision moves into a pure helper evaluated once against the
input schema, and the conflicting `type` key is skipped at build time in the
existing loop instead of being popped afterwards, so nothing is mutated
Behaviour is unchanged; all 27 tests in the schema-filter suite still pass
Bring the Entra ID / OAuth auth work for Azure AI Foundry routes up to date
with staging and fix the lint-budget regressions the merge surfaced:
- widen get_azure_ai_auth_headers return type to Mapping[str, str] (LIT001)
- build the azure_ai image_generation request headers into a new Final local
instead of rebinding the Final headers dict (reportGeneralTypeIssues)
- order HuggingFace rerank validate_environment params to match BaseRerankConfig
so litellm_params lines up positionally (reportIncompatibleMethodOverride)
- add a match= to the credential-error test and document the handler-boundary
patches the auth wiring tests rely on
Pulls in the detect-changes CI action and the test-unit job timeout bump, which clears the red lint and code-quality checks on this PR
The merged, tightened lint budgets flag this PR's own code, so this merge also makes video_reference_to_id a pure function instead of a helper that mutates its input dict, and rewrites the form-body regression test to call the video_edit and video_extension handlers directly rather than patching an internal class method. Adds pure-logic unit tests for video_reference_to_id
Move _update_litellm_params_for_health_check before can_user_make_model_call
so health_check_params cannot retarget the probe after the auth check. Type
the Pegasus test helper signature and drop the redundant test narrative.
Six defects in the RunwayML video provider:
- transform_video_create_request hardcoded /image_to_video, so text-to-video 400'd and video-to-video was unreachable; the endpoint is now selected from the inputs present (promptVideo/videoUri, promptImage, or text only)
- get_error_class raised instead of returning, turning a provider 4xx into a proxy 500 APIConnectionError; it now returns a RunwayMLError
- VideoObject.progress was typed int while Runway sends a 0..1 float, 500'ing status polls while RUNNING; it is now scaled to a 0..100 percent
- custom per-deployment pricing stored under litellm_metadata was ignored for video; the deployment model_info lookup now checks both metadata keys
- stale cost-map entries (gen3a_turbo, gen4_aleph) were removed and current models added, with output_cost_per_second_480p/_4k tier keys plumbed through the model-info and router types
- video cost now falls back to Runway's estimatedCost from the create response when no custom pricing is configured, and custom pricing always wins over it
Fixes#36483
Resolves the test-file conflict by keeping both sides, extends the
finish-reason gate to trace-bearing metadata events so guardrail trace
chunks keep their pre-regression delta shape, parametrizes the
regression test over tool-call, mixed, and reasoning streams, and
repairs the one ant-design icon usage the lucide-react migration left
behind in skill_detail.tsx (semantic conflict on the base branch)
The playground message bubble painted its fill, border and avatar circle from
inline hex values, so in dark mode both bubbles stayed near-white while the text
inherited the dark foreground: the message body was unreadable. The MCP-events
placeholder bubble in ChatUI carried the same three fills.
They move onto the tokens the rest of the sweep already uses, so the assistant
surface is bg-card over border-border and the user surface is the info tint at
the same weight the other selected-state surfaces take. Light mode keeps the
same colour family it had.
The regression test asserts the token classes and that no inline style survives
on either surface, which is the exact shape the bug took.
Regenerating a key from the key info page left the ?key= query param on the
old hash, so dismissing the dialog or reloading landed on a key that no longer
exists and the page rendered "Key not found".
Two defects had to line up. POST /key/{key}/regenerate returns the rotated
hash in token_id and leaves token null, but RegenerateKeyModal read
response.token || response.key_id, neither of which the endpoint populates, so
it always reported the old hash back to its parent. And KeyInfoView's
onKeyDataUpdate prop had no caller anywhere in the tree: VirtualKeysTable owns
the ?key= param and mounts the view but never passed it, so even a correct
hash went nowhere.
VirtualKeysTable now handles the update by pointing ?key= at the rotated hash
and refetching. KeyInfoView holds that callback until the regenerate dialog is
dismissed rather than firing it on the API response, because swapping the
selected key mid-dialog unmounts the view and tears down the one-time
plaintext key before the user can copy it.
* fix(ui): render team and org tpm/rpm limits of 0 as 0 instead of Unlimited
A tpm_limit or rpm_limit of 0 is a hard block on the backend (every request 429s) and only null means unlimited, but the team and organization views rendered both as "Unlimited" (and a team-member limit of 0 as "No Limit") because every display site used a falsy || fallback. The team member edit dialog also seeded its form with `tpm_limit || null`, so opening Edit Member on a member stored with 0 and clicking Save sent null to /team/member_update and silently turned the hard block into unlimited
Every limit display site in TeamInfo, organization_view, the organizations list cell and the team members table now uses a nullish check, and both member form seeding paths keep 0 for max_budget_in_team, tpm_limit and rpm_limit. Regression tests cover each site and the existing memberFormValues test that asserted 0 -> null is flipped to assert 0 survives
Resolves LIT-5760
* test(ui): assert a stored 0 member limit survives an untouched save
The EditMembership integration test named the old 0 -> null collapse as the expected payload, so the related-tests CI job went red once the form kept 0. It now asserts 0 survives and only the empty budget_duration collapses to null. The TeamMemberTab fixture is built with a map instead of mutating the nested membership
The tooltip popup is an inline-flex row, so the four sibling blocks passed as a fragment laid out side by side in four columns. Wrap them in a single flex-col container instead.
The inline code samples also used bg-muted, which is defined against the page surface, not the inverted tooltip surface, so they rendered as near-white chips carrying near-white text. Tint them from the popup's own token instead.
The virtual key shown after creating a key sits in a div with a
hardcoded #f8f8f8 inline background, so in dark mode the box keeps
the light background while the key text inherits the light foreground
color, leaving the key nearly unreadable. Swap the inline styles for
the bg-muted and text-foreground tokens, which resolve per theme.
* fix(terraform): add soft_budget, tags, and soft_budget_alerting_emails to litellm_team
The team resource rejected soft_budget and tags at plan time and had no way
to express the list-valued metadata.soft_budget_alerting_emails the proxy
reads for soft-budget alerts, even though /team/new and /team/update accept
all three. Add the attributes, forward them in buildTeamData (alert emails
merged under metadata, where the proxy stores them), and send the full
metadata map whenever either half changes because /team/update replaces
metadata wholesale.
Read was decoding /team/info as if the team fields were top-level, but the
proxy nests them under team_info, so every attribute silently fell back to
prior state. Decode the envelope and split the proxy's metadata back into
tags / soft_budget_alerting_emails / string metadata, dropping the
server-managed team_member_budget_id.
Verified with OpenTofu plan/apply against a live proxy: the attributes are
accepted, land on the proxy, refresh into state, re-plan clean, propagate
on update, and clear when removed from HCL.
* fix(terraform): clear litellm_team.soft_budget in state when the proxy returns null
Read only wrote soft_budget when the proxy returned a value, so a soft
budget cleared outside Terraform stayed in state and never surfaced as
drift. Set it from the response unconditionally so a null clears it.
`exception_type` decides the class and status a caller sees for every provider
failure, across 190 raise sites, and the tests for it were written one incident
at a time. Nothing said what a plain 401 from any given provider should be, so
mutating a raise site went unnoticed: swapping the class at each of the 190 in
turn, the mapped test file caught 15.
Adds two tables asserted end to end through `exception_type`: 25 providers by
the 9 upstream statuses, and the three error shapes the router branches on
(a full context window, a content policy block, a timeout). The same 190
mutants now fail 97 of them.
The tables record today's behavior, uneven where it is uneven. cloudflare,
ollama and vllm map no status at all, so every failure reaches the caller as a
500. A full context window is recognised by 15 of the 25, and a content policy
block by 11, which bounds where `context_window_fallbacks` and the content
policy retry policy can fire.
proxy-endpoints and proxy-infra are the unit tier's critical path at 358s and
325s of pytest, measured on staging 2026-08-21, and both run two xdist workers
on a four-vCPU runner. proxy-server already runs four. This is the cheaper half
of splitting them: no second job, so no second setup to pay for.