* refactor(ui): extract shared tab-routing helpers
Every per-tab-routed page copy-pastes the same URL<->slug logic and the
same active-tab/redirect engine. Extract two reusable pieces:
- createTabRoutes(baseSegment, slugs) in utils/tabRoutes.ts returns
{ baseSegment, slugs, tabHref, slugFromPathname }, the trailing-slash
href builder (via migratedHref) and the pathname->slug reader.
- useTabRouting({ routes, baseTabKey, visibleKeys, ready }) derives the
active tab from the pathname, redirects an unknown/forbidden slug to
base once ready, and returns an onTabChange navigator.
visibleKeys + ready exist so a role-gated page can pass its filtered tab
set and defer the redirect until permissions resolve, rather than
bouncing a user off a still-loading valid tab. Both are pure/unit-tested.
No page consumes them yet.
* refactor(ui): migrate Models + Endpoints onto the shared tab-routing helpers
Replace the page's hand-rolled tabRoutes.ts (base segment + slug tuple +
href builder + slugFromPathname) with createTabRoutes, keeping the
existing named exports as thin re-exports so callers and tests are
unchanged. The layout drops its local activeSlug/isKnownSlug/activeKey
derivation, its redirect useEffect and its router.push onChange in favor
of useTabRouting, passing the role-filtered visibleKeys and a ready flag
(!teamsLoading && !uiSettingsLoading) so the permission-gated redirect
behavior is preserved exactly. The antd tab bar, role-gated tab set, the
refresh button and the ?model=/?team= drill-in overlay are untouched; the
file's pre-existing antd import is now recorded in the suppressions
baseline since editing it makes it a linted-as-changed file.
The existing models-and-endpoints layout.test.tsx and tabRoutes.test.ts
pass unchanged, which is the regression guarantee.
* test(ui): make the agents route's tests markup-agnostic before migration
Rewrites the two assertions that were coupled to antd's DOM and adds the
missing characterisation test for agent_cost_view, so the suite describes
behaviour rather than antd markup and can stay untouched across the shadcn
migration.
The skill selection test reached the checkbox with a querySelector on
input[type=checkbox]; antd renders an input while Base UI renders a
span[role=checkbox], so it now queries by role and accessible name, which
both libraries derive from the wrapping label.
The delete confirmation test queried role=dialog; antd Modal is a dialog
while Base UI AlertDialog is an alertdialog, so it now anchors on the
confirmation text and accepts either role.
agent_cost_view had no test at all; it gets one covering the null render,
the dollar-prefixed values, the omitted rows, and a zero cost that must not
be mistaken for unset.
All 55 tests pass against the current antd components.
* refactor(ui): migrate agents to shadcn
Replaces antd and Tremor with shadcn (base-vega) primitives across the five
files the agents route exclusively owns. Markup only; no behaviour, data
fetching or route structure changes.
Modal becomes AlertDialog, with a plain destructive Button in the footer
rather than AlertDialogAction, because that action is AlertDialog.Close and
would dismiss the dialog before the delete request settles, losing the
in-flight state. Alert, Tag, Spin, Space, Collapse, Descriptions, Typography
and the antd icons map onto alert, badge, ui-loading-spinner, flex/grid
utilities, collapsible, a definition list, semantic headings and lucide.
The shadcn CLI emits alert.tsx importing cva from class-variance-authority,
which this project does not depend on; it uses the cva object syntax from
lib/cva.config. The generated file fails to typecheck, so the adapted copy
lives in components/shared instead, per the convention that ui/ stays
CLI-managed.
Colour comes from tokens throughout, so the info callout is now the neutral
card style rather than antd's blue, and nothing hardcodes a colour in the way
of a later theme change.
The 55 tests in the route pass unchanged from the previous commit. The visual
gate re-baselined agents and all 34 other routes stayed pixel-identical.
Makes the three metric columns on the cache leakage table sortable, each with a
sensible first-click direction: most uncached tokens and biggest potential
savings first, worst cache hit rate first. Repeat clicks toggle the direction.
Renames Uncached input to Uncached input tokens, since the column is a token
count
Adds a By virtual key / By model toggle to the cache leakage table. The model
view aggregates the daily activity model breakdown and is scoped to Anthropic
(Claude) models, which support prompt caching. Renames the columns to plain
language: Uncached input, Cache hit rate, and Potential savings (replacing
Realized caching savings and Est. savings left), with a tooltip on Potential
savings that spells out how it is calculated
require_env hard-failed a test (and, for the shared litellm-ops secret, drove
piling every provider credential into one blob) whenever an optional cred was
absent. Most call sites either read a value the test actually uses or just
gated on the runner's env for a key the gateway consumes.
Read os.environ directly where the test uses the value; drop the presence-only
gates so those cases run against the proxy instead of pre-failing on the
runner's environment. Removes the require_env helper from e2e_config.
Adds GET /v1/tool/spend returning per-tool and daily tool spend with a
deduplicated request total, and a cache leakage breakdown on the Prompt
Caching tab of the Cost Optimization page. Tool-spend rows are validated at
the boundary with pydantic, the endpoint is scoped to proxy admins, date
params are cast to timestamptz for real-Postgres query_raw, and the leakage
math treats litellm-normalized prompt_tokens as cache-inclusive
(uncached = max(0, prompt - cache_read - cache_creation)).
* feat(ui): rebuild Organization Settings on react-hook-form + zod with a dirty-field PATCH
Replaces the antd Settings form in organization_view.tsx with OrgSettingsForm,
the first consumer of the shared RHF + zod form kit. The form derives a minimal
payload from RHF dirty tracking via pickDirty and sends it to the typed
PATCH /v2/organization/{organization_id}, so untouched fields are omitted,
emptied widgets clear with null ([] for lists), and the old full-send builder
with its length > 0 clear-dropping guards is deleted.
Adds src/lib/forms/useZodForm.ts so every form gets the z.input/z.output
generics and zodResolver wiring from one place, and forwardRefs ui/textarea
so RHF can register it under React 18
* fix(ui): forwardRef InputGroupTextarea to match the forwardRef'd Textarea
* test(ui): pin that an mcp server edit preserves existing org toolsets
* docs(ui): explain the useZodForm generics
* chore(ui): re-prune eslint suppressions after rebase onto staging
The v2 credential resolver owns oauth2_token_exchange end to end: any server
with a token-exchange config maps to a non-None TokenExchangeConfig spec, and
that config is in _create_mcp_client's override-exclusion set, so a caller
x-mcp-* override cannot force it back to v1 either. The v1 handler
resolve_mcp_auth reached at spec is None was therefore dead for OBO, including
its warn-then-proceed-unauthenticated fall-through. Delete auth/token_exchange.py
and the exchange branch, dropping the subject_token parameter that only fed it.
Separately, the REST listing and call paths still ran the v1 per-user OAuth
lookup for servers the v2 resolver owns. Unlike the two protocol-path call
sites they gated on auth_type == oauth2 only, with no to_server_spec check, so a
migrated authorization_code server did a DB round-trip whose Authorization
header _resolve_v2_auth then discards. Add the same guard via
_is_v1_resolved_oauth2_server, shared by the per-server lookup and the prefetch
preflight.
Also collapses MCPOAuth2TokenCache.async_get_token's now single-caller
require_client_credentials_flow kwarg and removes the dead
_get_bulk_user_oauth_headers helper (zero callers).
* refactor(ui): migrate request logs table onto the shared DataTable
Moves the Request Logs tab off the local view_logs/table.tsx clone and onto the
shared DataTable in server sort, pagination, and filter mode. The container is
split into RequestLogsPanel (data owner: the spend-logs query, the session dedup
and composition pipeline, and the detail drawer), a thin RequestLogsTable, and
RequestLogsTableColumns. The clone itself stays for now because TopModelView and
TopKeyView still consume it
The advanced filter bar moves into the shared DataTableFilterDrawer, so filters
commit on Apply and render as removable chips. That makes the per-keystroke
debounce in the query hook redundant, and the hook now takes ColumnFiltersState,
PaginationState, and SortingState directly instead of carrying its own filter
shape. Reset still restores the default 24 hour window alongside the filters
Adds shared/PaginatedSearchSelect, a Base UI combobox with server-side search and
infinite scroll, and uses it for the Key Alias and Model filters. That retires the
three logs-only antd pickers (PaginatedKeyAliasSelect, PaginatedModelSelect,
FilterTeamDropdown) and the FilterComponent molecule they plugged into. The shared
TeamDropdown is deliberately untouched: six other surfaces still render it, five of
them as a bare child of an antd Form.Item that injects value/onChange implicitly
* test(ui): pin team-scoped key alias filtering in the logs filter drawer
The Key Alias filter narrows its options to the team selected in the same
drawer, a cross-filter dependency carried over from the antd picker it
replaced. Nothing covered it: the live QA pass explicitly did not exercise
it either, so it was the one behaviour in this migration that could regress
silently
Asserts the selected team id reaches useInfiniteKeyAliases, that the lookup
stays unscoped when no team is picked, and that the scope does not leak into
the Model lookup, which shares the same combobox but takes no team
* refactor(ui): migrate models and endpoints table onto the shared DataTable
Rebuilds the All Models table on the shared DataTable, following the 2a
treatment from the Models + Endpoints design: one card holding search, the
Team and View selectors, refresh, columns and filters, with the active
filters on a chip row and the pagination footer at the bottom.
Retires the last hand-rolled tremor renderer (all_models_table.tsx) and the
antd/tremor column defs in molecules/models/columns.tsx, replacing them with
a thin AllModelsTable consumer plus AllModelsTableColumns built from the
shared cell library.
Behavior is preserved end to end. The server sort field mapping now lives
next to the column ids so the two cannot drift. Status keeps its column and
its sort, hidden by default behind the Columns menu because the design shows
nine columns. Access groups collapse into a "+N more" tooltip instead of a
per-row expand toggle, and the full reset moves into the filter drawer
footer where the design puts it.
Adds the shadcn hover-card primitive (Base UI PreviewCard in the base-vega
style) for the model information hover, which needs an interactive surface a
tooltip cannot provide.
* fix(ui): stop the models tab re-querying on mount
The mount-time effect fires the debounced search with the initial empty
value, and its callback rebuilt the pagination object unconditionally. That
produced a second render (and a second query) roughly 300ms after mount with
no user input, which on a slow CI machine swapped the table's row nodes
mid-interaction and made a click land on a detached node.
resetToFirstPage now returns the existing state when already on the first
page, so React bails out instead of re-rendering. Pinned with a test that
asserts no additional query after the debounce settles; it fails without the
fix.
* test(ui): characterise the workflow runs detail drawer before migrating it
Pins the drawer's behaviour against the current antd implementation: the
metadata fields it surfaces, the timeline ordered by sequence number, the
empty-events copy, the messages section staying collapsed until opened, the
in-drawer refresh refetching, and the close control dismissing it.
Every assertion is role/text based so the same file can stay green once the
component moves off antd, without being edited.
* refactor(ui): migrate workflow runs to shadcn
Replaces the antd Drawer, Collapse, Button, Spin, Tooltip and Empty on the
Workflow Runs page with the installed Base UI primitives (Sheet, Collapsible,
Button, UiLoadingSpinner, Tooltip) and lucide icons, and moves the page's
hardcoded hex colours, fonts and geometry onto design tokens and utility
classes so the page can be themed. The only inline styles left are the gantt
bars' computed left/width, which are runtime values.
Behaviour is unchanged: the drawer's characterisation tests were written
against the antd version in the previous commit and pass here without being
edited.
Retires the file's now-unused antd no-restricted-imports suppression.
Compress the security rationale in the gateway-session admission path of
user_api_key_auth_mcp.py, keeping the load-bearing "why" and dropping the
restatement, and remove a garbled dead comment in get_allowed_tools_for_server
In the tests, hoist the duplicated _team / _admitted_subject fixtures to
module-level factories and parametrize the four fail-closed session-bearer
variants into one case. No behavior change; the 294 tests in the file still pass
Admits a keyless SSO user (no virtual key) at the aggregate /mcp endpoint from a gateway DCR
session bearer, resolving team/org/SCIM/budget authorization fresh on every call.
- Aggregate DCR front door: stateless /register (sealed llm_dcrc_ client ids), SSO-backed
/authorize + /authorize/complete, and /token minting identity-only session tokens with PKCE,
single-use codes/flows, and rotating refresh tokens.
- Admission: a session-shaped Authorization at the aggregate scope opens via _admit_gateway_session,
reloads the live user, and runs the centralized policy gate; failures return the RFC 9728
invalid_token challenge. Gated on the un-forgeable, server-only mcp_admitted_user_subject marker,
so virtual-key and JWT auth are unchanged.
- Authorization model: an admitted subject is resolved as one plain UserAPIKeyAuth per grant source
(its own grants, plus each team it is a live roster member of), each answered by the SAME resolver
virtual keys use, then unioned. That branch is the FIRST statement of BOTH public resolvers, so no
single-credential prelude runs for it and a fault in a lookup it never uses cannot deny its grants. A source team counts only while it is a live grantor: roster membership, not
blocked, and neither the team nor its owning org over budget (enforced through the SAME
_team_max_budget_check / _organization_max_budget_check owners common_checks uses for keys).
Each team source carries that team's own org, so the existing org
ceiling caps it; for a keyless source the org list only ever intersects (a ceiling must not become
a grant) and an unresolvable ceiling denies rather than silently uncapping, on both the server and
tool axes. _roster_team_object is the single owner of "which teams count": a team whose roster no
longer lists the user neither grants servers nor throttles, in one place.
- Rate limits: the subject is bounded by its user rpm/tpm AND by the per-server mcp_rpm_limit of
the team a call is ATTRIBUTED to — the same single source billing charges, from the same owner. A key charges its one pinned team's bucket; a keyless
subject has no team_id, so admission stamps each granting team's limit map onto the auth
(server-only field, stripped from validated input like the marker) and the limiter emits that
team's mcp_per_team descriptor. Charging every granting team instead would let one cross-team user
drain several teams' SHARED buckets on a single call and block their other members; and a server
the user's OWN grant reaches charges no team bucket at all, because no team provided it. Per-KEY
MCP limits do not apply because there is no key.
- Wrapper channels: the manager-level union treats the admitted subject by the same grant model.
The admin-role short-circuit and the absolute no_mcp_servers early-return are key-credential
rules and never apply to it (a session bearer is a third-party client credential, not the
dashboard, and the subject's opt-out silences only its own source). Operator-open channels
(allow_all_keys, the user's own BYOM submissions) are owned by one operator_open_server_ids
helper that BOTH the server union and the admitted tool resolution consult (suppress-BYOM-when-
explicitly-scoped is a key-credential rule and never applies to the subject, whose user row
carries the DB-default empty mcp_servers), so an open-channel
server is default-open for tools instead of listable but uninvokable.
- Redirect URIs: one owner, validate_redirect_uri_shape, decides redirect-URI hygiene (bad scheme,
fragment, missing host, userinfo, backslash host) and resolves allowlisted native callbacks, shared
by DCR registration and the OAuth endpoints. Registration keeps a deliberately wider trust policy
than validate_trusted_redirect_uri: public dynamic registration accepts any https client, and its
controls are mandatory S256 PKCE plus the consent screen.
- Egress leak-defense: a gateway admission credential (session bearer / bridge envelope) is scrubbed
from EVERY egress header context, anchored to the credential shape, so it can never be forwarded
upstream and replayed.
- Single-use guard: auth-code, refresh and connect-flow claims resolve the proxy's cross-worker redis
cache themselves rather than trusting the cache passed in, and fail CLOSED on a Redis fault instead
of falling back to a per-worker count that a captured id could replay through another worker.
- Sign-in return_to: one shared, never-raising helper persists a safe return_to for every sign-in
branch (SSO/Okta/generic and username/password), and every branch RESUMES through the same
_sso_return_to_redirect the SSO callback uses, so however a deployment signs in the stored value
is honored identically (same-origin path directly; control_plane_url via the one-time login-code
handoff). A stale cookie is ignored rather than failing a completed sign-in.
- Budgets, both halves: ENFORCEMENT (an already over-budget team or its owning org stops being a
grantor, in the source gate) and ACCOUNTING (a team-derived tool call is billed to the granting
team and ITS org, so that budget accumulates and the right organization is charged). A server the
user's own grant reaches bills the user; when several teams grant one server the pick is the
lowest team_id, stable and auditable. Billing rides a COPY, so authorization still sees the full
union, and it is inert when the target server cannot be resolved from the tool name.
Deferred (tracked): client-selected server scoping of the session token (LIT-4680).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(docker): bake non_root prisma engines at /opt/prisma so migrations run offline for any uid
The non_root image baked the prisma CLI and engines under /app/.cache and used
the CLI's default (library) engine mode. Prisma stopped baking the library
engine, so `prisma migrate deploy` fell back to downloading it at startup,
which needs network egress and a writable cache. Under an arbitrary non-root
uid (OpenShift restricted-v2), an air-gapped network, or a readOnlyRootFilesystem,
that download fails and the proxy starts on an empty schema while every DB
endpoint returns 500. The migration entrypoint exits 0 on that failure, so a
default-uid `docker run` with network never surfaced it
Bake to /opt/prisma, a fixed world-readable path no cache mount shadows, and
pin PRISMA_CLI_PATH plus PRISMA_CLI_QUERY_ENGINE_TYPE=binary so the baked binary
engine is used directly, matching Dockerfile and Dockerfile.database. A
build-time guard asserts the binary query engine is present, so a future prisma
change that stops baking it fails the image build instead of silently degrading
migrations
Adds docker/test_offline_migration.sh, run from image-scan, which migrates a
fresh Postgres with no egress as a non-root uid and asserts the schema was
created, the case a default-uid `docker run` with network cannot catch
* test(docker): move the offline migration check into a gated pytest and stop pinning XDG_CACHE_HOME at the read-only bake
The offline migration check lived in docker/ as a shell script. It now lives in
tests/proxy_migration_tests/ as a pytest gated on LITELLM_IMAGE, matching the
sibling schema-migration test gated on DATABASE_URL, and image-scan invokes it
with pytest instead of bash. It also asserts the migration entrypoint's exit
code alongside the table count, so a crash or a container-startup failure fails
loudly rather than only surfacing as a low table count
Runtime XDG_CACHE_HOME pointed at /opt/prisma/.cache, which is baked a+rX with
no write, so any XDG-aware library writing a cache at runtime would be denied
for every uid. Leave it unset so it falls back to $HOME/.cache (/app/.cache,
created here and owned by the runtime uid), matching Dockerfile and
Dockerfile.database which never pin XDG at runtime. A second test guards against
a future edit pointing a cache or home var back at the read-only bake
* test(ui): pin search-tools info view behavior before shadcn migration
Rewrite the markup-coupled copy-button assertions in SearchToolView to
role queries plus lucide icon-state, and add a role/text characterization
suite for SearchConnectionTest, which had none. Both are green against the
current antd/Tremor components so they can act as an unedited regression
net across the migration.
* refactor(ui): migrate search-tools info view to shadcn
Port the search-tools detail view and its two helpers off antd and Tremor
onto the installed shadcn primitives plus token utilities:
- SearchToolView (the info page reached by clicking a tool) now uses
ui/button, ui/card and a plain CSS-grid header instead of Tremor
Card/Grid/Title/Text and antd Button
- SearchToolTester swaps antd Input/Button/Spin and Tremor Card/Title for
ui/input, ui/button and UiLoadingSpinner, with no inline styles
- SearchConnectionTest swaps antd Button/Divider/Typography and the inline
keyframe spinner for ui/button, ui/separator and UiLoadingSpinner
Markup only; no behavior change. The list page (SearchTools) and its create
and edit forms stay on antd because they are Form-bearing and blocked until
the forms migration. Icons move from antd and heroicons to lucide. The
retired antd no-restricted-imports suppressions are pruned from the
baseline.
* fix(ui): drop redundant vertical padding in SearchToolTester card
The shadcn Card already applies py-6 and gap-6 to its flex children, so
the pt-6/pb-6/mb-6 added during the migration stacked on top of it and
roughly doubled the vertical whitespace. Keep only px-6 (Card has no
horizontal padding) and let the Card own the vertical rhythm, which
restores the original 24px spacing.
* style(ui): format SearchConnectionTest test file with prettier
* fix(organization): persist cleared fields on /organization/update
Clearing an org field (the Metadata box or a TPM/RPM/max_budget limit) via PATCH /organization/update looked like it saved but reverted on refresh; the partial-update merge could not tell a cleared field from an untouched one and dropped every clear
The endpoint now decides SET vs CLEAR vs UNTOUCHED purely from which keys the raw request body carried, via a pure build_organization_update_plan. Budget nulls flow to update_budget (null clears via exclude_unset), metadata is replace-when-sent (written as {} for the non-nullable Json column), and a budget write on an org with no budget_id creates and links a budget row. This removes the exclude_none dump, both "if v is not None" filters, and the additive _update_dictionary merge
Resolves LIT-3664
* feat(organization): add RESTful PATCH /v2/organization/{organization_id}
Adds a v2 organization-update endpoint with a deterministic partial-update contract, and reverts the v1 /organization/update changes so its public behavior stays untouched
On v2 a field present in the request body is written (null/[]/{} clears, a value sets) and an omitted field is left untouched; presence is read from model_fields_set. Clearing a TPM/RPM/max_budget limit or the metadata now persists instead of being dropped as if it were never sent. Metadata is replace-when-sent and written as {} when cleared, since the org metadata Json column is non-nullable. Budget nulls flow to update_budget, and an org with no budget row gets one created and linked. The endpoint is hidden from the public Swagger docs via include_in_schema=False, and stays typed in the generated dashboard schema
Resolves LIT-3664
* test(organization): cover v2 auth guard, negative budget, and object_permission
Adds v2 endpoint tests that were missing: the real _verify_org_access path rejects a non-admin caller with 403 and writes nothing, a negative max_budget is rejected with 400 before any DB access, and a sent object_permission is passed to the upsert helper with its id linked onto the org write
Refs LIT-3664
* fix(organization): 400 on null-clear of required org fields; drop dead budget upsert
organization_alias and models are non-nullable columns, so a v2 request clearing them with null hit a 500 (NOT NULL violation) and could partially apply the budget half of the request first; the endpoint now returns a 400 with a clear message. Also removes the unreachable "create a budget when the org has none" branch from _apply_organization_budget_updates, since budget_id is a non-nullable FK and every org already has one, so the endpoint no longer needs to link a newly-created budget id
Refs LIT-3664
* fix(organization): let v2 clear object permissions when sent as null
Sending object_permission: null now detaches the org's permission by setting the nullable object_permission_id to null, instead of being a silent no-op, so the endpoint honors its documented "null clears" contract and an admin can actually revoke vector-store/MCP access. Sending a value still merges as before
Refs LIT-3664
* fix(organization): make v2 PATCH atomic, strict, and 422-consistent
Tighten the PATCH /v2/organization/{id} endpoint against standard HTTP
PATCH (RFC 5789 / RFC 7396 JSON Merge Patch) semantics:
- Apply the budget-row and org-row writes in one prisma transaction so a
failure between them can no longer half-apply the patch (RFC 5789 requires
a PATCH to apply atomically). The budget write is inlined as a tx-aware
call mirroring the team-member budget path rather than the standalone
update_budget route handler
- Set extra="forbid" on OrganizationUpdateRequestV2 so an unknown or
misspelled key is a 422 instead of a silently dropped no-op; the contract
is presence-driven, so swallowing unknown keys is unsafe
- Return 422 (not 400) for the hand-rolled field validations (negative
budgets, null-clear of required organization_alias/models, invalid
model_max_budget) so every validation failure matches the 422 that
pydantic already returns for bad values
- Document the per-field clear tokens accurately: null clears budget limits
and metadata, [] clears models, and organization_alias cannot be cleared
Tests cover the single-transaction write path, unknown-field rejection, the
422 status changes, and the budget_reset_at recompute.
* fix(organization): reject empty object_permission on v2 PATCH instead of silently keeping grants
object_permission is a nested merge field on PATCH /v2/organization/{id}: a
sent object merges into the existing permission row (updating one grant list
without touching the others), and null detaches it. An empty {} therefore
merged nothing and left every existing vector-store/MCP grant in place, so an
admin who sent {"object_permission": {}} to strip access silently kept it.
Reject a present-but-empty object_permission with a 422 that points the caller
at null, mirroring how the endpoint already rejects a null clear of the
required organization_alias/models. This keeps merge semantics for non-empty
payloads and does not affect the Admin UI, which only ever sends a fully
populated object or omits the field.
* fix(organization): JSON-serialize model_max_budget on the v2 budget write
model_max_budget is a Json column on the budget table. Route the budget-row
write through jsonify_object so a dict value is serialized the same way
new_budget and the org-row metadata write already do it, keeping every Json
column on this endpoint written consistently.
Raw dicts already round-trip (update_budget writes them unserialized), so this
is not a correctness fix so much as making the one Json column on the budget
path follow the same serialization as the rest of the file. Added a test that
a patched model_max_budget reaches the budget write JSON-serialized.
* refactor(organization): trim v2 docstrings and consolidate planner tests
Trim the verbose docstrings on the v2 endpoint, request model, and the two
pure helpers to the essential contract, and drop a stale line that still
referenced update_budget's exclude_unset (the budget write is inlined now).
Collapse the nine per-case planner tests into one parametrized test asserting
exact budget/org split per body, and fold the two model-validation rejection
cases into one parametrized test. Same 36 test cases run; the planner
assertions get stronger (exact-equality instead of presence/absence) and the
test additions shrink by ~85 lines.
* refactor(organization): inline the v2 update planner into the endpoint
Fold the OrganizationUpdatePlan dataclass and build_organization_update_plan
helper into update_organization_v2. The budget-vs-org split is a few dict
comprehensions built in one shot, so the extra type plus builder was more
ceremony than the job needed. Drops the now-unused dataclass/AbstractSet
imports and the isolated planner unit tests; the split is exercised end-to-end
by the endpoint tests.
* fix(organization): run v2 object permission upsert inside the update transaction
prepare_object_permission_upsert splits the shared helper's read-and-merge
step from its write so the v2 endpoint can upsert the permission row on the
same prisma transaction as the budget and org writes. Previously the upsert
ran before the transaction, so a rolled-back org write left merged grants
live on the permission row the org still pointed at. The upsert record now
pins object_permission_id, since the column's @default(uuid()) would
otherwise mint a fresh-create id different from the one linked on the org.
v1 and the team/key callers of handle_update_object_permission_common keep
their existing behavior
* fix(lint): keep the v2 org PR within the strict-rule budget
The strict gate flagged the PR's new code after the base merge: 11 UP045
Optional fields and a typing.List on OrganizationUpdateRequestV2, Dict
annotations in the new upsert helper and the TypeAdapter, and a B008 from
the v2 endpoint's Depends default. The model and helper now use pipe
unions and builtin generics, and the endpoint takes its auth dependency
via Annotated, which avoids the call-in-default pattern B008 targets
* fix(routes): expose /v2/organization on the backend component allowlist
The component-split coverage test requires every app route on a component;
the new v2 org PATCH belongs with the other management endpoints on the
backend, alongside the existing /v2/key and /v2/team prefixes
* fix(organization): clear budget_reset_at when budget_duration is cleared via v2 PATCH
* feat(ui): give each Models + Endpoints tab its own path
* refactor(ui): decompose Models + Endpoints into per-tab pages with URL-driven detail
Dissolve the 488-line ModelsAndEndpointsView monolith into one page per tab
under the models-and-endpoints route, with a persistent layout owning the
header, cost banner, tab bar and refresh. Each tab page owns only its own
state; shared lists come from a small useModelDashboardData hook.
Replace the stateful model/team drill-in (setSelectedModelId/setSelectedTeamId
full-page takeover) with real URL navigation: ?model=<id> and ?team=<id> render
ModelInfoView/TeamInfoView from the layout, so a model or team detail view is
now shareable, bookmarkable and back-button friendly. Removes the empty
placeholder pages from the first commit.
Swap the tab bar off phased-out tremor onto antd Tabs.
* fix(ui): render model tab panels standalone instead of Tremor TabPanel
AllModelsTab, ModelRetrySettingsTab and PriceDataManagementTab rooted their
render in a Tremor <TabPanel>, which only renders inside a Tremor <TabGroup>.
After the decomposition these panels live under antd Tabs / as route pages with
no such ancestor, so All Models (and the other two) rendered blank. Root them in
a plain container instead.
The existing component tests mocked @tremor/react (stubbing TabPanel to render
children), which hid this; add a regression test that renders with real Tremor
and asserts the content is visible standalone.
Also type visibleSlugs/TAB_LABELS with the canonical ModelTabSlug so a tab added
without a matching label is a compile error.
* fix(ui): make model/team drill-in navigation work under the /ui static mount
The drill-in close (Back to Models) and open were no-ops: the dashboard is a
static export served under /ui, a prefix the Next router (basePath "") does not
know, so a router.push to the current pathname with only the query changed is
deduped and never re-renders. Drive the ?model=/?team= overlay via real browser
navigation (window.location) so open and close reliably work; verified live.
Also address review feedback: gate the tab-permission redirect on teams/uiSettings
having loaded so a team admin hard-loading /add is not bounced to the base before
their membership resolves, and memoize getProviderFromModel on modelCostMapData so
the health tab's provider labels refresh when the cost map loads.
* fix(ui): use window.location.replace for the tab-permission redirect
router.replace is unreliable under the /ui static mount (same class of Next-router
issue that broke the drill-in back button), so the forbidden-tab redirect could
fail to fire. Use window.location.replace, which keeps the no-history semantics of
a permission redirect and is deterministic. Redirect stays gated on teams/uiSettings
having loaded.
* fix(ui): drive model/team drill-in with history.pushState for client-side nav
Switch the ?model=/?team= overlay navigation from window.location.assign to
window.history.pushState, which Next's App Router observes. This keeps navigation
client-side (no full page reload, React Query cache preserved) while still working
for the same-path query-only change that router.push cannot do under the /ui static
mount. Open, close (Back to Models) and browser Back are all verified in the built
UI. Adds unit coverage for the open/close/read behavior.
Extract the inline sync streaming post/decode into make_sync_call so it can be
exercised with an injected client, mirroring make_async_call, and add a sync
regression test that each token is forwarded after exactly one pulled frame.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Mirror the sagemaker_chat fix on the native sagemaker/ streaming path: the
sync and async completion handlers read the invocations-response-stream body
with iter_bytes(chunk_size=1024) / aiter_bytes(chunk_size=1024), so httpx
withholds bytes until 1024 accumulate and tokens arrive in gap-then-burst
waves. Drop the fixed chunk size so each decoded event is forwarded as its
bytes arrive.
Also add a boundary-agnostic decoder test proving frames reassemble correctly
regardless of where transport reads split the stream.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(bedrock): include type in tool_choice disable_parallel_tool_use config for Converse
* fix(bedrock): let parallel_tool_calls-derived disable flag win over raw tool_choice value
* chore(ui): add filename, size, JSX-handler, prefer-const, and antd lint rules
Wires up five error-level ESLint rules on the dashboard, grandfathering every
current offender into eslint-suppressions.json so the gate only bites new code
and ratchets down as files are fixed
- local/filename-pascal-case: new local rule requiring PascalCase .tsx names,
exempting Next.js reserved files (page, layout, route, ...) and test/spec files
(239 grandfathered)
- max-lines: 800 lines over src/**, excluding tests, src/data, and generated
schema.d.ts (20 grandfathered)
- local/no-complex-jsx-arrow: new local rule flagging inline JSX arrow handlers
with block bodies over two statements; each failure is a small extract-to-named
-handler refactor (65 grandfathered)
- prefer-const: flipped from off to error (103 grandfathered)
- no-restricted-imports: added antd to the phase-out ban alongside tremor, and
pointed both messages at shadcn/ui primitives (405 antd import sites grandfathered)
Both new local rules ship with RuleTester coverage
* fix(ui): preserve secondary extensions in filename-pascal-case suggestion
The suggestion text built the rename from only the head segment, so a
multi-dot file like my-component.utils.tsx was told to become
MyComponent.tsx instead of MyComponent.utils.tsx. Rebuild it from the
PascalCased head plus the untouched remaining segments, and add tests
covering multi-dot filenames and the hyphenated Next.js reserved names
(global-error, apple-icon, opengraph-image, twitter-image)
The UI vitest suite is CPU-bound; move it to a 16-core larger runner and raise vitest fork concurrency from 4 to 14 (leaving headroom for the coordinator, jsdom, and the OS) so the full suite and PR-scoped runs finish faster.
The huggingface embedding test fixture reloaded
litellm.llms.custom_httpx.http_handler, creating a new HTTPHandler class
object. llm_http_handler keeps the class captured at import time, so any
test running later in the same process that injects a client built from
the reloaded class fails the isinstance check and the mock is silently
discarded, causing a real network call. Under pytest-xdist loadscope this
surfaced as a deterministic failure of
test_accept_header_in_completion_request_jwt whenever an unrelated PR
shifted worker distribution.
Also removes the same reload pattern from the vertex rerank integration
test (both were previously removed in a6df01caec and resurrected by a
merge conflict resolution) and hardens the agentcore victim test by
dropping the bare except that swallowed the real error
Resolves LIT-4581
A true_passthrough MCP server created without the at-creation auth step
has no stored client_id, and the tools-page browser flow supplies none,
so GET /v1/mcp/server/oauth/{id}/authorize dead-ended on a 400
missing_client_id. The client-forwarded-token modes forbid the gateway
from persisting an OAuth client, so client acquisition moves into the one
chokepoint every caller crosses: the authorize endpoint.
resolve_ephemeral_dcr_client owns the whole mint policy (mode gate,
authorization-url precondition, required S256 PKCE, redirect trust, then
a TTL-deduped, per-server single-flighted RFC 7591 mint). The minted
client rides the encrypted OAuth state; /callback seals it with the
upstream code and server_id into an llm_ptcode_ gateway code, and
redeem_passthrough_authorization_code recovers it at the token endpoint
(server binding plus required code_verifier) to authenticate the upstream
exchange. Nothing is persisted; every value rides the encrypted blobs, so
it works across replicas.
Client acquisition is one predicate applied across the whole auth-mode
matrix: the gateway mints for a clientless authorize iff true_passthrough
(any dcr_bridge) or oauth_delegate-and-not-dcr_bridge, and the UI
gatewayMintsClientFor mirrors that set exactly so the browser pre-registers
a client through the dcr_bridge front door only for the cells the gateway
does not mint (the interactive oauth_delegate dcr_bridge sign-in and the
legacy oauth2 passthrough). A minted flow runs the bridge short-circuit
arm; the relay front door stays for external clients that present their
own client_id. Both sides are pinned against the same truth table
(test_resolve_ephemeral_dcr_client_mint_set_is_exact and the
gatewayMintsClientFor matrix test) so no mode can silently diverge. The
authorization_code hook and M2M/token-exchange modes are unchanged.
* test(e2e): cover key max_budget blocks on personal, team, and team-member keys
* refactor(e2e): convert budget enforcement cases to the resources-fixture pattern
The E2ECase class pattern existed only in this file; every other suite uses
plain pytest tests with the resources fixture. Rewrites the nine cases as two
spec classes and removes the now-dead E2ECase protocol and run_case driver
from lifecycle.py
Moves the dashboard's next pin from 16.2.6 to the latest 16.2.x patch and bumps eslint-config-next to match. Regenerating the lock also healed in explicit bundled-dependency records under @tailwindcss/oxide-wasm32-wasi
* test(ui): characterise transform-request panel behaviour before migration
* refactor(ui): migrate transform-request to shadcn
* fix(ui): keep transform-request panels within the fixed-height content fold
* fix(ui): let transform-request flow naturally so the shell scrolls instead of clipping
* test(ui): select the copy button by its accessible name
* test(ui): characterise the old usage page before migrating it
Role- and text-based coverage of the route as it behaves on Tremor, so the
shadcn migration has a regression net it did not get to write. Pins the
DISABLE_EXPENSIVE_DB_QUERIES branch (warning copy, the docs link and its
target, and that every expensive query is skipped), the admin vs non-admin
tab set, the cost cards, and the provider and customer tables
* refactor(ui): migrate old-usage to shadcn
Replaces Tremor with the installed shadcn primitives and the shared recharts
wrappers on the only file the route owns. Tabs, cards, tables, the key select
and the tag multi-select come from src/components/ui; the bar, area and donut
charts come from src/components/shared/charts. Tremor BarList has no shared
equivalent, so Total Spend Per Team is composed from ui/meter, which also means
the per-team totals stay numbers in state instead of pre-formatted strings; a
team total of 1,000 or more used to make the bar widths NaN.
The Database Query Limit Reached warning moves with it: same copy, same docs
link, still short-circuiting every expensive query.
Drops the file's no-restricted-imports suppression and the dead customTooltip,
getTopKeys, DataDict and UserData symbols. The characterisation test from the
previous commit is unchanged and green on both sides