user.type dispatches one event per character and re-renders the whole form on
each one, so a test that only needs a field to hold a value pays for every
keystroke. Where the value is all the test wants, fireEvent.change sets it in
one go.
Measured against a control on the same tree: the 100 converted files went from
654s to 619s of CPU, 5.4% cheaper, while the files nobody touched drifted 1.2%
the other way. Modest, and honest about it.
Rolled out one file at a time, running each before and after and keeping the
conversion only where the file stayed green. That rejected 35 files, all cases
where the keystrokes are the behaviour: Base UI comboboxes drive their filter
from real keyboard input and ignore a raw change event, and the same goes for
autocompletes, debounces and key handlers. Those keep user.type.
Both OpenAPI dispatch arms sourced the upstream credential only from the
deprecated global / BYOK mcp_auth_header and never from mcp_server_auth_headers,
so x-mcp-{alias}-authorization was silently dropped on spec_path servers and the
upstream API received no Authorization at all. The managed path already resolves
it through lookup_mcp_server_auth_in_headers, so the two had drifted.
_resolve_openapi_tool_auth now owns that resolution for both arms. A per-server
value is already a complete header value and is forwarded verbatim, while a BYOK
credential keeps its auth-type prefix, so the two are never conflated into
"Bearer Bearer <token>". The resolved credential is also handed to
resolve_openapi_upstream_auth, whose passthrough arm reads it through
_passthrough_token_from_mcp_auth_header and outranks the ContextVar.
server.py loses its inlined copy of the forwarded-header logic along with its
mcp_server is None guards, which are unreachable after the 503 raised above them.
Credit to the earlier analysis and approach in #33349, which this supersedes
against the current v2 credential resolver.
* refactor(ui): port the MCP server forms off antd Form onto react-hook-form
The MCP create and edit forms were the last antd `Form` graph in the
dashboard. antd's `onFinish` hands back only the fields mounted at submit
time, while react-hook-form with `shouldUnregister: false` hands back the
whole store, so a direct port would quietly widen every create and update
request.
All 14 files now bind through `MountedFormField`, whose mount registry
reproduces antd's mounted-only submit: both roots build their payload from
`projectMountedValues` instead of `getValues`. `mcpFormStore` carries the
rest of the FormInstance surface the two roots relied on, each piece
matched to what rc-field-form actually does rather than to what the API
name suggests: `setFieldsValue` deep-merges plain objects and writes an
explicit `undefined`, `resetFields` restores the seeded values rather than
clearing the key, and `onValuesChange` is rebuilt from a `watch`
subscription filtered to user input, carrying a single changed branch.
Two watches needed the mount gate moved rather than translated.
`MCPPermissionManagement` renders outside the transport gate that mounts
`auth_type`, so antd's watch read `undefined` there and mounted the
pass-through toggle; the effective auth type now arrives as a prop that
each root computes with exactly that gate. The edit root reads every watch
off the mounted projection for the same reason, which also stops the token
material in a saved server's credentials from reaching the tool preview.
`Form.List` becomes `useFieldArray` plus a `useMountedName` registration
for the list key itself, because antd registers a list as one field: a
per-user variable row keeps the `value` its scope hides, and an empty list
still submits `env_vars: []` instead of dropping the key.
* test(ui): cover the mount registry's unregister path on the real primitive
The existing MountedFormField suite drove a hand-written registry whose
register returned a no-op, so nothing exercised useMountRegistry's
ref-counting or the cleanup that React wires from useMountedName's effect
return value. A reviewer read that gap as a missing unregister.
These three cases drive the real hook through a gated tree: a key leaves
the submitted payload when its gate unmounts the field, a required field
that unmounts stops blocking submission, and a name held by two fields
survives one of them releasing it.
Verified by mutation: rewriting the effect body to discard the cleanup
turns the first two red, the second reporting the reviewer's exact
symptom, "expected [ 'server_name', 'token_url' ] to not include
'token_url'".
* test(ui): prove the permission panel's booleans reach the create payload
CreateMCPServer.integration.test.tsx mocks MCPPermissionManagement, so the
four booleans createServerPayload writes were invisible to every existing
create-side test. vi.mock is file-scoped, so rendering the real panel needs
its own file.
Four cases, each killed by a different mutation:
unbind allow_all_keys -> "sends allow_all_keys true"
unbind available_on_public_internet -> "sends the panel's defaults"
invertedSwitchControl -> switchControl -> "sends ... false when the
operator restricts"
isOAuth2 gate forced open -> "omits delegate_auth_to_upstream"
A payload assertion expecting false cannot detect an unbound field, since
Boolean(undefined) is false too, so the two cases carrying unbinding
detection are the ones asserting true. The other two are pinned by the
switch-inversion and mount-gate mutations instead.
PassThroughGuardrailsSection threads its `disabled` prop to the guardrail
selector and to all three quick-add buttons, but not to the two `TagsInput`
controls for request and response field targeting. Every other prop crossed,
so the section greys out while a user can still type field names straight
into both tag inputs.
Latent rather than live: neither call site passes `disabled` today, so the
prop is currently dead across the tree. This arms the gate for whoever passes
it first.
`TagsInput` already honours `disabled` end to end, measured rather than read:
with it set, the input carries a real disabled attribute and all three commit
paths, token separator, Enter and blur, are inert. So the fix belongs at the
call sites and the primitive needs no change.
The new test drives the control rather than asserting an attribute, since a
Base UI control can paint a disabled state while still accepting input. The
enabled cases are the liveness gate: they type the same text through the same
gesture and assert it commits, so the disabled cases cannot pass vacuously.
* refactor(ui): extract the MCP server edit save payload into a pure builder
`handleSave` built the update payload inline across 276 lines, spreading
`...restValues` straight off a mounted-only `onFinish`. That makes the payload a
function of which fields happen to be mounted, and it leaves no seam to test the
shape without rendering the whole edit form.
Move the payload construction into `editServerPayload.ts` as
`buildEditServerPayload(values, ui)`, a pure function over the submitted values
plus the nine pieces of component state the handler reads. Failures become values
rather than early returns with a toast: the six error branches are a tagged union
that `editPayloadErrorMessage` maps back to the exact strings shown today, via an
exhaustive switch. `handleSave` keeps the network call, the OAuth token
persistence and its own try/catch.
This is a move, not a rewrite. To prove that, `editServerPayload.differential.test.ts`
holds a baseline machine-extracted from the pre-refactor function body by line
range, with the failure branches converted by exact string replacement. The
generator refuses to emit unless the slice is still present verbatim in the
source, every conversion matches exactly once, no toast call survives, and a
deliberately corrupted probe still trips that check. 59 scenarios run both
implementations and compare the payload object, its key order, and its serialised
bytes, so a re-ordering that leaves values untouched is caught too.
The duplicate local `AUTH_TYPES_REQUIRING_CREDENTIALS` is dropped in favour of the
identical exported list, and `reduceStaticHeaders` is shared with the create side.
Both were verified equal before reuse.
Behaviour is unchanged. The 466 pre-existing tests in the directory pass unedited.
* refactor(ui): type the MCP edit payload builder instead of Record<string, any>
The extraction created a new public signature, so it should carry a real
contract. buildEditServerPayload now takes EditServerFormValues and returns
EditServerPayload, both declaring every field the builder actually reads and
writes, with an unknown-valued index signature for the keys the form passes
straight through. handleSave is annotated too, so antd's untyped onFinish
value is narrowed once at the boundary rather than travelling as any.
Fields that arrive from the store with their own runtime validation
(static_headers, env_vars, credentials) stay unknown rather than being given a
narrower declared type the form does not actually guarantee. Values are not
run through a parser: the payload's serialised key order is part of the
contract this module exists to hold, and rebuilding the object would reorder
it.
The credentials assignment moves from two post-hoc mutations to a single
resolved entry, which keeps the payload readonly end to end and lands the key
in the same position in all four branches.
Behaviour is unchanged. The 59 differential scenarios still match the frozen
pre-extraction body on object, Object.keys order and JSON.stringify bytes, and
the mcp-servers suite is 525/525 across all 30 files. Three tsc probes confirm
the new types have teeth: a wrong payload assignment, a misspelled field read
and an invalid value each fail the type check.
* feat(ui): add mounted-field projections for the MCP server form graph
antd's onFinish reports exactly the fields mounted at submit time, and both MCP
server payload builders spread that object straight through. react-hook-form
with shouldUnregister false hands back the whole store instead, so a port needs
the mount set written out explicitly before any JSX moves.
This adds mountedEditFieldNames / mountedCreateFieldNames as pure functions over
the form values, plus the projections that apply them, covering all 22 gates
across the graph's 89 named bindings. No JSX changes, nothing imports them yet.
Three behaviours are probed against antd 5.29.3 rather than assumed, and the
tests pin all three:
- a mounted-but-unset field is EMITTED as a key holding undefined, at the root
and inside credentials, so the projection emits rather than omits
- a Form.List row is NOT projected down to its mounted sub-fields, so env_vars
rows pass through whole; filtering them would drop per-user values
- the two roots disagree on more than StdioConfiguration: edit gates url on a
deny-list while create uses an allow-list, and create additionally gates the
whole auth section on a non-empty transport
* refactor(ui): port the create key form off antd Form onto react-hook-form
antd hands onFinish exactly the fields mounted at submit time, so a collapsed
section contributes nothing to the request while the values typed into it
survive for re-expansion. react-hook-form reaches only one of those two
behaviours per shouldUnregister setting, so the store is kept intact and
projected down to the mounted set through an explicit mount registry.
MountedFormField carries the rest of the Form.Item contract the payload
depends on: defaults taken from each field's own declaration rather than a
blanket empty value, and help text that replaces the rule message instead of
sitting beside it.
The 60-case submit differential runs unedited against the port, joined by
cases for the writers outside the submit path, mounted-set validation, Enter
to submit, and switch coercion.
* test(ui): pin the mounted-field sets by membership, not array order
Two credential assertions compared the returned array with toStrictEqual
against a literal in source order, so they failed when two names were
swapped inside the source array even though the projected payload was
unchanged. Key order is not observable in the payload, so those two
assertions rejected a refactor that changes nothing a caller can see.
Route both through the same sorted() helper the other fifteen set
assertions already use. Membership keeps its teeth: deleting any one of
the eighty emitted field names still fails the suite, while reordering
two of them now passes.
* docs(ui): state the mounted projection's static-name limit at its export
The registry counts by name and the projection emits flat keys, so a
Form.List row and its per-row sub-fields, whose names are generated at
runtime, are never in the mounted set and go missing from the payload.
That is silent and it is correct for every static field around it, so the
contract belongs where the next consumer reads it.
* refactor(ui): cut the mounted field's explanatory comments to the contract limit
The mechanism the projection uses and the reason a helped field hides its
rule message are both derivable from the code, so they belong in the pull
request rather than in two places. What survives is the one thing no reader
can derive: that a runtime-generated name is silently absent from the payload.
* refactor(ui): drop the doc comment from MountedFormField
The static-name constraint it described moves to the PR description, where
it is not a second place to keep in sync with the code.
* fix(ui): rebuild nested and list paths in the mounted-field projection
projectMountedValues emitted one flat key per registered name, so a field
registered under a dotted path produced a literal "credentials.client_id"
key instead of a nested credentials object, and a field-array row produced
"env_vars.0.name" instead of a row.
Both are silent. buildEditServerPayload destructures credentials and passes
it to buildCredentials, which returns undefined for a non-object, so every
credential drops out of the payload while the code compiles and the existing
suites pass. reduceStaticHeaders loses its rows the same way.
Split each registered name on "." and rebuild the value, treating a numeric
segment as an array index so field-array rows come back as arrays rather
than objects keyed by digits. Nested credentials and both field-array sites
are the same defect and take the same fix.
* fix(ui): make mounted-field nesting opt-in via array names
The first cut split every registered name on ".", which diverges from antd.
antd's getNamePath is toArray, so a string name is a one-element path and
"a.b" is stored as a literal flat key; only an array name nests.
check_openapi_schema registers names taken from a live /openapi.json at
runtime, so the field set is an input rather than source. A spec property
containing a dot would have silently nested under the unconditional split
and changed that payload with nothing failing.
Accept string | readonly string[]. An array nests, with a numeric segment
as an array index, which is what the credential paths and the field-array
rows already use. A string stays one literal key. The registry keys by the
joined path but projects from the original shape, so the two cannot drift.
The suppression said exercising this component requires a real antd Form.
That stopped being true once the create key form began driving it through
a bare mounted field, so the harness and the suppression go together.
Two of the auto-rotation cases were racing the Base UI select popup all
along: findByText resolves as soon as the option lands, while the popup
still carries pointer-events none until its open transition settles. The
antd harness happened to hide it, at 0 failures in 6 runs against 1 in 6
for plain state and 5 in 6 under a Controller, since each extra render is
another chance to catch the popup mid-transition. Both now take the same
PointerEventsCheckLevel.Never their sibling case already used.
* fix(ui): restore the cache control Role and Index field hints
The add_model cache control editor lost both field hints when it moved off
antd Form.Item in #37392. "LiteLLM will mark all messages of this role as
cacheable" and "(Optional) If set litellm will mark the message at this index
as cacheable" went with the Form.Item tooltip props and neither string exists
in dashboard source any more. The Index hint was the only thing telling a user
that field is optional, so this is lost information rather than styling.
Both come back as shadcn tooltips beside their labels, matching how the
surviving switch-level hint is already rendered.
Also adds the payload characterization net this graph did not have. Before
this commit the seven suites over add_model and model_add held 37 cases, no
antd module mock, and zero toStrictEqual, so nothing pinned the submit
payload. AddModelPanel.integration.test.tsx drives the real panel, the real
antd store and the real prepareModelAddRequest, and asserts the object handed
to modelCreateCall.
It pins the distinctions only a strict assertion can see: litellm_credential_name
arrives as null from its initialValue while api_key, api_base, mode and
access_groups arrive as undefined, and team_id is absent entirely until the
Team-BYOK switch mounts it. It also pins the mount gate in both directions,
since a collapsed Advanced Settings drops both its keys and anything typed
into it while re-expanding restores them, and the empty-string skip, since a
cleared api_base must vanish rather than arrive as "".
Every fixture was captured from the running component rather than written by
hand. A 12-mutation battery over the bindings, the empty-string skip, the two
required rules and an added keepMounted all go red, each run gated on having
executed the expected case count.
* refactor(ui): port the add model form off antd Form onto react-hook-form
The Add Model form graph is shared by three antd hosts, so it only moves as
one piece: AddModelPanel, LlmCredentialsPanel and CredentialModal all mount
the same children. Form and Form.Item are replaced everywhere, and every
widget inside them is left alone, so the change is the binding layer only.
antd submits the mounted fields, react-hook-form submits its whole store. A
shared mount registry keeps that difference from reaching the request: each
field registers on mount, and the panel projects the store down to the
registered names before it builds the payload. shouldUnregister would have
been the other option, but it drops a collapsed section's typed values, so
re-expanding Advanced Settings would come back empty.
The antd rules modules are reused as-is through a thin validator adapter, so
the messages stay in one place rather than being reworded per field.
Advanced Settings held a Form.useForm() instance in a component that renders
no Form, which made ten imperative calls dead. They are removed rather than
translated, and the three behaviours they looked like they drove were checked
against the antd original first: invalid LiteLLM Params still blocks submit,
the pass-through toggle still leaves LiteLLM Params empty, and turning custom
pricing off then on still keeps the typed cost.
The existing 14 case payload net runs unedited against the port.
* test(ui): pin the three add model behaviours the dead form instance looked like it drove
Advanced Settings used to hold a form instance it never rendered, and the ten
imperative calls against it were dead. The inherited payload net covered none
of the three behaviours those calls appeared to own, so removing them looked
riskier than it was. These cases characterise what the antd original actually
did, checked against it before the port.
Invalid LiteLLM Params blocks the submit, which also closes the one mutation
the inherited net could not kill: dropping the JSON rule left all 14 green.
The proxy resolves its own public origin from PROXY_BASE_URL before it
looks at anything on the request, so a developer who has that set for
their own deployment watched 65 OAuth discovery, redirect_uri, and client
registration cases fail against an origin no test ever asked for
An autouse fixture now clears it for every unit test, matching the host
AWS config isolation that already sits beside it, and the tests that do
exercise a configured public origin keep setting it in their own body
* refactor(ui): port the create key form off antd Form onto react-hook-form
antd hands onFinish exactly the fields mounted at submit time, so a collapsed
section contributes nothing to the request while the values typed into it
survive for re-expansion. react-hook-form reaches only one of those two
behaviours per shouldUnregister setting, so the store is kept intact and
projected down to the mounted set through an explicit mount registry.
MountedFormField carries the rest of the Form.Item contract the payload
depends on: defaults taken from each field's own declaration rather than a
blanket empty value, and help text that replaces the rule message instead of
sitting beside it.
The 60-case submit differential runs unedited against the port, joined by
cases for the writers outside the submit path, mounted-set validation, Enter
to submit, and switch coercion.
* docs(ui): state the mounted projection's static-name limit at its export
The registry counts by name and the projection emits flat keys, so a
Form.List row and its per-row sub-fields, whose names are generated at
runtime, are never in the mounted set and go missing from the payload.
That is silent and it is correct for every static field around it, so the
contract belongs where the next consumer reads it.
* refactor(ui): cut the mounted field's explanatory comments to the contract limit
The mechanism the projection uses and the reason a helped field hides its
rule message are both derivable from the code, so they belong in the pull
request rather than in two places. What survives is the one thing no reader
can derive: that a runtime-generated name is silently absent from the payload.
* refactor(ui): drop the doc comment from MountedFormField
The static-name constraint it described moves to the PR description, where
it is not a second place to keep in sync with the code.
* refactor(ui): extract the MCP server edit save payload into a pure builder
`handleSave` built the update payload inline across 276 lines, spreading
`...restValues` straight off a mounted-only `onFinish`. That makes the payload a
function of which fields happen to be mounted, and it leaves no seam to test the
shape without rendering the whole edit form.
Move the payload construction into `editServerPayload.ts` as
`buildEditServerPayload(values, ui)`, a pure function over the submitted values
plus the nine pieces of component state the handler reads. Failures become values
rather than early returns with a toast: the six error branches are a tagged union
that `editPayloadErrorMessage` maps back to the exact strings shown today, via an
exhaustive switch. `handleSave` keeps the network call, the OAuth token
persistence and its own try/catch.
This is a move, not a rewrite. To prove that, `editServerPayload.differential.test.ts`
holds a baseline machine-extracted from the pre-refactor function body by line
range, with the failure branches converted by exact string replacement. The
generator refuses to emit unless the slice is still present verbatim in the
source, every conversion matches exactly once, no toast call survives, and a
deliberately corrupted probe still trips that check. 59 scenarios run both
implementations and compare the payload object, its key order, and its serialised
bytes, so a re-ordering that leaves values untouched is caught too.
The duplicate local `AUTH_TYPES_REQUIRING_CREDENTIALS` is dropped in favour of the
identical exported list, and `reduceStaticHeaders` is shared with the create side.
Both were verified equal before reuse.
Behaviour is unchanged. The 466 pre-existing tests in the directory pass unedited.
* refactor(ui): type the MCP edit payload builder instead of Record<string, any>
The extraction created a new public signature, so it should carry a real
contract. buildEditServerPayload now takes EditServerFormValues and returns
EditServerPayload, both declaring every field the builder actually reads and
writes, with an unknown-valued index signature for the keys the form passes
straight through. handleSave is annotated too, so antd's untyped onFinish
value is narrowed once at the boundary rather than travelling as any.
Fields that arrive from the store with their own runtime validation
(static_headers, env_vars, credentials) stay unknown rather than being given a
narrower declared type the form does not actually guarantee. Values are not
run through a parser: the payload's serialised key order is part of the
contract this module exists to hold, and rebuilding the object would reorder
it.
The credentials assignment moves from two post-hoc mutations to a single
resolved entry, which keeps the payload readonly end to end and lands the key
in the same position in all four branches.
Behaviour is unchanged. The 59 differential scenarios still match the frozen
pre-extraction body on object, Object.keys order and JSON.stringify bytes, and
the mcp-servers suite is 525/525 across all 30 files. Three tsc probes confirm
the new types have teeth: a wrong payload assignment, a misspelled field read
and an invalid value each fail the type check.
The model resync now mutates the router under MODEL_RECONCILE_LOCK, and the
agent resync shares the new AGENT_RECONCILE_LOCK with the periodic agent
reload, so a reconcile built from a pre-write DB snapshot can no longer evict
or duplicate what a read-through just registered. Every resync checks
should_load_db_object for its object type, keeping read-through consistent
with what the replica is configured to load, and the a2a raise sites tag
ProxyModelNotFoundError as non-retryable so an agent miss no longer burns the
model resync budget.
* feat(ui): add mounted-field projections for the MCP server form graph
antd's onFinish reports exactly the fields mounted at submit time, and both MCP
server payload builders spread that object straight through. react-hook-form
with shouldUnregister false hands back the whole store instead, so a port needs
the mount set written out explicitly before any JSX moves.
This adds mountedEditFieldNames / mountedCreateFieldNames as pure functions over
the form values, plus the projections that apply them, covering all 22 gates
across the graph's 89 named bindings. No JSX changes, nothing imports them yet.
Three behaviours are probed against antd 5.29.3 rather than assumed, and the
tests pin all three:
- a mounted-but-unset field is EMITTED as a key holding undefined, at the root
and inside credentials, so the projection emits rather than omits
- a Form.List row is NOT projected down to its mounted sub-fields, so env_vars
rows pass through whole; filtering them would drop per-user values
- the two roots disagree on more than StdioConfiguration: edit gates url on a
deny-list while create uses an allow-list, and create additionally gates the
whole auth section on a non-empty transport
* test(ui): pin the mounted-field sets by membership, not array order
Two credential assertions compared the returned array with toStrictEqual
against a literal in source order, so they failed when two names were
swapped inside the source array even though the projected payload was
unchanged. Key order is not observable in the payload, so those two
assertions rejected a refactor that changes nothing a caller can see.
Route both through the same sorted() helper the other fifteen set
assertions already use. Membership keeps its teeth: deleting any one of
the eighty emitted field names still fails the suite, while reordering
two of them now passes.
* fix(ui): restore the cache control Role and Index field hints
The add_model cache control editor lost both field hints when it moved off
antd Form.Item in #37392. "LiteLLM will mark all messages of this role as
cacheable" and "(Optional) If set litellm will mark the message at this index
as cacheable" went with the Form.Item tooltip props and neither string exists
in dashboard source any more. The Index hint was the only thing telling a user
that field is optional, so this is lost information rather than styling.
Both come back as shadcn tooltips beside their labels, matching how the
surviving switch-level hint is already rendered.
Also adds the payload characterization net this graph did not have. Before
this commit the seven suites over add_model and model_add held 37 cases, no
antd module mock, and zero toStrictEqual, so nothing pinned the submit
payload. AddModelPanel.integration.test.tsx drives the real panel, the real
antd store and the real prepareModelAddRequest, and asserts the object handed
to modelCreateCall.
It pins the distinctions only a strict assertion can see: litellm_credential_name
arrives as null from its initialValue while api_key, api_base, mode and
access_groups arrive as undefined, and team_id is absent entirely until the
Team-BYOK switch mounts it. It also pins the mount gate in both directions,
since a collapsed Advanced Settings drops both its keys and anything typed
into it while re-expanding restores them, and the empty-string skip, since a
cleared api_base must vanish rather than arrive as "".
Every fixture was captured from the running component rather than written by
hand. A 12-mutation battery over the bindings, the empty-string skip, the two
required rules and an added keepMounted all go red, each run gated on having
executed the expected case count.
* refactor(ui): drop the explanatory comments from the add-model payload net
The repo bans explanatory source comments. The liveness-gate pairing the
second one described now lives in the test name instead, where a reader
deleting the paired case will actually see it.
* fix(ui): make the cache control field hints reachable without a pointer
SimpleTooltip renders its trigger as a bare span with no tabindex, so the
guidance behind it is mouse-only and the focus-visible ring classes it
already carries can never fire. The antd tooltips these hints replaced set
tabIndex null too, so this is an improvement on the original rather than a
restoration of it.
The primitive is shared by 20 files and is CLI-managed, so the trigger is
composed at the call site instead: a real button, an accessible name that
says which field it explains, and the icon marked aria-hidden.
Two tests cover it by tabbing to each trigger rather than counting keys,
so they keep working when a field is added between them. Swapping the
button for a span with role=button turns exactly those two red and leaves
the other five green.
* test(ui): pin the teams create and update payloads before the form migration
The teams graph (Teams.tsx, TeamInfo.tsx and the MetadataKeyValueFields child they
share) is next for the antd Form to react-hook-form migration, and its submit payload
is a function of which collapsible sections the user happened to open. Nine sections
across the two files use the shadcn Collapsible, none of them passes keepMounted, and
Base UI unmounts the closed branch, so a closed section registers nothing and its keys
never reach the request body.
That matters beyond parity. /team/update reads the body with exclude_unset, so an
omitted key is never written, while an explicitly null team member budget key reaches
clear_team_member_budget_fields and nulls max_budget, budget_duration, rpm_limit and
tpm_limit on the shared budget row. antd cannot reach that today because the field is
unregistered rather than null. A port that seeds those fields or coalesces on the way
into the payload would turn a save with the section never opened into a silent clear.
The coverage that shipped with the team modal reached one of the four gating sections
on the create side and asserted key sets rather than the request body, so a null where
antd sent undefined would have passed. These cases assert both the raw payload and its
JSON round trip with toStrictEqual, which is what separates absent from null from
undefined, and they cover every gating section on both screens.
Also pinned, because each is a live behaviour a port can quietly change:
- the create path sends max_budget, tpm_limit and rpm_limit as strings, while
team_member_budget arrives as a number through its normalize prop
- an invalid secret manager config blocks the create with its rule message suppressed
by the item's help prop, so nothing is shown to the user
- the disable global guardrails switch is inert for a non premium user
- a value typed into a section survives collapsing and re-expanding it
Verified by adding keepMounted to all nine panels, which is the change a porter reaches
for on noticing that fields go missing: 35 of 118 went red, including every one of these
cases. The files were restored byte identical afterwards.
No production file changes here. 145 tests pass across the three files.
* refactor(ui): migrate the teams form graph off antd Form onto react-hook-form
Teams.tsx and TeamInfo.tsx were the last large antd `Form` graph in the
dashboard. Both now use `useZodForm` + `FormField`, with the shared
`MetadataKeyValueFields` child converted to a `useFieldArray`.
antd only returns the mounted registered fields from `onFinish`, so a
closed collapsible contributed no keys at all. react-hook-form keeps
unmounted values in the store (and `shouldUnregister: true` would lose
them on re-expand), so both forms project the submitted values through
the currently mounted section list before handing them to the existing
payload builders. Closed sections therefore still produce absent keys
rather than nulls, which matters at /team/update where an explicit null
clears the shared budget row.
Widgets that had no shadcn equivalent are replaced with the existing
shared ones: SearchSelect for the organization pickers, MultiSelect for
default member models, TagsInput for guardrails/policies, and a new
GuardrailsSelect for the grouped global/other guardrail dropdown.
* refactor(ui): forward the field ref to NumericalInput in the teams forms
staging turned NumericalInput into a forwardRef, so the teams graph can stop
dropping the react-hook-form ref on the floor.
* test(ui): pin the capability gate, required rules and guardrail kill switch
A mutation run over the ported teams forms found five survivors the payload
cases did not reach: the viewPolicies gate on both forms, the team name rule
on both forms, the guardrail kill switch resync, and the number coercion on a
typed model rate limit. Six cases close them.
The gemini 3.6 flash batch rates landed at half the standard rates in
94a29e0708, so the hardcoded standard-rate expectation started failing
on staging and red-lit misc / Run tests on every PR.
Resync registry misses with single-row DB fetches (guardrail by unique
name, agent by unique id or name, model by name then id) instead of
full-table loads, and bound them with a global budget of 20 resyncs per
5s window per registry that fails closed without negative-caching the
key.
Access group create/update now trust the reconcile outcome snapshot
captured under the reload lock instead of a post-lock router read, so a
concurrent reconcile can no longer surface a false degraded-serving 500.
Router.upsert_deployment restores the previously served deployment when
the replacement add fails under ignore_invalid_deployments, so a bad
update no longer silently drops a healthy deployment from serving.
The strict-denial pin only asserted a 401, so any unrelated 401 (a bad key, a deleted key) would have kept it green while tag routing silently broke. The harness now keeps the 401 response body, the way it already does for 429s, and the pin asserts the tag-routing denial message.
* fix(ui): highlight the first member search match so Enter picks it
Moving the modal off antd Form swapped antd Select for Base UI Combobox, which
highlights nothing until an arrow key moves the cursor. Typing an email and
pressing Enter therefore selected no one, and the form submitted anyway, so
/team/member_add went out with member.user_email undefined.
autoHighlight="always" restores the behaviour antd had. The Enter key still
does not submit the form, which the existing test continues to cover.
* fix(ui): keep the always-highlight prop past the type check
Combobox.Root re-declares autoHighlight as boolean while the AriaCombobox it
wraps types it as boolean | "always". Only "always" highlights a list this
component filters server-side, since the plain flag highlights on Base UI's own
filtering pass, which filter={null} turns off.
Verified with next build, the same type check that failed in CI.
The earlier sweep only caught the conformance suite in tests/llm_translation.
Groq retired llama-3.1-8b-instant alongside llama-3.3-70b-versatile, and four
tests under tests/local_testing still call them for real, so litellm_router_testing
and both local_testing shards 404 with model_not_found.
Only the sites that leave the process move. The chunk fixtures in
test_stream_chunk_builder, and the cost and routing tests that never open a
socket, keep the old ids because the string is data there, not a request.
* feat(otel): attribute Prisma database spans to PostgreSQL instead of localhost
Prisma reaches PostgreSQL through a query engine on loopback, so transport
instrumentation attributes database waits to localhost and operators cannot
tell the work is PostgreSQL or correlate it with the database's own metrics.
Datastore service spans now carry db.system.name, db.system, db.operation.name
and, for PostgreSQL, server.address, server.port and db.namespace derived from
DATABASE_URL, and are emitted as CLIENT spans. Only host, port, database and
schema are read, so no credential reaches an exporter. Endpoint attributes are
omitted when a read replica is configured, because routing is decided per Prisma
call underneath the span.
* fix(otel): reject a mis-split DSN authority and name socket-only databases
An unencoded '/' in the password truncates the URL authority, so urlparse
reports the username as the host and the password tail as the database, which
put credential material in db.namespace. Postgres drivers reject that DSN
outright, so the only safe reading is no endpoint at all.
A hostless 'postgresql:///litellm' is a valid local-socket DSN that Prisma
accepts, and it now yields db.namespace with no server address rather than
nothing. The default schema is matched case-insensitively, since an unquoted
PostgreSQL identifier folds and one deployment must yield one namespace.
* fix(otel): keep a non-default schema in db.namespace
Prisma quotes the schema name, so a DSN with ?schema=PUBLIC provisions a
second schema alongside public rather than reusing it. Observed on a live
proxy: the PUBLIC schema came up with its own 70 tables next to public's 78,
and a key created under one was not visible under the other.
Case-folding the two into a single namespace therefore reported two different
schemas as the same database, which is the misattribution this feature exists
to remove. Match the default literally.
* fix(otel): reject any DSN whose userinfo fell outside the authority
An unencoded '#' or '?' in the password sends the tail to the fragment or
query, leaving the path empty, so the marker check on the database segment
never fired and urlparse's hostname (the database username) was exported as
server.address.
The stranded userinfo '@' is the general tell for every mis-split, so guard on
that instead of enumerating the characters that cause it.
* fix(otel): allow an at-sign inside a well-formed DSN query
The previous guard rejected any DSN whose userinfo at-sign fell outside the
netloc, which also caught libpq parameters that legitimately carry one, so
?application_name=svc@prod and ?user=admin@company.com lost their endpoint
attributes.
Discriminate instead: a PostgreSQL DSN never has a fragment, its database name
cannot hold an unencoded at-sign or slash, and an at-sign in the query is only
suspicious when the query did not parse as parameters.
* fix(otel): resolve the database endpoint per span instead of once per process
The endpoint was cached for the process lifetime on the premise that
DATABASE_URL is deployment-static. It is not. The RDS IAM refresh rebuilds the
URL from DATABASE_HOST/PORT/NAME/SCHEMA on every rotation, the reconnect path
re-reads DATABASE_URL, and the DB-backed environment_variables config overlay
sets arbitrary keys post-startup with no blocklist covering DATABASE_*. A
process that had genuinely failed over kept exporting the old server.
Read the environment per span, which is also what Prisma connects with, so the
span can no longer name a different server than the one serving the query;
get_secret_str consulted a secret manager first and could diverge from it. Only
the parse is memoized, keyed on the URL.
* fix(otel): reject a question-mark mis-split whose tail parses as parameters
A '?' in a password strands the rest of the authority in the query, and that
tail can still parse as key=value, so testing only for an unparseable query let
the login through as server.address. One spelling hijacked the host= parameter
and put the password suffix there directly.
A legitimate at-sign in a query always follows a database path, and a
'?'-mis-split never leaves one, so require both.
* refactor(otel): drop the DSN parse cache that retained rotated credentials
The cache was keyed on the full DATABASE_URL, so up to eight complete DSNs,
each carrying a password or a retired IAM token, stayed referenced for the
process lifetime and outlived the rotation that replaced them. Nothing reached a
span, but a heap dump or crash report would have surfaced them.
Parsing costs about four microseconds against a span emission that costs orders
of magnitude more, so the cache bought nothing worth that.
* fix(otel): avoid a set construction the tightened LIT002 budget rejects
* fix(otel): refuse an ambiguous DSN authority instead of guessing at it
A password holding both an unencoded slash and a query-like tail defeated all
three shape checks: the slash left a clean path carrying the password
remainder, the query still parsed as parameters, and no fragment survived. The
login went out as server.address, the password's leading digits as server.port
and the rest as db.namespace.
A DSN whose at-sign sits in a query parameter is indistinguishable from that
mis-split by any property of the parse; both leave no userinfo, a host, a port
and a path. Guessing wrong publishes a credential fragment, so the ambiguity
now resolves to refusing the endpoint. Such a DSN loses server.address and
db.namespace and keeps the rest of the span; percent-encoding the at-sign
restores them.
Also honour port= alongside host=, which libpq pairs and this read ignored.
* docs(otel): fix a spliced sentence and a stale cache claim in db_endpoint
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* refactor(ui): extract the create-key payload builder out of create_key_button
handleCreate built the POST /key/generate body inline by mutating the object antd
handed it, across roughly 170 lines mixing form values, twelve pieces of React
state and five sources that all fold into object_permission. Nothing could assert
that shape without rendering the whole modal.
The construction now lives in createKeyPayload.ts as a pure function returning a
tagged union, with 46 unit tests that run in single digit milliseconds and pin
whole payloads with toStrictEqual, following the createServerPayload pattern the
dashboard CLAUDE.md documents.
Behaviour is unchanged. The extraction was checked against the pre-extraction
handler with a differential harness over 50 input combinations, comparing the
built object, its key order and its serialised bytes, and eight mutations of the
new module were each confirmed to fail the committed tests.
* refactor(ui): align the key payload builder to the programme's entry-condition spec
Folds the duplicate-alias guard and the endpoint choice into the builder, so
KeyPayloadResult now carries three variants and handleCreate holds no payload
decision of its own. The two failure branches keep their original position
around toast.info and setIsModalVisible, and the builder checks the alias before
the agent selection, so the observable order is unchanged.
Pins the serialised wire shape as well as the object. The closed form registers
team_id at null through initialValue while organization_id has no initialValue
and stays undefined, so an untouched create sends seven of its eight keys.
Opening Optional Settings takes the object to 23 keys and the wire to nine. Both
directions of the definedness contract are now covered: undefined must not
become null, and null must not be dropped.
The earlier fixture fed a team_id the closed form cannot produce and pinned a
six-key wire as a result.
* refactor(ui): fold the service-account metadata write into its only caller
Removes assignServiceAccountId as a separate helper so there is no mutating
function available for reuse, which was the substance of the review finding. The
write now sits two lines below the JSON.parse that produced the value, so it is
visibly local and cannot reach a caller-owned object.
The write itself stays. Metadata has no validation rules, so a user can submit a
JSON body that parses to a primitive or an array. On a primitive the property
write raises a TypeError and the existing catch surfaces an error toast, and on
an array it leaves the array intact through JSON.stringify. A spread coerces
both to plain objects instead, silently creating a key from input the form
rejects today. Two tests pin those cases and go red against the spread.
gemini/imagen-4.0-generate-001 now 404s with "no longer available, please
update your code to use models/gemini-3.1-flash-image", and
groq/llama-3.3-70b-versatile reached its deprecation_date of 2026-08-16, which
is the day the llm_translation and image_gen jobs went red.
Only the two live call sites move. The remaining references to the old ids sit
in offline transformation tests, where the string is just a routing key and no
request leaves the process.