* refactor(ui): migrate the model and router settings pages off antd
Converts the add model flow, credential panels, model settings and router
settings onto the shadcn primitives, moves the mapping table onto the
shared DataTable, and drops the dead uploadProps prop chain that only
existed to carry antd's UploadProps type.
* fix(ui): split comma-separated custom technical keywords into one term each
* refactor(ui): migrate the MCP servers pages off antd
Converts the MCP server create, edit, connect and permission screens plus
the MCP tools and selector components onto the shadcn primitives, and
rewrites the test helpers that drove antd's select and collapse DOM.
* fix(ui): finish the MCP servers antd migration so the shared field rules have one contract
mcpFieldRules and MCPPermissionManagement were already flipped to the shadcn
prop shape, but CreateMCPServer and UserEnvVarsModal were still rendering antd,
so the create modal spread onValueChange onto an antd Select that ignores it and
passed searchValue props that no longer exist. Convert both off antd, drop the
searchValue plumbing the MultiSelect now owns, and normalise tag values before
the tag list renders so a delimited or empty string cannot crash it.
Rewrite testUtils.selectOption to drive the shadcn listbox instead of
.ant-select, expand the collapsed permission panel before querying its switches,
and assert the dismiss case after a reopen now that Dialog unmounts closed
content.
* fix(ui): split multi-tag entries the MCP tag inputs commit as one value
The tag input hands back whatever the admin typed as a single custom value, so
"read,write" was stored verbatim and reached the backend as one malformed scope.
tagsControl already split delimited values on the way in; run the same
normalisation on the way out and dedupe, so both directions agree.
* fix(ui): stop splitting tag entries that are not scope lists
The previous commit split every tag field on whitespace and commas, but only a
scope list is delimited. A stdio arg or an access description item may contain
both characters as part of the value, so splitting them changed the argv the
process receives. Keep those entries verbatim and move the splitting behind
scopesControl, which the OAuth, token exchange and ID-JAG scope fields use.
* fix(ui): split tag entries on comma only, matching the antd token separator
Every tag field here was an antd Select carrying a comma token separator, so a
comma committed a tag and nothing else did. Splitting on whitespace as well
broke stdio args, and splitting neither left comma-separated extra headers and
access groups stored as one malformed value. Apply the comma rule in both
directions, trim each entry, and drop the scope-specific helper the previous
commit added, since the backend types scopes as a list rather than the
space-delimited string that helper assumed.
* fix(ui): stop rewriting stored tag values that an admin never edited
Stdio args are process argv, so a comma inside one argument and a
deliberately repeated flag both have to survive a round trip through the
edit modal. Two places were rewriting them. tagsControl split and deduped
the value it read back from the server, and MultiSelect re-split every
already-committed chip on each change rather than only the entry just
typed. Both now leave settled values alone, which keeps the antd token
separator applying to typing and nothing else.
* refactor(ui): migrate shared primitives and common components off antd
Adds the success variant to the shared Alert plus success, warning and
info variants to Badge, introduces UtcDateTimeInput to replace antd's
DatePicker, and converts the common components and key/team helpers onto
the shadcn primitives.
* fix(ui): keep MultiSelect and budget input faithful to their antd behaviour
Restore the clear-all control MultiSelect lost, split comma-separated
custom entries into one value per token, and stop rounding the budget
input on every keystroke so a fractional amount survives typing.
* test(ui): drive the access group picker through the migrated MultiSelect
AccessGroupSelector no longer renders an antd Select, so the placeholder
is an input label rather than a text node and the popup inerts the page
until it closes.
The dashboard drew its icons from two libraries at once: lucide-react,
which shadcn/ui ships with, and @ant-design/icons, left over from antd.
This moves the last 39 files onto lucide and drops the dependency, so
the icon set matches the component library everywhere.
antd icons sized themselves from the inherited font-size and rendered as
role="img" with an aria-label, neither of which a lucide svg does, so the
swap carries explicit size classes and gives the two icon-only plugin
buttons real accessible names.
`ProxyConfig.get_config()` walked the parsed config and replaced every
`os.environ/<KEY>` string with `get_secret(value)` before anything initialized
the secret manager, so a key held only by the manager resolved to `None` and
that `None` was written back into the config. The later fallback in
`load_config` could not recover it, because the key now existed with a `None`
value.
Hoist the initialization into `get_config()`, ahead of the resolution pass, so
every entrypoint gets it: the CLI already did this itself, but the microservice
entrypoints (`gateway/main.py`, `backend/main.py`) uvicorn the app directly and
bypass the CLI. `load_config`'s own call is now redundant and is dropped, so
startup builds the manager once instead of building one and discarding it.
`get_config()` also runs on management-endpoint request paths, so this returns
early once a manager exists rather than rebuilding the client per request.
Also warn when a reference the manager would have been asked for resolves to
`None`. The reporter had no log line at all to work from. `get_secret` only
reaches the manager when reads are enabled and the name is in `hosted_keys`, so
`secret_manager_would_be_consulted` mirrors that gate and keeps the warning off
env-only references, which are expected rather than an error.
detect-backend-changes diffed the event payload's base.sha against the
checked-out ref. Those are two different points in time: actions/checkout
resolves refs/pull/N/merge, and GitHub recomputes that ref whenever the base
branch advances, so the diff picked up whatever landed on staging between the
event firing and the job starting. On a recent UI-only pull request three
backend commits from staging were attributed to the branch, and every backend
shard ran in full
Ask the API which files the pull request touches instead. That is the same set
the Files changed tab shows, and it is immune to either endpoint moving. The
shell body moves into .github/scripts/detect_backend_changes.sh so it can be
exercised directly, and the fail-open paths now also cover an API failure, a
file list past the API's 3000-entry listing ceiling, and a classifier that
prints something unexpected
Every dynamic tracer-provider build called Resource.create, which scans the entry
points of every installed distribution, roughly 3ms and 200 file opens. The dynamic
providers reach it from the async logging path, which runs on the event loop serving
requests, so past the provider cache bound every request paid it and delayed the
requests in flight alongside it
The value derives only from the logger's config and process environment, so it is
built once per logger and reused. This logger's own init-time providers share it,
which also removes redundant startup builds. ArizeLogger overrides _init_tracing and
still builds its own, so it keeps one extra build
Refs LIT-5437
The proxy pre-creates the logging object before the router picks a deployment,
and the native /v1/messages handler never copied the deployment's
vertex_location into the logging params it updates, so cost resolution fell
back to the environment or the default region and priced every call on this
surface with the regional endpoint uplift. Copy the explicitly configured
location from the request's litellm params, the same source dispatch builds
the request URL from, and register the new regional_endpoint_uplift_multiplier
field in the cost map schema test.
The checkbox and its visible text both sat inside the max_budget
FormField label, which renders a single <label htmlFor> pointing at the
number input, so clicking the text focused Max Budget instead of ticking
the box. The checkbox only carried an aria-label, so it had no label of
its own to catch the click.
Wrap the checkbox and its text in their own <label>, the association the
antd checkbox wrapper used to provide. The accessible name now comes
from that label, so the aria-label is dropped rather than duplicated,
and the outer field label still points at the number input.
Auth and pass-through failures reach post_call_failure_hook with the raw request body unstripped, so a client-supplied standard_logging_object could feed the new attribution fallback when the logging object carries none. Pop the key before the lift so only the logging object may supply it. Also coalesce a None recovered cost to 0.0 so the lift always overwrites any client-supplied response_cost, matching the merge base's clobber semantics.
* test(ui): cover the two modals no test would catch breaking
Both files sit in the antd Modal migration's blind spot. EditSSOSettingsModal's
test replaced antd wholesale with a stub Modal and asserted the stub's own
data-testid markup, so it proved nothing about the modal a user sees and would
have stayed green through any regression. routing_groups had no test at all.
Rewrite the first against the real antd Modal, querying by dialog role and
accessible name so the assertions hold under either library, and add an
integration test for the second that drives the row menu and the delete
confirmation end to end.
Modal width drops out of the SSO assertions: antd carries it as an inline style
and shadcn as a max-width class, so either form couples the test to the library
rather than to anything a user perceives.
* refactor(ui): move the straightforward antd Modals onto the shared Dialog
Twenty files whose Modal only used title, open, width, footer, className and
onCancel, so each one maps onto Dialog without judgement calls. Width becomes a
max-width class, the body gets the house scroll cap so tall content stays
reachable, and destroyOnHidden goes away because Base UI unmounts a closed
dialog on its own.
EditMembership needed a real fix rather than a translation. Clearing the form
after a submit resolved only ever worked by accident: the reset set every field
to undefined, which react-hook-form does not push out to a subscribed
Controller, and the fields looked cleared only because antd's Modal happened to
re-render the subtree afterwards. Dialog does not, so the stale values showed
through. emptyMemberFormValues now returns the empty value each control
actually understands, an empty string, null or an empty list, and the reset
lands whatever renders around it. Its test asserted the undefined shape while
describing the behaviour it was missing, so it now checks the values instead.
* refactor(ui): migrate the antd Modals that needed a judgement call
Sixteen more files. Most carried a prop that does not translate literally:
maskClosable becomes disablePointerDismissal, afterOpenChange becomes
onOpenChangeComplete, and closable={false} becomes showCloseButton={false}.
The styles prop went away everywhere it appeared. All but one instance set the
body to 24px and the header to 24px with no border, which is what DialogContent
already renders, so keeping it would have meant writing the default back by
hand.
Several Modals passed onOk alongside footer={null}, so antd rendered no OK
button and the handler could never fire. Each of those handlers was a
character-for-character copy of the neighbouring onCancel, so they are gone
rather than translated.
Rich titles now sit inside DialogHeader with DialogTitle carrying the heading
text, instead of the whole header block being nested inside DialogTitle. That
had put an h2 inside another h2, which is invalid and gave one dialog two
headings.
UserEnvVarsModal loses its formGeneration counter. Remounting the form when the
modal finished opening only mattered because antd kept a closed modal's
children mounted; Base UI unmounts them, so reopening is blank on its own. Its
test helper had encoded that remount as a timing assumption, so the file now
states the requirement outright and checks that reopening shows an empty field.
CreateMCPServer's cancel test read the tool list while the modal was closed,
which only worked because forceRender kept it mounted. It now asserts what a
user can actually observe: the panel is gone while closed, and reopening brings
back an empty URL and no tools.
Unmounting an open Base UI dialog leaves its scroll lock on <html> and <body>,
which survives cleanup() and makes every later test in the file see a locked
page where popups compute pointer-events: none and clicks quietly do nothing.
The shared setup now releases it.
* refactor(ui): finish the antd Modal migration onto the shared Dialog
Fifteen files whose Modal relied on antd's built-in footer. okText, cancelText,
onOk, okButtonProps, cancelButtonProps and confirmLoading collapse into two
explicit buttons in a DialogFooter, with danger becoming the destructive
variant and the various loading flags becoming disabled plus aria-busy. The one
okButtonProps that also hand-set a red background drops it, since the variant
already carries that.
add_guardrail_form keeps its own chrome, so its DialogContent turns off the
built-in close button and the padding, and its heading becomes the DialogTitle.
Under antd it passed title={null} and had no accessible name at all.
Modals that positioned themselves near the top of the viewport needed
translate-y-0 alongside top-8, because DialogContent centres itself with a
transform that top alone does not undo.
TeamGuardrailsTab's test reached its Mode select by index into every combobox on
the page. A modal dialog hides the rest of the page from assistive technology,
which antd never did, so the count changed and the index pointed at the wrong
control. It asks for the field by label now.
The mask-dismissal test drove antd's .ant-modal-wrap class directly; it uses the
overlay slot our own component exposes, and still fails if
disablePointerDismissal is dropped.
CreateUserButton stays on antd. Its Modal converts cleanly, but the colocated
test file then fails a varying handful of cases, and the cause sits in the test
file rather than the component, so it wants its own change.
Pruning suppressions for the touched files also cleared four
react-hooks/set-state-in-effect entries on CreateMCPServer that were already
stale before this branch.
* test(ui): pick Base UI select options through the shared helper
React 19's flush timing loses the race this test was relying on: the option
lands in the DOM one render before its positioner drops pointer-events: none,
so user-event refused the click. tests/test-utils already exports
chooseSelectOption for exactly this, added alongside the React 19 upgrade.
The chart shipped ghcr.io/berriai/litellm-database as its image default,
with a comment offering it as the "optimized image with database". That
distinction no longer exists: Dockerfile and docker/Dockerfile.database
differ only in comment text and one builder-stage apk package, and both
published images bake the prisma CLI, engines, schema.prisma and
prisma_migration.py into /app, so either one runs the migrations job.
Point the default at the canonical image the release notes, the cosign
verification instructions and the chart's own README already name, and
update the chart's tests and README so nothing still refers to the
legacy repository.
* refactor(ui): migrate the remaining dashboard pages off antd
Converts the teams, usage, guardrails, vector stores, cost tracking,
agents, policies, login and onboarding screens onto the shadcn
primitives, including the team info tab shell and the virtual keys
hover cards.
* fix(ui): close out the antd migration's failing type checks and tests
Alert and Badge were missing the success and info variants their call
sites already used. Combobox dropped disabled because Base UI merges the
primitive's own props over the render child, so the flag never reached
the input, and the guardrails status filter had no accessible name, which
left two comboboxes indistinguishable to the tests.
The remaining test updates swap antd's title-based queries for the roles
the shadcn controls expose.
The reasoning override's floor was pinned to tier_boundaries.simple_medium,
so an operator could not restore the unconditional promotion nor raise the bar
independently of the SIMPLE/MEDIUM cut. Setting reasoning_override_min_score
was accepted and echoed back by /model/info, because the config model allows
extra keys, while routing ignored it.
Resolve the floor through one accessor that falls back to simple_medium when
the field is unset, so moving that boundary still moves the floor with it, and
an explicit 0 is a real floor rather than an absent one. Record the resolved
value on the routing decision so a logged row states the floor that applied,
which is also what lets the Admin UI stop hardcoding the copy PR #37500 added.
* fix(auth): resolve bare model names against wildcard deployments in model access groups
* test(e2e): cover model access group permission checks on keys and teams
`lite up` already patches ~/.claude/settings.json, but only for as long as it
runs in the foreground, and it restores the original file on exit. Users
proxying Claude Code through LiteLLM therefore have to re-wire it by hand after
every login.
--config-claude makes that write persistent. It reuses the settings shape
`lite up` writes (env.ANTHROPIC_BASE_URL plus an apiKeyHelper invocation),
preserves every unrelated key, creates the file when missing, and writes it
atomically with owner-only permissions. Plain `lite login` is unchanged.
Reaching the credential through apiKeyHelper rather than copying it into the
file means a later login refreshes it with no further action, and keeps the
short-lived CLI token out of settings.json entirely.
The shared parts of the settings-file handling move from up.py into a new
claude_settings.py, since up.py imports auth.py and so auth.py cannot import
up.py back. That module now also owns the registry of commands that can be
temporarily managing the file, so the persistent write refuses while either
`lite up` or `lite autoroute up` holds a backup it would later restore over
this write.
Because this write has no backup and no `lite down`, it is stricter than
`lite up` about the user's file: it writes through a symlinked settings.json
rather than replacing the link with a regular file, and it refuses rather than
silently discarding a non-object `env` value.
Also fixes the apiKeyHelper command itself: --base-url belongs to the
top-level `lite` group, so `lite auth print-token --base-url X` is rejected by
click with "No such option". Every settings file `lite up` has written carries
that malformed command, which makes the helper return nothing and every Claude
Code request lose its token. The existing tests only string-matched the
generated command, so the new tests parse it through the real CLI instead.