Commit graph

4874 commits

Author SHA1 Message Date
Yuneng Jiang
1b5f40b47d
refactor(ui): move the model alias manager onto design tokens and shadcn controls
The shared alias editor was light-only: 18 hardcoded palette classes across
its headings, table cells, config preview and five raw controls, so it read
grey-on-grey against the dark dashboard theme.

Its five buttons also carried no type attribute, and all three consumers
render it inside a real antd Form with an onFinish. Measured against the
pre-change file in jsdom with a liveness gate on both sides: clicking Add
Alias fired the parent's onFinish once, and a row action fired it once more.
So editing a model alias inside the create-team, team-update or create-key
form also submitted that form. shadcn Button renders type="button", which
closes the path.

Swapping the raw input and button elements for the shadcn primitives is what
makes the colours resolve, since a bare element needs the whole token set
hand-written to work in both themes.
2026-08-18 15:35:26 -07:00
Yuneng Jiang
48986da99c
test(ui): pin the placeholder branch alongside the label branch
`items` feeds `hasNullItemLabel` as well as label resolution, and that
selector decides whether `Select.Value` renders the placeholder. None of
the twenty fixed sites has a null-valued item, so their placeholders are
unchanged, but nothing pinned that.

Cover both halves: the placeholder still renders when nothing is
selected, and a null-valued entry in `items` takes over from it. The
second case is the trap to avoid when adding `items` to a Select that
relies on its placeholder.
2026-08-18 15:34:03 -07:00
Yuneng Jiang
653dc5c601
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/modest-hodgkin-5775d7 2026-08-18 15:30:37 -07:00
Yuneng Jiang
4cbceb565b
fix(ui): show select labels on the trigger instead of raw values
Base UI's Select.Value resolves an option's label only when the root
carries an `items` prop or the Value has a child. `resolveSelectedLabel`
in @base-ui/react/internals/resolveValueLabel.js falls through every
branch to `stringifyAsLabel(value)` otherwise, and `state.items` is
written only from the root's `items` prop, so the `<SelectItem>` children
rendered inside `<SelectContent>` never populate it.

A self-closing `<SelectValue />` on a root without `items` therefore
renders the raw value once something is selected. The placeholder branch
still works, so the trigger looked right until the user picked an option
and then showed `development` for Development, `LiteLLM_VerificationToken`
for Keys, `all` for All Actions, and `24h` for Daily.

Pass `items` at the 20 affected sites, using the array form the other 52
call sites already use. Where a literal option sat alongside mapped ones,
build one array and map the options over it so the labels and `items`
cannot drift.

The record-map form is avoided deliberately: `items[value]` on an object
literal reaches Object.prototype, so a dynamic value named `toString`
would resolve to a function and React would throw on it. The array form
matches with `.find` and has no prototype lookup, which matters where the
values are user-supplied model groups, team ids and key aliases.

Also replace the option lookup in CompetitorIntentConfiguration's test
helper, which searched by text and clicked the last match. That match is
now ambiguous because the trigger carries the label too, and the helper
already flaked roughly one run in six before this change.
2026-08-18 15:29:35 -07:00
Yuneng Jiang
aebdba510f
refactor(ui): move the team member search modal off antd Form
Ports user_search_modal from antd Form to react-hook-form plus the shadcn
kit, keeping the antd Modal and Alert shells. Payload parity was proven by
rendering the antd original beside the migration in one describe.each: an
untouched submit yields the same three keys with the identity fields
undefined, and picking an option yields the same email and id on both sides.

antd Select swallows Enter, so the original never submitted from a field.
The Base UI combobox does not, which added an Enter-to-submit path; the
inputs now swallow Enter and both sides measure zero submits from every
field with one from the button.
2026-08-18 15:27:50 -07:00
ryan-crabbe-berri
b13fabe9c2
refactor(ui): move the cache settings and playground model selector off tremor (#37323)
* refactor(ui): move the budget, cache, cost tracking and playground forms off tremor

Swaps tremor Accordion for the Base UI Collapsible, TextInput for the shadcn Input and the two
tremor Buttons for the shadcn Button across the budget modals, cache settings, the cost tracking
add-provider and add-margin forms and the playground model selector. The accordion bodies keep
tremor's unmount-when-closed semantics, since headless-ui's Disclosure.Panel and Base UI's panel
both default to unmounting, so the antd fields inside behave exactly as before.

The two cost tracking buttons are the one deliberate behaviour change. tremor's Button renders a
bare button with no type, so inside the antd Form that wraps both components it was an implicit
submit on top of its own onClick. For the discount form that meant every click ran
handleAddProvider twice, once from onClick and once from the form's onFinish, and for the margin
form the submit did nothing at all because that Form has no onFinish. The shadcn Button forces
type="button", so the add now fires once from onClick alone and no type="submit" is added back.

The three inputs that used onValueChange now read e.target.value, and each one gained a test that
types into it and asserts the reported string, so the wiring cannot silently regress. The cache
settings suite gained a collapse contract test that the advanced sections are absent until the
section is expanded. Prunes the six no-restricted-imports suppressions these files no longer need,
each dropping from two to one for the antd import that stays.

* fix(ui): keep enter to submit on the cost tracking add forms

The shadcn Button forces type="button", so converting the two tremor buttons left both cost
tracking modals with no submit button at all. Each form still holds two fields that block
implicit submission, the provider select's search input and the value input, so pressing Enter
stopped adding anything. Both buttons get type="submit" back.

For the discount modal that alone would restore the double add the conversion had just removed,
since a submit also ran the form's onFinish, so the parent drops onFinish and the now dead
handleFormSubmit. Click and Enter both go through onClick exactly once. The margin form's parent
never had an onFinish, so restoring the submit type there is enough on its own.

Adds three cases to the cost tracking settings suite: the discount add fires once from a click,
the discount add fires once from Enter, and the margin add fires once from Enter. Dropping either
type="submit" kills the Enter cases and putting onFinish back makes both discount cases see two
calls. Also drops the two empty placeholders on the budget modals that only existed to suppress
tremor's "Type..." default.

* fix(ui): restore Enter-to-submit on the margin modal

The tremor Button rendered a bare native button, which defaults to
type="submit", so Enter in the percentage field submitted the margin
modal. The shadcn Button wraps Base UI, which defaults to type="button",
and the migration also replaced the margin modal's form element with a
plain div, so Enter went inert while the visually identical discount
modal kept working.

Give the margin modal the same form wrapper the discount modal already
has and mark its action button as the submit button. Also move the cache
settings advanced-section test into the integration file, where a test
that renders the real component tree belongs.
2026-08-18 15:26:40 -07:00
Mateo Wang
6b7adf011e
Merge pull request #37355 from BerriAI/litellm_ultrafast_service_tier_cost
fix(cost_calculator): recognize the ultrafast service tier in cost calculation
2026-08-18 15:25:53 -07:00
ryan-crabbe-berri
9c38d6d002
refactor(ui): move the teams page and team detail views off tremor (#37317)
* refactor(ui): move the teams page and team detail views off tremor

Swaps the tremor Accordion, Badge, Button, Card, Grid, Text, TextInput and
Title usages in Teams.tsx, TeamInfo.tsx, EditMembership.tsx and
LoggingSettings.tsx for the shadcn layer. The team model badge colour map
becomes a variant map: all-proxy, direct and access-group chips render as
secondary and no-default as outline, so the kind is now conveyed by the
tooltip rather than by hue. The LoggingSettings top decoration is drawn with
border-t-4 border-t-blue-500 and its light red Remove button becomes a ghost
button with red text. antd stays in place for this pass and the eslint
no-restricted-imports counts for the four files ratchet down by one each.

* fix(ui): keep the password reveal and model badge hues in the team views

The tremor TextInput rendered a show/hide button for every password field, so
the shadcn swap silently dropped it for the sensitive logging parameters. The
password branch now renders an InputGroup with an eye toggle, matching the
pattern email settings already uses, and a test pins the masking.

The team model chips go back to four distinct colours by way of the shared
StatusBadge, so a directly granted model still reads differently from an
access group one without hovering for the tooltip.

The hand-drawn blue accent on the logging integration card is dropped: the
tremor decoration it replaced never rendered, because the caller's own border
classes won the class merge, so the bar was new rather than preserved.

* fix(ui): drop the dead empty placeholder on the team name field

The team name input carried placeholder="" only to suppress tremor
TextInput's default "Type..." hint. shadcn Input has no default
placeholder, so the empty string does nothing and the field now relies on
its label, matching the other converted create-team fields.
2026-08-18 22:15:41 +00:00
yucheng-berri
0b82b087fd
feat(team-callbacks): add DELETE /team/{team_id}/callback/{callback_name} (#37331)
Removes one named callback from a team and leaves the team's other callbacks
registered and firing. Before this, the only removal route was
POST /team/{team_id}/disable_logging, which clears every callback at once, so a
tenant sharing a team could not deregister its own integration

The handler filters metadata["logging"], keeps the survivors encrypted, refreshes
the cached team so the removal applies to keys that are already live, and emits a
redacted audit row, matching what the add and disable routes do

Resolves LIT-5161
2026-08-18 14:56:45 -07:00
ryan-crabbe-berri
2059033352
refactor(ui): move the add model and credential forms off tremor (#37325)
* refactor(ui): move the add model and credential forms off tremor

TextInput becomes the shadcn Input, Text becomes a sized paragraph, the
advanced settings Accordion becomes a bordered Collapsible, and the
Team-BYOK Switch moves to the Base UI switch with onCheckedChange plus an
aria-label. That switch stays wrapped in a span so the antd Tooltip still
shows on hover while it is disabled for non-premium users, matching what
the tremor wrapper div did. provider_specific_fields keeps antd's
Input.TextArea through an AntdInput alias so its antd import stays a single
statement. Prunes the tremor no-restricted-imports suppressions these files
no longer need.

* fix(ui): keep the reveal toggle on the provider secret fields

tremor's TextInput drew its own show/hide button whenever the type was
password, and the shadcn Input is a plain native input, so every provider
secret this form renders (API keys, client secrets, and the same fields
inside the add credential modal) lost that affordance.

Puts the password branch on antd's Input.Password, which is what the other
dynamic credential forms in the dashboard already use, so the reveal comes
back and the antd Form.Item wiring stays untouched. The control chain moves
into an early-return helper, which keeps the extra branch from pushing the
file past its no-nested-ternary budget and drops that count from 5 to 3.
2026-08-18 14:47:04 -07:00
ryan-crabbe-berri
92cf577f82
refactor(ui): move the virtual key create and edit forms off tremor (#37324)
* refactor(ui): move the virtual key create and edit forms off tremor

Swaps the tremor primitives in the create-key modal, the key edit view and
their two shared field components for the in-repo shadcn layer: Accordion
becomes Collapsible, Grid/Col become grid divs, Text/Title become real
paragraphs and headings, and TextInput becomes the shadcn Input. antd stays
where it already was, so the antd Input keeps rendering the textareas and
hidden fields under the AntdInput alias.

Two behavioural notes. The key edit view's Save Changes button keeps saving
because it carries an explicit type="submit"; Base UI's button otherwise
defaults to type="button". Its Cancel button now really is type="button",
where the tremor one had no type at all and so submitted the form on top of
calling onCancel, and a test pins that.

Base UI's Collapsible panel unmounts while closed exactly like the headless
Disclosure panel tremor wrapped, so the create-key tests now open Optional
Settings before querying inside it instead of relying on a tremor mock that
flattened every accordion.

* refactor(ui): hoist the create-key collapsible header classes and keep optional settings a heading

Names the repeated Collapsible trigger and chevron class strings the way the
cost tracking conversion does, since nine copies of each lived in this one
file, wraps the Optional Settings trigger in an h3 so the section keeps a real
heading next to Key Ownership and Key Details, and drops the placeholder=""
that only ever existed to suppress tremor's default hint.
2026-08-18 14:46:41 -07:00
ryan-crabbe-berri
28266d90e7
feat(vector_stores): add Valkey as a managed vector store provider (#37002)
* feat(vector_stores): add Valkey as a managed vector store provider

Adds a valkey provider for managed vector stores, searchable via the
valkey-search module over RESP. Introduces BaseDirectVectorStoreConfig
for datastores that execute searches directly instead of building an
HTTP request, and refactors the valkey semantic cache to share the new
connection URL helper. Registered in the provider enum, router params,
proxy config registry, Admin UI Add Vector Store modal, and provider
endpoint support matrix.

* fix(vector_stores): join list queries and bound valkey socket timeouts

Review feedback: multi-string queries are now space-joined like every
other embedding-based provider instead of dropping all but the first,
and the request timeout is threaded through the direct vector store
interface into bounded socket_connect_timeout / socket_timeout values
on both redis clients so an unreachable Valkey host cannot pin proxy
workers until the OS TCP timeout.

* chore(ui): regenerate schema.d.ts for valkey vector store fields

* docs(ui): make the Valkey vector store setup note and field tooltips explicit

* feat(ui): pick the Valkey embedding model from the proxy's models like Milvus

* fix(ui): number the setup steps in the vector store provider alerts
2026-08-18 21:45:22 +00:00
Yassin Kortam
3fe0201d40
fix(proxy): let org admins view their organization's usage (#37235)
An internal user who administers an organization saw an empty
Organization Usage dashboard and had to be promoted to proxy admin to
see any of it.

Two independent gates were closed on them. The route layer rejected
GET /organization/daily/activity with 401 before the handler ran, since
the route belonged to no list a non-proxy-admin can reach, and the
handler's own org-admin scoping was therefore dead code. In the
dashboard, viewOrganizationUsage was granted by session role alone, and
an org admin's session role is internal_user, so the Organization Usage
option never rendered and its data fetch stayed disabled.

The route now sits in self_managed_routes, where the handler restricts
results to organizations the caller is ORG_ADMIN of and 403s on any
other org, and viewOrganizationUsage joins the existing per-capability
org-admin allowance that already covers viewDeletedTeams.

A caller who administers no organization resolves to an empty id list
rather than to None, so the organization-alias lookup is scoped by that
same list instead of reading the whole table.

The Usage page falls back to the global view when org-admin membership
is revoked while it is open, so the selector never keeps a value it no
longer offers.
2026-08-18 14:44:36 -07:00
yuneng-jiang
c180849210
refactor(ui): migrate the MCP per-user env vars, toolset and tool arguments forms to react-hook-form and shadcn (#37349)
* refactor(ui): migrate the MCP per-user env vars modal to react-hook-form and shadcn

Moves UserEnvVarsModal off the antd Form store onto react-hook-form with a
zod schema built from the server's declared per-user variables, and swaps
antd Input.Password for the shared PasswordInput.

The submit payload is unchanged: every declared variable is still sent as a
key, trimmed, with an untouched field sending an empty string. antd reset
the store from the modal's afterOpenChange; the migrated form reproduces
that by remounting on the same callback, so reopening still starts blank.

Adds UserEnvVarsModal.test.tsx, which was written against the antd original
and proven green before any production change, then re-run unedited against
the migration. Two further cases cover the reveal toggle, which antd
provided through visibilityToggle.

* refactor(ui): migrate the MCP toolset create and edit form to react-hook-form and shadcn

Moves the toolset name and description fields off the antd Form store onto
react-hook-form with a zod schema, and takes the surrounding panel onto
semantic colour tokens so the tab renders in dark mode. The purple selected
tool styling keeps its hue and gains dark variants rather than flattening
to neutral.

Payload is unchanged: create still sends toolset_name, description and
tools, an untouched description is still the empty string rather than
undefined, and the tool selection is still held outside the form. The antd
form carried no onFinish and its buttons sit outside the form element, so
the migrated form keeps submit on the footer button and neutralises its own
submit rather than introducing Enter to save.

Adds MCPToolsetsTab.test.tsx, proven green against the antd original before
any production change and re-run unedited afterwards.

* refactor(ui): migrate the MCP tool arguments form to react-hook-form and shadcn

Moves the schema-driven tool argument form off the antd Form store onto
react-hook-form. Validation moves to an explicit resolver that reproduces
antd's rules field by field, including the per-field required message and
the JSON object and array messages, and the same resolver is reused by
getSubmitValues so the imperative path and the rendered errors cannot
disagree.

getSubmitValues still rejects with a plain object carrying errorFields
rather than an Error. ChatUI branches on `err instanceof Error` to choose
its toast, so rejecting with an Error would have silently changed the
message the user sees. That is pinned by a test proven green against the
antd original with a Form.Item liveness gate, and proven red when the
rejection is switched to an Error.

Enum and boolean fields keep the antd Select, whose allowClear has no
shadcn equivalent; dropping it would remove the only way to unset an
optional enum. Everything else moves to the shadcn Input and Textarea and
onto semantic colour tokens.

Adds MCPToolArgumentsForm.test.tsx covering the string, integer, number,
boolean, object, array, nested-params and string-schema paths, written
against the antd original and re-run unedited afterwards.

* refactor(ui): drop the decorative antd Form.Item from the MCP connect guide

The connect guide rendered a single antd Form.Item with no field name and no
Form ancestor, so it registered nothing and carried no payload; it was only
supplying bottom margin. It becomes a div with the same margin class, which
removes the file's last antd Form dependency.

Also takes the guide onto semantic colour tokens so it renders in dark mode.
The blue and green callouts keep their hue and gain dark variants rather
than flattening to neutral, since the colour carries meaning there.

* chore(ui): ratchet the MCP tool arguments form lint suppressions

The react-hook-form migration removed four of the five nested ternaries
in MCPToolArgumentsForm, so lower the grandfathered count to match and
hoist the one inline object literal the budget rule flags.

* test(ui): classify the MCP modal batteries as integration tests

Both render a real component tree down to the form controls and stub only
the network boundary, which is the repo's definition of an integration
test rather than a unit test. The tool arguments battery renders a single
module in milliseconds, so it stays unsuffixed.
2026-08-18 21:41:53 +00:00
yuneng-jiang
2502776700
refactor(ui): migrate agent forms to react-hook-form and shadcn (#37357)
* refactor(ui): migrate agent forms to react-hook-form and shadcn

Move the agent create wizard and the agent detail editor off antd Form onto
react-hook-form with shadcn primitives. The two parents share three children
(agent_form_fields, dynamic_agent_form_fields, cost_config_fields), so the whole
form graph migrates in one commit.

The submit payload is unchanged. antd validates and submits only fields that are
currently mounted, and its Collapse panels mount lazily on first open and then
stay mounted, so a payload depends on which panels the user ever expanded.
react-hook-form keeps every registered value instead, so the panels track their
own mounted set and the detail editor filters the never-opened panels back out
before building the request. shouldUnregister stays off, since it drops values
for collapsed panels rather than merely excluding them from submit.

Tags, examples and forwarded header names move from antd tags-mode selects to a
combobox in the kit. Base UI clears the combobox input on blur before the blur
handler runs, so the pending text is committed from the input-clear reason,
which is what antd did when the field lost focus.

The agent type picker stays on antd Select: its popup content is not part of the
form graph, and the Base UI popup opens a macrotask later, which the existing
unit test cannot observe.

* refactor(ui): use the shared PasswordInput in the agent forms

* refactor(ui): drop the narration comments from the agent wizard
2026-08-18 14:40:42 -07:00
yuneng-jiang
3c82791968
refactor(ui): migrate the guardrail forms to react-hook-form and shadcn (#37364)
Moves the guardrail form graph off antd Form onto react-hook-form with the
shadcn field primitives. The graph migrates atomically: add_guardrail_form and
guardrail_info own the form instances, and guardrail_provider_fields,
guardrail_optional_params and LLMJudgeFields are field groups rendered inside
them, so an antd parent could not host a react-hook-form child either way.

The submit payload is unchanged. Two characterization suites, 25 cases, pin it:
each case was written against the antd original, proven green there, and passes
unedited against the migration.

Behaviour worth calling out. Nested provider fields are keyed with ":" rather
than "." so they stay flat keys the way antd stored them, since a dotted name
is a lodash path in react-hook-form and would have started shipping a nested
object. antd InputNumber clears to null and clamps on blur where a native
number input does neither, so the judge criteria weights reproduce that. The
guardrail_info submit handler is read through a ref at validation-resolution
time, matching how antd re-read onFinish, so a submit fired by the same click
that changed state still sees that state.

Two antd behaviours are preserved rather than fixed, both worth their own
follow-up: deselecting every mode blocks Next instead of falling back to the
seeded default, and a required provider-specific field is never enforced at
create time. One is fixed and disclosed: a failed validation now names the
problem instead of rendering "[object Object]", and the guardrail name label is
associated with its control, which it was not before.

MultiSelect takes an optional id so the label can point at the control.
SkipMessageSelect was duplicated verbatim in both parents and now lives in the
shared field module.
2026-08-18 14:39:36 -07:00
yuneng-jiang
56d926ac54
test(ui): cover the edit project modal's required-field validation (#37363)
The edit project modal had seven payload tests and none of them cleared a
required field, so rewiring its submit button to send raw form values with
validation skipped left all seven green. The validation layer was untested
on that path while looking well covered.

Adds the missing case: clearing the project name blocks the save and shows
the error. It passes on current code, so the behaviour was always correct,
and it fails under the validation-bypass mutation, so it has teeth.
2026-08-18 14:35:43 -07:00
yuneng-jiang
2e44689ea0
refactor(ui): migrate SSO, SCIM and vault forms to react-hook-form and shadcn (#37347)
* refactor(ui): migrate the SCIM and Hashicorp Vault forms to react-hook-form and shadcn

Both forms move off antd Form onto react-hook-form plus the shadcn kit, with
neutral greys on semantic tokens and coloured callouts keeping their hue behind
a dark variant, so both are dark-mode ready.

Neither file had a test, so each one gained a characterization test written
against the antd original and proven green there before any source changed. The
same files pass unedited after the migration.

Two payload details the migration has to reproduce rather than tidy up. antd
onFinish emits a mounted but never-set field as a key holding undefined, and the
vault handler turns each of those into an empty string to clear it server-side,
so every rendered vault field is seeded to an empty string rather than left
absent. And the vault form still refuses to seed or send a blank sensitive
field, so a stored secret stays write-once.

SCIM keeps its Enter-to-submit path: its footer button was a Tremor Button
carrying an explicit type=submit, which Tremor forwards, so the form could
already be submitted from the keyboard.

* refactor(ui): migrate SSO, SCIM and vault forms to react-hook-form and shadcn

Moves the SSO settings form graph, the SCIM token form and the Hashicorp
Vault config form off antd Form onto react-hook-form plus the shadcn
FormField primitives, keeping today's submit payloads byte for byte.

The SSO graph migrates atomically because an antd Form.Item parent cannot
host a react-hook-form child. BaseSSOSettingsForm now owns the shared
schema, the field components and a mounted-field picker that reproduces
what antd's onFinish actually sent: rc-field-form validates only mounted
entities, so hidden provider and mapping fields never reached the wire.
submitMountedSSOValues keeps that behaviour explicit instead of leaving it
to which fields happen to be rendered.

EditSSOSettingsModal seeds through an explicit mapper rather than
spreading the server record, so a field the form does not declare cannot
leak into an update. The vault modal keeps its two distinct behaviours for
blank inputs, clearing non-sensitive fields with an empty string and
omitting blank secrets so a stored credential survives a save.

* fix(ui): render SSO select labels and guard seeding completeness

The migrated Select triggers rendered the raw stored value rather than the
option label, so an untouched Default Role showed "internal_user" and a
chosen provider showed "okta". Base UI resolves a label only through a
Value function child, so both selects now format through the same option
list that builds their items.

Adds three characterization cases the earlier suite did not reach: an
empty required provider credential blocks the submit and names the field,
reopening the modal against a different stored config replaces every
seeded value rather than merging, and every field the provider forms can
mount survives the seeding mapper. The last one fails by name when a key
is dropped from that mapper, which is the class of defect an explicit
allowlist invites.

* test(ui): cover the edit SSO modal against its real form tree

The existing modal test stubs BaseSSOSettingsForm out, so no field ever
registers and validation passes trivially. Rewiring the Save button to
call the submit handler with raw form values, skipping both validation and
the mounted-field filter, left all 112 tests green.

Adds an integration test that renders the real modal, the real form body
and the real antd shell, stubbing only the two data hooks. Clearing a
required credential now blocks the save and names the field, and a valid
save asserts the exact payload. The bypass mutation fails both cases, and
dropping only the mounted-field filter fails the payload one.
2026-08-18 14:30:17 -07:00
ryan-crabbe-berri
ef72e1afd6
refactor(ui): move the tag and vector store views off tremor (#37311)
* refactor(ui): move the search tool, tag and vector store views off tremor

Swaps the tremor Button, TextInput, Text, Title, Card, Badge, Accordion and
TabGroup usages in the search tools, tag management and vector store views for
the shadcn primitives, following the tremor conversion cookbook. Both tab panels
in the vector store info view carry keepMounted so the tester's state survives
switching to Details and back, and a test pins that contract.

tremor's Button renders a bare button element with no type, so inside the antd
Forms here the Test Connection button in the create search tool modal and the
Cancel buttons in the tag editor and the vector store form were implicit submit
buttons. The shadcn Button defaults to type="button", so they can no longer
submit, and every button that is meant to submit now carries an explicit
type="submit". Clicking Test Connection and Cancel against a live proxy on the
merge base already only ran the connection test and only cancelled, so this
closes a latent trap rather than changing what the pages do.

Decrements the seven no-restricted-imports suppression counts these files no
longer need, leaving the antd half of each entry in place for the antd pass.

* refactor(ui): use the line tab strip in the vector store detail view

The detail view's tabs kept the default pill TabsList, so it no longer matched
the underline strip tremor rendered before the swap or the one the vector store
list view already uses.

* test(ui): pin the tag and vector store form save and cancel buttons

Cancel in the tag editor used to submit the form and save the tag because the
tremor button carried no type; nothing in the suite failed if it started doing
that again. Each form now has a pair of cases: Save Changes and Create still
submit, and Cancel leaves the record alone.

* fix(ui): keep the reveal toggle on the search tool API key

tremor's TextInput drew its own show/hide button whenever the type was
password, and the shadcn Input is a plain native input, so the straight
prop pass-through silently deleted that affordance from the create search
tool form's API key field.

Puts it on antd's Input.Password instead, which is what the sibling edit
form in the same directory (SearchTools.tsx) already uses for the very
same field, so the reveal survives and the two forms behave the same.
The file already imports antd, so this adds no import and no suppression.
2026-08-18 14:29:06 -07:00
yuneng-jiang
573aa61084
refactor(ui): move the shared key form controls off antd onto shadcn (#37348)
* test(ui): pin the antd submit payloads for the key create and edit forms

Characterization only, no source change. Both suites are green against the
current antd components, so they can gate the react-hook-form migration that
follows without being edited.

key_edit_view had no exact-payload assertion, only objectContaining, so nothing
caught a form that started sending server-only key fields. The new case asserts
the whole object.

create_key_button's existing suite runs against a hand-rolled antd fake and
stubs out KeyLifecycleSettings and RateLimitTypeFormItem, so neither the real
store nor those two controls were covered. The new file drives the real antd
form and pins the network payload instead.

* refactor(ui): move the shared key form controls off antd onto shadcn

KeyLifecycleSettings and RateLimitTypeFormItem each owned an antd Form.Item and
took the parent's FormInstance as a prop, so neither could be hosted by anything
but an antd form. That is what made the key create and edit forms one
inseparable migration unit.

Both are now presentational: they take value and onChange and let the parent own
the binding, so an antd Form.Item and a react-hook-form FormField can host them
equally. The two parents keep their antd forms for now and pass the binding down
unchanged, which is why every existing payload assertion still holds.

Controls are shadcn Select, Input, Switch and Checkbox on semantic colour
tokens, so both are dark-mode ready. The rotation notice keeps its blue hue and
gains a dark variant rather than flattening to a neutral.

The form prop the two components took was already inert: antd dispatches its own
store update before calling the child's onChange, so setFieldValue was writing a
value the store had just been given.

KeyLifecycleSettings.test.tsx keeps every assertion; only the harness moves the
duration binding up into a Form.Item, and one case disables user-event's
pointer-events check because Base UI leaves a reopened select popup inert under
jsdom, reproduced on a bare shadcn Select with none of this code involved.

* test(ui): pin the role-gated key fields and tidy the new assertions

Adds the case that proves policies and prompts leave the payload entirely for a
role that cannot see them, which a react-hook-form port would otherwise start
sending from defaultValues. Green against antd like the rest.

Also hoists the two large expected payloads into named constants and drops two
unused exports, so the lane adds no new lint-budget pressure.

* fix(ui): give the key expiry input and Never Expire checkbox separate labels

The expiry label carried htmlFor for the duration input while also wrapping
the Never Expire checkbox and its own label, so the two controls shared one
ambiguous association. Splitting the row into a plain container with a label
per control makes each name resolve to the control it describes.

Also drops the prop and test comments added in this branch, which the
repository comment policy does not allow.

* test(ui): pin the create-form expiry binding to the generate payload

create_key_button coalesces a missing or blank duration to null before it
calls keyCreateCall, so the key is present in the payload whether or not the
control is bound to the form. Every existing case stayed green with the
Form.Item removed, which left the binding uncovered.

The new case opens Key Lifecycle, types an expiry, and asserts it arrives as
that value. Proven red with the Form.Item removed and green with it restored.
2026-08-18 14:28:03 -07:00
yuneng-jiang
76ff0d5351
refactor(ui): migrate the model settings and credential rotation modals to react-hook-form and shadcn (#37342)
* refactor(ui): migrate the model settings and credential rotation modals to react-hook-form

Both modals owned a self-contained antd FormInstance with no shared form
children, so each migrates on its own without touching the add_model graph.

ModelSettingsModal keeps its antd Modal shell, footer buttons and Skeleton
placeholder. The single store_model_in_db field becomes a shadcn Switch inside
a FormField, the antd Form.Item tooltip stays a hover tooltip rather than
becoming always-visible description text, and the remount-on-new-config
behaviour that the antd `key` provided is now RHF's `values` option.

UpdateModelCredentialsModal keeps the antd Modal and warning Alert. The
Input.Password becomes an InputGroup with an Eye/EyeOff reveal toggle so the
reveal affordance survives, and the required rule ports to the same message.
Both submit paths stay exactly as they were: Enter still submits here because
the antd Form had onFinish and a real submit button, while the settings modal
keeps submitting only from its footer button.

* refactor(ui): reuse the shared PasswordInput in the credential rotation modal
2026-08-18 14:28:00 -07:00
ryan-crabbe-berri
d9e9270574
refactor(ui): move the internal user create, edit and detail views off tremor (#37309)
* refactor(ui): move the internal user create, edit and detail views off tremor

The tremor SelectItem rows nested inside the antd role Select become
antd Select.Option so the antd control keeps driving the form; the antd
removal is left to the antd pass. The user detail tabs move from a
numeric index to overview/details slugs (the public initialTab number
prop is unchanged and mapped at the boundary), every panel is
keepMounted, and a test pins that contract. The per-team remove button
keeps tremor's light red look as a ghost icon button rather than a
solid destructive one. Prunes the tremor no-restricted-imports
suppressions these files no longer need.

* refactor(ui): keep the user detail tab strip on the line variant

The tremor TabList defaulted to the underline strip, so the shadcn
TabsList needs variant="line" to keep that look instead of the filled
segmented pill; the triggers pick up the same active classes the users
page strip right above already uses. Also drops the vestigial empty
placeholder on the embedded create user email field, which only existed
to suppress tremor's built-in "Type..." hint.
2026-08-18 21:17:36 +00:00
tin-berri
1f4acbb924
feat(complexity_router): custom classifier plugins via classifier_type 'custom' (#37249)
* feat(complexity_router): custom classifier plugins via classifier_type 'plugin'

Adds a third classification mode where an operator-supplied hook decides the
tier instead of the heuristic scorer or the LLM classifier. The hook implements
an async classify(context) returning a tier name (built-in value, tier_labels
label, or tier_definitions name) or None to decline; failures, timeouts, and
unknown tiers fall back exactly like a failed LLM classifier. The context
carries the request messages and metadata, including caller identity, so a
plugin can route by team, spend, or any business rule.

The plugin resolves from a dotted path at proxy startup with a load-time check
that classify is a coroutine function, and is closed off over HTTP like the
routing plugins list. Routing decisions record the new classifier_plugin cause.
tier_definitions now accepts classifier_type 'plugin' alongside 'llm'.

* fix(proxy): resolve plugin dotted paths in _delete_deployment before hashing ids

The db-sync reconcile re-reads the raw config and hashes litellm_params to
compute which ids the config wants served, but the router's ids were hashed
from the resolved params where plugin dotted paths are live instances. The
mismatched ids made the reconcile evict every plugin-bearing auto-router one
sync after startup, on any proxy with a database connected. This also affected
the existing routing plugins list, not just the new classifier plugin.

Resolving the plugins in _delete_deployment the same way load_config does makes
both sides hash the same canonical form. A plugin module broken on disk at
reconcile time skips cleanup instead of evicting valid deployments, matching
how a get_config failure is handled

* fix(complexity_router): treat non-string plugin verdicts as declines, centralize the empty-mapping sentinel

A hook returning a non-string raised inside resolve_classified_tier outside the
plugin exception boundary, failing the request instead of falling back. Also
moves the read-only empty mapping to constants.py per repo convention and moves
the classifier plugin product docs out of the package README for the docs repo

* refactor(complexity_router): rename the plugin classifier mode to classifier_type 'custom'

The mode value now names the operator's intent while classifier_plugin keeps
naming the mechanism; routing decisions keep the classifier_plugin cause

* refactor(proxy): pin plugin-bearing deployment ids from the raw params instead of resolving in the reconcile

Replaces the previous approach of re-running plugin resolution inside
_delete_deployment, which imported operator modules on every reconcile cycle
and skipped the whole cleanup pass when any one module was broken on disk.
load_config now stamps model_info.id from the raw litellm_params before
resolution swaps dotted paths for live instances, so the reconcile's raw-config
hash matches by construction and needs no resolution at all: a broken module
cannot stall cleanup for unrelated models, and any future param-transforming
resolution is covered by the same pin. _generate_model_id becomes a staticmethod
so the pin can run before the Router exists; its statically dead non-string key
branches are removed. Also documents candidate_models as an informational
snapshot for classifier plugins, unlike the narrowing surface RoutingPlugin
filters

* fix(router): restore _generate_model_id key handling, align classifier context with the routing-plugin pattern

The staticmethod conversion accidentally dropped the non-string-key branches
from _generate_model_id, a silent hash change for any params with non-string
keys; they are restored verbatim. The classifier plugin context now follows
the Router-level routing-plugin recipe exactly: structured messages come from
resolve_structured_messages over the raw messages, and the metadata key comes
from the shared get_metadata_variable_name_from_kwargs helper, which also
replaces the duplicated inline sniff in _pick_model_for_tier. This removes the
raw-or-resolved fallback where a plugin could silently receive resolved
messages when a call site forgot to pass the raw ones

* refactor(router): make generate_model_id public, guard classifier context construction

Two modules legitimately hash deployment ids with the same helper now (Router
and the proxy's config-load pin), so the private name was lying about its
audience and the cross-module call needed a pyright suppression; renaming it
public restores the static safety net. The classifier plugin's RoutingContext
construction moves inside the failure boundary, matching the LLM path where
litellm-side prompt building also falls back rather than failing the request,
and a prompt-only call with no message list is now covered by a test
2026-08-18 14:09:19 -07:00
ryan-crabbe-berri
fca470a64b
refactor(ui): move the MCP server forms and detail tabs off tremor (#37329)
* refactor(ui): move the MCP server forms and detail tabs off tremor

Swaps the tremor Button, TextInput, Title, Text and Tab primitives in the six
MCP server components for the shadcn layer, and leaves the antd Modals, Forms
and Selects alone for the antd pass. In the two files that mix both input
libraries, antd's Input is imported as AntdInput so the shadcn Input keeps its
canonical name.

Two behaviours needed care. Base UI's Button forces type="button", so the
create button in CreateMCPServer now carries an explicit type="submit"; Cancel
and the OAuth authorize button stay non submitting, which also drops the
accidental implicit submit they inherited from tremor. Every TabsContent gets
keepMounted, because Base UI unmounts inactive panels while tremor only hid
them, and a save started from the Cost Configuration tab reads fields that live
in the Server Configuration panel. mcp_server_edit.test.tsx gains a regression
test for that: drop keepMounted and the pending edit never reaches the update
payload.

One affordance is gone: password fields no longer draw tremor's built in reveal
toggle, since the shadcn Input is a plain native input.

Prunes the six no-restricted-imports suppressions these files no longer need.

* fix(ui): keep the reveal toggle on the MCP secret fields

tremor's TextInput drew its own show/hide button whenever the type was
password, and the shadcn Input is a plain native input, so the straight
prop pass-through silently deleted that affordance from five fields: the
create modal's authentication value and the OAuth client id and secret in
both the M2M and the interactive flow.

Puts them on antd's Input.Password instead, which is what every sibling
secret field in this directory already uses (TokenExchangeFormFields,
IdJagFormFields, AwsSigV4Fields and the edit form), so the reveal survives
and the five fields now match their neighbours instead of behaving
differently inside the same form. Both files already import antd, so this
adds no import and no suppression.

* test(ui): pin the connect tab mount contract

mcp_connect's per-card "limit tools to specific MCP servers" toggle lives
in panel local state that feeds the rendered header block, so the panels
have to stay mounted across a tab switch. Base UI unmounts an inactive
panel unless keepMounted is set, and unlike the edit form there was no
test holding that down.

Toggles the header on from the LiteLLM Proxy panel, switches to Cursor and
back, and asserts both the switch and the x-mcp-servers line in the curl
example survived. Dropping keepMounted from that panel fails it.

* fix(ui): keep the MCP tab strips underlined instead of segmented

A bare tremor TabList is variant="line", so the connect strip and the
server settings strip both drew an underlined tab on a full width
divider. Converting them bare turned each into a filled segmented
control, because the shadcn TabsList defaults to the pill.

Both strips now use variant="line" with the divider recipe, and the
connect strip gets back the grey rounded box tremor drew around its four
tabs.
2026-08-18 21:07:36 +00:00
yuneng-jiang
5997ef0423
refactor(ui): migrate the vector store creation form to shadcn (#37353)
* refactor(ui): migrate the vector store creation form to shadcn

CreateVectorStore and S3VectorsConfig were the last antd Form.Item users on
the vector stores page. Both are now built from the shared Field primitives
and shadcn controls, so the page picks up the design tokens and dark mode.

CreateVectorStore's antd Form was inert: no Form.Item carried a name, there
was no onFinish, and the submit button sat outside the form element, so the
form store never held anything. Form.useForm is dropped rather than replaced
with react-hook-form, and the existing imperative validation is unchanged.

S3VectorsConfig's four Form.Item wrappers had no name either, so its inputs
were already prop-controlled and decoupled from the parent store. The
embedding model picker keeps its typeahead by moving to Combobox.

The submit payload is unchanged. A new characterization suite pins it: it was
written against the antd originals, proved green there first, and passes
unedited against the migration.

* test(ui): cover the S3 embedding model combobox end to end

The migration moved this control from an antd Select with showSearch to a
Combobox, and nothing exercised it: the suite pinned the payload but never
loaded the option list, filtered it, or selected from it.

The case drives the whole interaction. It stubs three models, one of which is
a chat model, opens the list and asserts the chat model is absent, types to
filter, selects the remaining embedding model and asserts it arrives in the
providerParams argument.

Proved green against the antd originals of both files first, then unedited
against the migration. It queries the control by role rather than by label,
because the antd original rendered a label with no control associated to it,
which the migration fixes.
2026-08-18 21:07:19 +00:00
yuneng-jiang
0cc2f29c78
refactor(ui): migrate pass-through, project and access group forms to react-hook-form and shadcn (#37354)
Moves six antd Form graphs onto react-hook-form + shadcn: the pass-through
create and edit forms with their two shared sections, the project create and
edit modals, and the access group edit modal.

Each form graph moved atomically. An antd Form.Item parent cannot host a
react-hook-form child, so a shared section that renders a Form.Item has to move
with every parent that mounts it or the field silently stops reaching the
payload. PassThroughSecuritySection holds Form.Item name="auth" and is imported
by both pass-through parents, so all four files move together.

Submit payloads are unchanged and pinned by characterization tests written
against the antd originals first. That includes the parts that look like bugs:
add_pass_through still sends timeout and cost_per_request as strings while
pass_through_info sends them as numbers, and both edit modals still omit fields
whose antd Form.Item never mounted.

One behaviour does change. Enter in the add pass-through form ran Cancel and
discarded the filled form, because HTML implicit submission activates the first
submit button in tree order and Tremor renders buttons with no type attribute,
making the footer Cancel that button. Cancel is now type="button" and Enter
submits.
2026-08-18 14:05:23 -07:00
yuneng-jiang
85333b286d
refactor(ui): migrate the caching, cost tracking, alerting and user detail forms to react-hook-form and shadcn (#37350)
* refactor(ui): migrate the caching, cost tracking, alerting and user detail forms to react-hook-form and shadcn

Moves the last of this lane's antd Form usage onto react-hook-form plus the
shadcn field kit, and takes the neutral greys in the files touched onto
semantic tokens so these screens are dark-mode ready.

Each unit is characterization-first: a payload-pinning test was written and
proven green against the antd original before any production code changed,
then re-run unedited against the migration. Every pre-existing test for these
files still passes without edits.

Two behaviour changes are deliberate and disclosed rather than buried.

Saving cache settings with the Advanced section collapsed used to drop
namespace, ttl, max_connections and the GCP fields from the payload, and to
send ssl as false even when the loaded value was true, because antd reports
only mounted fields at submit while its store keeps the rest. Keeping the
values in form state fixes that, and the payload no longer depends on whether
the section was expanded.

Adding a provider discount used to call the handler twice per click, because
the child submit button sat inside a Form carrying onFinish that called the
same function the button's onClick already called. Removing the leftover
wrapper collapses it to one request. Both calls read the same config and sent
the same body, so this changes request count rather than stored state.

* fix(ui): restore Enter submission on the provider discount form

Removing the antd Form wrapper from cost_tracking_settings also removed
native Enter submission from the Add Provider Discount modal, where the
original carried onFinish and AddProviderForm renders a type="submit"
button. Wrapping the fields in a native form whose onSubmit only calls
preventDefault restores it: implicit submission clicks the default
button, so Enter and a click each produce exactly one request rather
than the two the antd version fired on both paths.

The margin modal is deliberately left as a div. Its antd Form carried no
onFinish and AddMarginForm renders type="button", so Enter was already
inert there and a test now pins that alongside the working click path.

Also swaps five any[] mock forwarders for unknown[] in the user detail
test and asserts the role trigger renders its label rather than a blank
or raw value.
2026-08-18 21:04:44 +00:00
mateo
e67373304d chore(ui): regenerate schema.d.ts for ultrafast pricing fields
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-18 21:02:57 +00:00
ryan-crabbe-berri
ceeab01b0d
refactor(ui): retire the tremor date range picker in favour of the shared advanced picker (#37302)
* refactor(ui): retire the tremor date range picker in favour of the shared advanced picker

UsageDatePicker was the last tremor DateRangePicker surface. Its three call sites in the old usage page and the caching dashboard now render AdvancedDatePicker, which already had the same prop interface, preset list and idle-callback day-boundary adjustment. AdvancedDatePicker drops its own tremor Button and Text for the shadcn Button and a plain paragraph, and it now applies the className prop it already declared so the mb-4 the tag-based usage tab passes keeps landing on the picker root. usage_date_picker.tsx and its calendar-grid test are removed, and the two no-restricted-imports suppressions those files carried are pruned

* fix(ui): let the advanced date picker anchor its panel to the trigger's left edge

The picker's dropdown is 600px wide and right-anchored to a 300px trigger, which was fine while every caller sat at the right edge of its row. The two old usage tabs place it in the left column, so the preset column landed left of the main scroll container and was clipped. AdvancedDatePicker gains an align prop (default right, unchanged for existing callers) and the old usage call sites pass left. The caching dashboard grid gives the picker an auto track instead of a third equal share, so the fixed-width trigger no longer spills past the card at laptop widths

* fix(ui): give the advanced date picker a real focusable trigger

The picker's display was a click-only div, so tabbing through the usage,
old usage, caching, cost optimization and guardrails monitor pages skipped
the date range control entirely and its focus ring classes never fired. It
is now a type="button" element carrying aria-expanded, which restores the
keyboard and screen reader access the tremor picker had. The panel also
reports its anchoring as data-align so the test can assert intent instead
of a Tailwind class

* fix(ui): make date picker relative-range presets keyboard-operable

The presets were non-focusable divs with click handlers, so a keyboard-only admin tabbed past Today / Last 7 days / Last 30 days / MTD / YTD and had to type both dates by hand. They are now buttons carrying aria-pressed.
2026-08-18 13:17:18 -07:00
mateo-berri
842bb7e917 Merge branch 'litellm_internal_staging' into feature/bedrock-mantle-quota-project-itr1 2026-08-18 13:14:14 -07:00
tin-berri
340d30867e
feat(ui): plan-mode override tier in the auto-router create and edit forms (#37319)
* feat(ui): plan-mode override tier in the auto-router create and edit forms

The backend plan_mode_min_tier field (#37230) was API-only. Both forms now
carry an Advanced: Plan-Mode Override panel in the shared complexity config
component: a toggle derived from field presence, so on writes the highest
tier that has models and off deletes the key, and a tier select limited to
tiers with models because the backend rejects a floor at an empty tier. The
edit modal manages the key on every save, so clearing it actually clears the
stored config instead of the preserved copy resurrecting it, while unmanaged
keys like plan_mode_patterns still round-trip untouched

* refactor(ui): hoist the eligible plan-mode tier list out of the panel JSX

* refactor(ui): move tierOptions into complexity_router_tiers, the shared tier-utility module
2026-08-18 13:13:21 -07:00
yuneng-jiang
00e1f25e9b
refactor(ui): migrate the login, onboarding and search tool forms to react-hook-form and shadcn (#37334)
* refactor(ui): migrate login, onboarding and search tool forms to react-hook-form and shadcn

Moves four forms off antd Form and Tremor widgets onto react-hook-form plus
the shadcn kit, and onto semantic colour tokens so the screens are dark-mode
ready. antd Modal and Alert stay as the shells.

The submit payload is unchanged in all four. Each unit is pinned by a
characterization test that was proven green against the antd original before
any production code changed, and the pre-existing test files pass unedited.

Extracts the search tool payload builder, which was duplicated verbatim
between the create and edit forms, into searchToolPayload.ts with unit tests,
and adds a shared PasswordInput so the four reveal toggles antd and Tremor
gave for free are preserved on one component.

* refactor(ui): keep the search tool Test Connection button an implicit submit

Tremor's Button renders no type attribute, so inside a form it defaults to
submit. The Test Connection button therefore fires both its own onClick and
the form's onFinish today, which creates the search tool as a side effect of
testing the connection. shadcn's Button renders type="button", so the naive
swap silently dropped that second path.

Restores parity with an explicit type="submit" and pins it with a test, so
the double submit is recorded rather than quietly changed. Fixing it belongs
in its own change.

Also prunes the two now-stale eslint suppression counts for the migrated
search tool files, scoped to those keys only.

* refactor(ui): announce the login button spinner the way antd did

antd's Button renders its loading indicator as role="img" with aria-label
"loading", so a screen reader announces the request in flight. The shadcn
spinner is a bare svg, which drops that. Labels it on the login button to
match, as already done on the onboarding submit button.

* fix(ui): drop noValidate from the migrated login, onboarding and search-tool forms

antd's Form renders no novalidate attribute and its required rules emit
aria-required rather than the native required attribute, so nothing in
these four forms was ever gated by native constraint validation. The only
type="email" input is disabled and readOnly, which bars it from validation
in every browser. Measured in jsdom and again in Chrome against a live
proxy: form.checkValidity() is true with the fields empty, a native submit
reaches react-hook-form, and zod blocks it with the same messages.

Removing the attribute keeps the rendered form faithful to antd, and keeps
a constraint added later behaving the way antd would have behaved instead
of being silently suppressed.

* fix(ui): accept a null api_key when seeding the search tool edit form

The list endpoint declares api_key as str | None and search_tool_info as
dict | None, and the masking helper returns non-string values untouched, so
a tool stored without an API key comes back as "api_key": null. zod's
optional() accepts undefined and rejects null, so the edit form for any
such tool failed with "expected string, received null" and could never be
submitted. antd carried no schema and forwarded whatever the server sent.

nullish() restores that, and the payload still forwards the null rather
than coercing it to an empty string. The new case seeds both null vectors
and fails without this change.

* chore(ui): drop the narration comments from the search tool forms

These restate the state change or the JSX block directly below them, which
the repo's comment policy rules out, and both files are rewritten by this
change rather than merely touched.
2026-08-18 13:12:27 -07:00
yuneng-jiang
6d32d4081d
refactor(ui): drop the unreachable user edit modal (#37327)
EditUserModal was rendered by the users dashboard but nothing could ever
open it. Its two pieces of state, editModalVisible and selectedUser, were
only ever set to false and null, so the modal short-circuited to null on
every render.

The edit path users actually reach goes through the row actions menu,
which routes to the user detail view and its edit form, so removing this
leaves no capability behind. The submit handler that fed the dead modal
goes with it, along with the imports it was the last consumer of.
2026-08-18 19:16:01 +00:00
yuneng-jiang
aa90828811
refactor(ui): migrate prompt, UI access, plugin and MCP filter forms to react-hook-form and shadcn (#37297)
* refactor(ui): migrate prompt, UI access, plugin and MCP filter forms to react-hook-form and shadcn

Moves four more admin dashboard forms off antd Form onto react-hook-form with the
shared shadcn form kit, keeping the submitted payload byte-identical in every case.

Each form was pinned with a characterization test proven green against the antd
original before any production code changed, then re-run unedited afterwards.

Behaviours that needed reproducing by hand rather than falling out of the port:

- antd onFinish reports only mounted fields, so UIAccessControlForm blanks a seeded
  but hidden restricted_sso_group at submit time instead of sending it
- antd InputNumber returns null on empty and clamps on blur, so MCPSemanticFilterSettings
  keeps top_k as null when cleared and clamps to [1, 100] rather than sending "" or NaN
- PluginSettings seeds plugin_key blank on edit so an untouched save preserves the
  stored credential instead of overwriting it with the redacted placeholder
- antd's url rule and zod's .url() disagree in both directions, so the async-validator
  pattern is ported verbatim to avoid silently changing which URLs are accepted

Forms with no onFinish keep preventDefault so no Enter-to-submit is introduced, and
the plugin key regains a reveal toggle built on InputGroup.

* chore(ui): prune stale eslint suppressions left by concurrent form-migration PRs

* Revert "chore(ui): prune stale eslint suppressions left by concurrent form-migration PRs"

This reverts commit e36bcc5862.

* fix(ui): keep the embedding model unclearable, matching the antd Select

The antd Select for embedding_model had no allowClear, so an admin could never
empty it. SearchSelect renders a clear button whenever a value is set and emits
an empty string, so the migration silently added a way to persist an empty
embedding_model and break semantic filtering.

Adds an opt-out to SearchSelect that defaults to the current behaviour, leaving
the other twenty callers unaffected, and opts this one field out. Pinned with a
test that fails when the opt-out is removed.
2026-08-18 18:47:06 +00:00
yuneng-jiang
d6fe9712fa
refactor(ui): migrate guardrail and vector store forms to react-hook-form and shadcn (#37306)
* refactor(ui): migrate guardrail and vector store forms to react-hook-form and shadcn

Ports four antd forms in the guardrails and vector stores pages onto
react-hook-form with zod resolvers and the shadcn field kit, and takes the
files they live in off light-only Tailwind colors

VectorStoreForm and vector_store_info now build their payloads from typed
form values instead of an antd FormInstance, seeding the edit view through an
explicit mapper rather than spreading the whole server record. The submit
modal in TeamGuardrailsTab moves to the same shape, and its URL rule is
reproduced exactly: src/lib/forms/antdUrl.ts compiles the pattern
async-validator uses for `type: "url"`, with a test asserting the compiled
source and flags match, so a protocol-less www host keeps passing and a bare
domain keeps failing

CompetitorIntentConfiguration had no FormInstance at all: its antd Form was a
layout wrapper with no named items and no onFinish, so it moves onto the field
primitives directly rather than gaining form state it never had. Its tag and
threshold controls are replaced by local TagsInput and ThresholdInput
components that reproduce what antd did, comma token separators plus commit on
blur for tags, and clamp-on-blur with step-precision display for the
thresholds, without introducing the native number constraints that would
newly block the surrounding guardrail form

Every payload is pinned by a characterization test that was proven green
against the antd original before the swap and then re-run unedited

* fix(ui): keep the vector store edit form saving when the server sends null

The proxy returns null for an unset vector_store_name or
vector_store_description rather than omitting the key, and both columns are
nullable. z.string().optional() accepts undefined but rejects null, so
loading any store whose name or description was never set left the edit form
stuck on "Invalid input: expected string, received null" and it could not
submit at all. nullish() accepts both and forwards null unchanged, which is
what the antd version did

Pinned by an untouched-save case that seeds both fields null and clicks Save
without typing anything. It fails against the optional() schema with zero
requests sent, and passes against both the fix and the antd original, sending
vector_store_name and vector_store_description as null

Also drops the deep import into @rc-component/async-validator, an undeclared
transitive dependency that failed the knip gate. The URL parity assertion now
compares against a checked-in snapshot of the pattern async-validator 5.1.0
compiles, so it stays an exact-equality check, and removes a comment that only
restated the networking layer's error handling
2026-08-18 18:44:15 +00:00
yuneng-jiang
7d97bab405
refactor(ui): migrate auto router and credential forms to react-hook-form and shadcn (#37304)
* refactor(ui): migrate auto router and credential forms to react-hook-form and shadcn

Moves four antd Form surfaces onto useZodForm plus the shared FormField and
FieldGroup primitives: the routing group modal, the auto router edit modal, the
add auto router tab, and the reuse credentials modal. Field labels that carried
an antd tooltip= keep it as a hover Tooltip on a help icon, and hardcoded greys
give way to semantic color tokens.

Two Base UI combobox wrappers come out of the two auto router surfaces that
shared the same antd controls: AccessGroupTagsCombobox replaces mode="tags" for
model access groups, and ModelChoiceCombobox replaces the searchable single
select for default and embedding models.

Payload building for the routing group modal moves to routingGroupPayload.ts so
the JSON args parsing and the four bound fields can be asserted directly
instead of through a render.

handle_add_auto_router_submit now takes a resetForm callback rather than an antd
form instance, which drops one any from its signature.

No change to what any of these forms submit. The reuse credentials payload keeps
the same key set, with the stored credential values rendered read-only and
merged back in at submit rather than copied into form state.

* refactor(ui): type the auto router create payload boundary

handleAddAutoRouterSubmit took its values as any, so a change to either side
of the auto router create payload passed static checking. It now takes an
exported AddAutoRouterValues, and add_auto_router_tab annotates the object it
builds with that same type, so the producer and the consumer cannot drift.

Its model_info is built in one shot rather than assigned into after the fact,
which drops the second any and keeps the two conditional keys exactly as they
were.

Adds a routing group case that saves an untouched edit of a group whose stored
arguments are null, which is the shape the proxy returns for an unset field.
2026-08-18 18:43:47 +00:00
yuneng-jiang
e20e31e985
refactor(ui): migrate CloudZero and cost tracking forms to react-hook-form and shadcn (#37312)
* refactor(ui): migrate CloudZero and cost tracking forms to react-hook-form and shadcn

Moves four forms off antd Form onto react-hook-form with the shadcn field
primitives, keeping the request payloads byte identical.

The two CloudZero modals were near duplicates, so the payload builder and the
API key input now live in shared modules next to them. The Update modal keeps
its redaction behaviour: the key field arrives empty with a "leave empty to
keep existing" hint, and an untouched save omits api_key from the request so
the stored secret survives. There is a test that fails if that regresses.

add_provider_form had no Form instance of its own, and its Form.Item wrappers
carried no name, so nothing was registered in the parent store. The parent in
cost_tracking_settings still owns an antd Form element, which stays for now,
and the migrated button keeps type="submit" so the parent's onFinish path
behaves exactly as before.

Each unit got characterization tests written against the antd version first,
then re-run unedited against the migration. Payload parity was also checked
side by side with toStrictEqual across seven scenarios.

* test(ui): guard the CloudZero null connection id against a zod type error

The proxy returns connection_id as null rather than omitting it, and a plain
z.string() rejects null. The seeding coalesces it to "" so an untouched save
reports the friendly required message instead of "expected string, received
null". Dropping that coalesce fails this test.
2026-08-18 18:42:58 +00:00
yuneng-jiang
b134c61300
refactor(ui): migrate the regenerate key and team member forms to react-hook-form and shadcn (#37300)
* refactor(ui): migrate the regenerate key and team member forms to react-hook-form and shadcn

Moves RegenerateKeyModal and EditMembership off antd Form onto react-hook-form
plus the shadcn field kit, and onto semantic color tokens so both are dark-mode
ready. The antd Modal shell and Alert stay as they are.

The submitted payload is unchanged on both. Payload construction is extracted
into regenerateKeyPayload.ts and memberFormValues.ts and unit tested there, and
each component keeps an integration test that was written against the antd
original and proven green before any source changed.

RegenerateKeyModal keeps antd InputNumber's precision=2 rounding of max_budget.
The rounding is string-exact rather than float based, so 1.005 still submits as
1.01 the way antd did. Submission stays on the modal footer button, so Enter
still does nothing.

EditMembership omits noValidate. Its numeric fields are already native number
inputs carrying min and step, so browser constraint validation blocks a bad
submit today and continues to. That is pinned by tests.

* fix(ui): accept null-valued fields the proxy returns for keys and members

The proxy returns null rather than omitting the key for an unset
key_alias, user_email or user_id. antd had no schema and forwarded
whatever came back, but z.string().optional() accepts undefined and
rejects null, so regenerating an alias-less key or editing a member
with no email failed validation and silently never submitted.

Widen those three fields to nullish() and pin each with a test that
seeds null and submits without touching the field. Each test passes
against the pre-migration antd component and failed against the
migration before this commit, and the payloads it asserts are the
ones antd put on the wire.
2026-08-18 18:42:33 +00:00
yuneng-jiang
2ee15a6efb
refactor(ui): migrate user, policy, and margin forms to shadcn (#37305)
* refactor(ui): migrate user, policy, and margin forms to shadcn

Move four dashboard forms off antd Form and Tremor onto the shadcn field
kit, with react-hook-form where the form owns its own submit. Submit
payloads are unchanged: edit_user still emits exactly six keys with spend
as a number and max_budget as a string, policy_test_panel still omits
empty context keys, and add_attachment_form still builds the same
attachment body.

add_margin_form had no Form of its own and no bound field names, so its
Form.Item rules were inert; it keeps its parent-owned state props and
only swaps the presentation.

Adds characterization tests for edit_user and policy_test_panel, both
proven green against the antd originals before the migration, plus a case
pinning the provider value add_margin_form reports upward. The existing
add_attachment_form and add_margin_form suites pass unedited.

Extracts TokenSelect for the tag and alias inputs shared across the
policy forms, keeping antd's token separators and blur-commit behavior.

* refactor(ui): seed the user edit form by remount instead of an effect

Key the form on the edited user so react-hook-form seeds from its
defaults on each user, replacing the effect that reset the form and the
exhaustive-deps suppression that came with it. Cancel and submit still
reset, so reopening the same user shows stored values.

* test(ui): pin that the user edit form forwards null fields unchanged

The proxy returns null rather than omitting unset fields, and antd
forwarded whatever it received. The only fixture seeded every field, so
nothing proved the migrated form still emits null instead of an empty
string. Proven against the antd original first, and it fails if
toFormValues coerces.
2026-08-18 18:42:16 +00:00
yuneng-jiang
5e2d6addc4
refactor(ui): migrate user, logging and policy forms to react-hook-form and shadcn (#37303)
* fix(ui): stop the policy modal cancelling its own save

The Create Policy and Update Policy buttons are Tremor buttons rendered
inside an antd Form. Tremor does not set a type, so both default to
type="submit" and a click ran two things at once: handleSubmit's own
form.validateFields(), and rc-field-form's onSubmit, which calls
formInstance.submit() and validates a second time.

rc-field-form keeps only the newest validation promise, so the first one
resolved as outOfDate and rejected with an empty errorFields list.
handleSubmit read that as a failure, so it never called createPolicy or
updatePolicy and instead reported "Failed to save policy". Creating and
editing a simple policy from the UI could not succeed.

Marking both footer buttons type="button" leaves the submit path solely
with handleSubmit. The new test file pins the request bodies for create
and update, and fails without this change.

* refactor(ui): migrate user, logging and policy forms to react-hook-form and shadcn

Moves four antd Form units onto react-hook-form plus the shadcn kit and
semantic color tokens, keeping every submit payload byte-identical.

- Settings/AdminSettings/LoggingSettings
- CreateUserButton
- users/_components/user_edit_view
- policies/_components/add_policy_form

* chore(ui): drop the eslint suppressions the migrated forms no longer need

* fix(ui): keep users with null optional fields editable

The proxy returns null rather than omitting user_alias, user_role,
budget_duration and metadata, and the new zod shape only allowed
undefined, so opening any such user and saving failed validation.
2026-08-18 18:42:12 +00:00
ryan-crabbe-berri
9ec0145986
feat(proxy): add /team/daily/activity/aggregated and switch the Usage team tab to it (#36562)
* feat(proxy): add /team/daily/activity/aggregated and use it in the Usage UI

The Team Usage tab drained row-paginated pages client side, which painted
newest days first and drew duplicate bars when a day's rows straddled a
page boundary. Serve the whole range in one SQL GROUPING SETS pass instead:
the aggregated query gains optional per-entity rollup levels (entity as the
most-significant GROUPING bit) so breakdown.entities keeps per-team spend,
aliases, and per-key splits. The endpoint shares the paginated route's
scoping via _resolve_team_daily_activity_scope, accepts the timezone the UI
already sends, and the api_key filter now takes a list so non-admin member
scoping works. The dashboard tries the aggregated endpoint first and falls
back to page draining on failure.

* chore: ratchet B008 budget down by the endpoint converted to Annotated Depends

* chore: keep mutable-ok suppressions on their annotation lines after formatting

* fix(proxy): reject malformed or over-wide ranges on team aggregated activity

The aggregated endpoint has no pagination bounding its work, so validate
start_date and end_date as real dates and cap the span at 400 days. The
dashboard's widest presets fit well inside the cap, and an over-cap range
falls back to the paginated flow. Also trim implementation comments that
restated the grouping-set code.

* fix(proxy): parse aggregated range bounds as UTC to satisfy DTZ007

* refactor(proxy): fetch entity rollups with a companion query instead of extending the main one

The entity-as-extra-GROUPING-bit approach made the bitmask layout
mode-dependent: the same constant meant (date) for normal rows and
(date, entity) for entity rows, disambiguated by masking. Split it out:
the shared WHERE builder feeds both the untouched main query and a small
per-entity rollup query keyed by GROUPING(api_key), run concurrently, and
a fold writes breakdown.entities onto the built response.

* refactor(proxy): share the daily-activity error and entity-metadata shapes

The type-discipline ceiling for LIT002 ratcheted down on staging, so the new
aggregated endpoint had to stop hand-rolling collections the codebase already
builds elsewhere. Funnel the `{"error": ...}` detail through one construction
site, turn the range validator into an error-as-value, reuse a single
entity-metadata lookup for both breakdown paths, and widen
get_api_key_metadata to any set so callers stop copying frozensets.
2026-08-18 11:29:57 -07:00
ryan-crabbe-berri
83ae623733
refactor(ui): codemod every toast call site onto lib/toast and delete the antd-era facades (#37253)
MessageManager and NotificationManager were thin facades over lib/toast since #37207. This
rewrites their ~750 call sites (226 files) to import { toast } from @/lib/toast directly:
success/info/warning/error keep their names, fromBackend becomes fromError, destroy/clear
become dismiss. The one config-object caller (CreateMCPServer's admin-review branch) becomes
an explicit toast.success(message, { description }). Behaviour is unchanged: no production
caller passed a duration, so every toast keeps the same kind, title and default duration.

Tests: the global vitest mock now targets @/lib/toast (toast.test.ts opts back out with
vi.unmock), so the per-file vi.mock boilerplate for the facades is deleted and assertions read
toast.success / toast.fromError. The two facade files, their test and their filename-case
suppressions are removed, along with the commented-out facade calls left in networking.tsx
2026-08-18 18:24:49 +00:00
tin-berri
8159f240c4
feat(complexity_router): plan-mode tier floor for coding-agent clients (#37230)
* feat(complexity_router): plan-mode tier floor for coding-agent clients

Claude Code and Copilot signal plan mode only through client-injected prompt
text, which the ask-extraction path deliberately strips, so the router could
never see it. Detect the sentinels on the raw wire body and route those
requests to at least plan_mode_min_tier.

The floor is raise-only and transient: classifier results above it still win,
it overrides a session-affinity pin only on turns carrying the sentinel
without rewriting the pin, and plan_mode decisions are not pinnable, so the
first turn after plan mode exits routes as if plan mode had never happened.
Classification is skipped when the floor is the top configured tier. On
adaptive routers the floor rides _soft_floor_pick as a hard_floor that
excludes below-floor candidates, closing the adaptive_eligible=all gap where
a request classified at or above the floor could still route below it.
Detection is staleness-aware: only leading system content and the newest-ask
tail count, so sentinels surviving in history after plan mode exits, built-in
or operator-supplied, never fire. Custom tier sets are supported with
severity from the tier_definitions list order, same as keyword_tier_rules.
Off by default; decisions are recorded with the new plan_mode cause and the
matched sentinel in matched_keyword

* fix(complexity_router): gate pin writes and the failure exit on sentinel presence, not the floor binding

A plan-mode turn classified at or above the floor keeps its ordinary cause,
but pinning it would carry a plan-mode-shaped choice past plan mode's exit
(on adaptive routers the hard floor constrained that pick), so no
sentinel-carrying turn writes the session pin. The default_model failure
exit is skipped for sentinel turns for the same reason: default_model's
placeholder tier can equal the floor while default_model itself sits in no
pool the floor can vouch for
2026-08-18 10:24:11 -07:00
yuneng-jiang
e09bbe9a14
refactor(ui): migrate tag and memory forms to react-hook-form and shadcn (#37266)
Moves CreateTagModal, the tag detail editor and MemoryEditModal off antd Form
onto react-hook-form with the shadcn field kit, and onto semantic colour
tokens so the pages follow the dashboard theme.

Submitted payloads are unchanged and pinned by tests. The collapsible budget
sections keep values typed into them when collapsed and reopened, matching
what antd's store did, rather than reverting to the seeded default.
2026-08-18 09:05:53 -07:00
yuneng-jiang
b08032c5f7
refactor(ui): migrate budget and skill forms to react-hook-form and shadcn (#37262)
* refactor(ui): migrate budget and skill forms to react-hook-form and shadcn

Moves three dashboard forms off antd Form onto react-hook-form plus the shared
shadcn field kit, and onto semantic color tokens so they render correctly in dark
mode. The submitted request bodies are unchanged.

Budget create and edit previously relied on antd InputNumber precision={2}, which
rounds the submitted value rather than only the display. That rounding is now an
explicit shared helper so the wire payload stays identical, and the helper carries
unit tests covering key presence, null passthrough, negatives and non-finite input.

Characterization tests for both budget modals were written against the antd
implementation first and pass unchanged against the migrated components, which is
what pins the payload. They are named .integration.test.tsx per the dashboard test
tiers, with the pure rounding logic unit tested separately.

* fix(ui): keep collapsed Optional Settings values so reopening does not lose them

react-hook-form shouldUnregister deletes a field's value when its section
unmounts, so typing a budget, collapsing Optional Settings and reopening it
submitted the seeded default instead of what was typed. antd reported only
mounted fields in onFinish but preserved their values in its store, so the
two behaviours have to be reproduced separately.

Drop shouldUnregister, drive the section from controlled state, and blank the
section's fields at submit while it is closed. Seed the edit form from the
five form fields rather than the whole budget record, which shouldUnregister
had been masking.
2026-08-18 08:51:22 -07:00
yuneng-jiang
0896015927
test(ui): await the playground model combobox before clicking it (#36850)
The ChatUI playground test helper looked up the model combobox with a
synchronous getByPlaceholderText. That control renders its placeholder
from the model-loading flag, so the text is "Loading models..." until
the mocked fetch resolves, and the element the helper wants does not
exist yet. Under a loaded full-suite run the query could land inside
that window and fail with "Unable to find an element with the
placeholder text of: Select a Model", while the same test passed in
isolation every time.

Switch the helper to findByPlaceholderText so it waits for the control
to come back after loading.
2026-08-18 08:43:25 -07:00
Mateo Wang
4d57bf0bdd
Merge pull request #37225 from BerriAI/litellm_lit5650_guardrail_usage_units
feat(guardrails): track bedrock guardrail usage units per invocation
2026-08-17 18:55:22 -07:00
ryan-crabbe-berri
68d4ba5da5
refactor(ui): move dashboard toasts from antd message/notification onto sonner (#37207)
* refactor(ui): move dashboard toasts from antd message/notification onto sonner

Add lib/toast.ts as the single toast surface (success/info/warning/error/
fromError/dismiss) backed by sonner, with a <Toaster /> in the root layout.
fromError titles a toast from the proxy error type or the HTTP status instead
of matching prose phrases, and shows the extracted proxy message as the
description.

MessageManager and NotificationManager become thin facades over lib/toast so
the ~250 existing call sites keep working; the mutable antd instance setters,
setMessageInstance/setNotificationInstance, and the antd App/message/
notification providers in AntdGlobalProvider are gone. Prunes the eslint
suppression baseline accordingly.

* test(ui): mock the MessageManager seam in the Fallbacks tests and drop toast doc comments

AddFallbacks and FallbackSelectionForm asserted on a mocked antd message spy
that MessageManager no longer calls; they now mock the facade the components
import. Also removes the explanatory comments Greptile flagged in lib/toast.ts
and both facades.

* fix(ui): keep NotificationManager's antd config-object contract on the sonner facade

success/info/warning/error accept the { message, description, duration } object
form again (CreateMCPServer's admin-review notice uses it) and fromBackend keeps
its extra.duration seconds argument, both mapped onto lib/toast. Prunes stale
suppressions picked up by the rebase.

* feat(ui): read the proxy error type and code out of JSON envelopes embedded in string errors

Legacy networking helpers throw new Error(responseText) and callers prefix
that text, so the envelope arrives as a substring. fromError now parses the
first embedded JSON object for type/code and shows the unwrapped message in
its place, so those toasts get a status title (Request Error, Not Found) and
a readable description instead of raw JSON.
2026-08-17 18:22:41 -07:00
Mateo Wang
b69068c290
Merge pull request #26900 from BerriAI/litellm_model-deprecation-alerts-55bc
feat(proxy): proactive model deprecation alerts and `/model/deprecations` endpoint
2026-08-17 18:15:20 -07:00
tin-berri
1648273469
feat(ui): configure the auto router's heuristic scorer from the Admin UI (#37216)
* feat(ui): configure the auto router's heuristic scorer from the Admin UI

The complexity router has always read tier_boundaries, token_thresholds and
dimension_weights from its config, and /model/new already persists them, but the
dashboard had no control for any of the three, so tuning the scorer meant editing
config.yaml by hand.

Adds an "Advanced scoring" panel to the classification section, shown whenever the
scorer actually runs: on a heuristic router, and on an LLM classifier that falls back
to the heuristic. An untouched knob is omitted from the payload, so a router keeps
tracking the shipped defaults instead of freezing today's numbers.

The three keys join MANAGED_COMPLEXITY_ROUTER_KEYS, so the edit modal now rebuilds
them from form state rather than carrying the stored copy through. That makes
hydration load-bearing, and it hydrates an absent knob to undefined rather than to
the defaults, so an untouched save cannot pin a router that was tracking them.

The "How Classification Works" card now reads the configured boundaries instead of
hardcoding 0.15 / 0.35 / 0.60, which would otherwise start lying the moment an
operator changed them.

* test(complexity_router): pin the dashboard scorer defaults against config.py

The Admin UI keeps its own copy of the boundary, threshold and weight defaults to
prefill its controls. The copy is display only, since an untouched knob is omitted
from the payload, so drift shows a stale placeholder rather than pinning a router.
Nothing caught that drift before, and a blank or dead control is worse, so the two
copies and the dimension key set are pinned against each other here.

* fix(ui): surface out-of-order scorer thresholds as an error, not a hint

Boundaries that decrease make the tiers between them unreachable, which silently
changes where traffic goes, so amber body text undersold it. Saving stays allowed:
a router configured this way in config.yaml would otherwise become uneditable in
the UI for every unrelated change.

* fix(test): search the default-model picker instead of trusting option order

The pinned model is appended after every model the presets contribute, and that list
has reached 11, so the option fell outside the virtualized dropdown's rendered slice
and the two default-model-pin cases failed on staging. CI only runs them when this
file is touched, which is why they went unnoticed. Searching for the model filters
the list to it, so the cases no longer depend on how long the preset list grows.

* revert(test): drop the dashboard scorer defaults parity test

It parsed TypeScript from Python with a hand-rolled brace matcher and a numeric
literal regex, which is not a mechanism this repo should carry: two review rounds
went into fixing the parser rather than the feature. The UI copy of the defaults is
display only, since an untouched knob is omitted from the payload, so drift shows a
stale placeholder and cannot pin a router.

* fix(ui): clamp the scorer inputs and drive the panel from one group spec

min and max are inert attributes on a text input, so the fields accepted a weight of
999, a boundary of -50, and Infinity, and persisted them into the router config.
Values are now clamped on commit and non-finite input is refused.

The three sections were near copies of each other, so they now render from a single
group spec, which also removes the triplicated warning logic.

Moves the scorer constants and types into heuristic_scoring_knobs, the leaf module.
Reading them back through ComplexityRouterConfig was a cycle, so the top-level
DIMENSION_KEYS.map in the panel ran while the constant was still undefined and every
test importing it failed to collect.

* feat(ui): serve the scorer defaults from the proxy instead of mirroring them

The dashboard kept its own copy of DEFAULT_TIER_BOUNDARIES, DEFAULT_TOKEN_THRESHOLDS
and DEFAULT_DIMENSION_WEIGHTS to prefill the Advanced scoring controls. Two copies of
one fact, and the earlier attempt to police the gap parsed TypeScript from a Python
test, which was worse than the problem.

GET /public/complexity_router/scorer_defaults now returns them, following the
/public/providers/fields pattern: a typed response model, the dashboard fetching it
through a react-query hook next to useProviderFields. The controls and the "How
Classification Works" card both read that, so a recalibration of the defaults can no
longer leave the form stating numbers the router stopped using.

The dimension set now comes from the proxy too, so a dimension added backend-side
renders without a dashboard change, under its raw key until it is given a label.
Hydration keeps a stored dict exactly as stored rather than filling it from a local
copy, since the backend already defaults any key omitted at scoring time.

* fix(types): type the scorer defaults response as Mapping, not dict

LIT001 gates mutable collections in annotations, and the three dict fields tripped it.
Mapping is what the codebase already uses for a read-only map on a response model, and
the endpoint hands the config constants over directly rather than copying them into a
fresh dict, which would have traded the LIT001 hit for a LIT002 one.

* test(ui): stub the scorer defaults request for the auto-router tree

The Advanced scoring panel and the classification card read the shipped defaults over
the network, so every render of that tree in a test paid for a request jsdom cannot
serve. That was enough to push the slowest default-model-pin case past its 30s timeout
on CI, where the suite runs 14 forks in parallel.

One fixture in tests/mocks, pulled in by a single vi.mock line per test file, rather
than the same stub pasted into each of the seven that render the tree.

* fix(ui): tell a failed scorer-defaults load apart from a slow one

The panel read only the query's data, so a permanent failure was indistinguishable
from a request still in flight and it sat on "Loading the shipped defaults..." for
good. It now branches on the query state: pending says loading, an error says so and
offers a retry, and the values the router already overrides stay visible and editable
either way.

Two more places had the same flaw. The classification card silently dropped the tier
ranges it used to always show, and now says they could not be loaded. The weight total
was summed over whatever keys were present, so a failed load made it state a total
built from the overrides alone; a total is only shown when the dimension set is known.
2026-08-18 01:10:13 +00:00