Commit graph

82 commits

Author SHA1 Message Date
ryan-crabbe-berri
3fc73c1aff
test(ui): scrub stale return-url cookie from e2e storageState (#30317)
The login flow stores a post-login return URL in the litellm_return_url
cookie (5 minute TTL). globalSetup snapshots cookies into the per-role
storageState that every spec reuses, so when the snapshot races ahead
of the app consuming that cookie, each test inheriting it gets
redirected to the stale URL (/ui/?login=success) mid-assertion the
first time it mounts a page. That one rogue navigation is behind the
recurring e2e failures whose call logs all show "navigated to
/ui/?login=success" while waiting for an element; which specs die
varies run to run with snapshot timing. Clear the cookie right before
saving the snapshot so no test starts with a pending redirect.
2026-06-22 18:11:05 -07:00
ryan-crabbe-berri
32bdd004bd
feat(ui): migrate api-keys landing to App Router path route (#30699)
Cut the default "Virtual Keys" landing (?page=api-keys) over to a path route at
(dashboard)/api-keys. The dashboard is extracted into a shared ApiKeysDashboard
component used by both the new route and the index's inline render, so there's
no duplication. Adding the MIGRATED_PAGES entry repoints the sidebar item and
redirects ?page=api-keys to /ui/api-keys.

The index is the post-login landing and still hosts the legacy switch for the
not-yet-migrated pages (models, pass-through, usage) plus the invitation flow,
so it stays. The auto-redirect now fires only for an explicit ?page= param,
leaving the bare /ui/ landing to render inline; this keeps the return-URL
handling and the invitation_id flow (both of which run at the bare landing)
intact, where a blanket redirect would have dropped them. The new route uses
useAuthorized for the login gate, matching every other migrated route.
2026-06-18 17:38:07 -07:00
ryan-crabbe-berri
382d78ec16
feat(ui): migrate old usage report to App Router path route (#30694)
Cut the legacy "Old Usage" report (?page=usage) over from the switch in
(dashboard)/page.tsx to a path route at (dashboard)/old-usage. The segment is
old-usage rather than usage because the modern usage dashboard (new_usage)
already owns /usage. Adding the MIGRATED_PAGES entry repoints the sidebar item
and redirects existing ?page=usage links to /ui/old-usage.

The report was the switch's catch-all else, so removing it means choosing a new
fallback: collapse the now-redundant explicit api-keys arm into the else so the
main dashboard (UserDashboard) is the default. Unknown ?page= values now land on
the dashboard instead of the Old Usage report, which is the sensible default.

The new route sources identity from useAuthorized() and passes keys={null}: the
key-filter dropdown read the parent's keys state, which was already empty on
direct navigation to ?page=usage, so this preserves that rather than wiring a
paginated key fetch into a deprecated report.
2026-06-18 10:28:05 -07:00
ryan-crabbe-berri
e568d8bffb
refactor(ui): remove orphaned pass-through-settings route (#30692)
The `page == "pass-through-settings"` arm in (dashboard)/page.tsx is
unreachable: it isn't a sidebar item and nothing in the app sets
?page=pass-through-settings. The Pass-Through Endpoints UI lives as a tab
inside the Models + Endpoints view (ModelsAndEndpointsView renders
PassThroughSettings), so the standalone switch arm is dead code. Remove it,
its now-unused import, and the matching enum member in the e2e pages fixture.
2026-06-17 18:26:21 -07:00
ryan-crabbe-berri
d97b17b161
feat(ui): migrate models page to App Router path route (#30677)
* feat(ui): migrate models page to App Router path route

Cut the Models + Endpoints page over from the legacy ?page=models switch
in (dashboard)/page.tsx to a path route at (dashboard)/models-and-endpoints.
Adding the MIGRATED_PAGES entry repoints the sidebar link and redirects old
?page=models bookmarks to /ui/models-and-endpoints.

ModelsAndEndpointsView already sourced identity from useAuthorized() and its
own data via useModelsInfo(), so the token/keys/modelData/setModelData props
were dead; drop them from ModelDashboardProps (and the parent's now-unused
setModelData state) to sever the last of the shared-state coupling.

* test(ui): scope migration smoke's shell probe to the exact sidebar link

The migration smoke used a loose `locator("a", { hasText: "Virtual Keys" })`
to assert the dashboard shell rendered. The Models + Endpoints page content
itself links to the "Virtual Keys page", so on that route the substring filter
matched two anchors and tripped Playwright strict mode. Match the sidebar link
by its exact accessible name instead, which resolves to just the nav item.
2026-06-17 18:10:31 -07:00
ryan-crabbe-berri
5c41aabab0
feat(ui): cut the teams page over to the /ui/teams path route (#30343)
* feat(ui): cut the teams page over to the /ui/teams path route

OldTeams renders from app/(dashboard)/teams/page.tsx via useAuthorized. The
component already fetched its own paginated, filtered team list through
v2TeamListCall; setTeams was only a round-trip back into the shell's lifted
state, so it becomes internal useState. The organizations prop was redundant
with the useOrganizations hook the component already calls, and searchParams
was never read, so both are dropped. The shell keeps its own teams state and
fetch for the still-coupled api-keys and models arms.

Tests no longer inject teams through a prop; they mock teamListCall to drive
what the component renders.

* test(ui): drop unnecessary as-any casts on teamListCall mocks

teamListCall returns Promise<any>, so mockResolvedValue already accepts the
payload untyped. The casts pushed the repo-wide no-explicit-any lint budget
over its ceiling in CI.

* test(ui): drop redundant as-any casts from team mock fixtures

The mocked teamListCall resolves an any-typed payload, so the inner team
fixtures no longer need casts to carry a null organization_id, a keys_count
field, or partial key objects.
2026-06-15 17:42:25 -07:00
ryan-crabbe-berri
685ec00afd
feat(ui): cut the organizations page over to the /ui/organizations path route (#30336)
* feat(ui): cut the organizations page over to the /ui/organizations path route

OrganizationsTable now owns its data through React Query instead of lifted
shell state: useOrganizations (extended with optional org_id/org_alias
filters) replaces the organizations/setOrganizations prop pair, and a new
useUserModels hook replaces the userModels prop. Create and delete
invalidate the organizations list queries rather than refetching into a
parent setter. The dead currentOrg and guardrailsList props are removed.

The shell keeps its own fetchOrganizations call because the teams and
api-keys arms still read the lifted organizations state; the userModels
state had no remaining readers and is deleted.

* fix(ui): seed organization detail initialData from any cached list, not just the unfiltered one

useOrganization's initialData only read organizationKeys.list({}), so on a
session that only ever fetched a filtered organization list the detail view
fell back to a loading state and a redundant info call. Scan every cached
list variant via the lists() prefix instead, with regression tests covering
the filtered-cache hit and the no-cache fallthrough.
2026-06-13 14:04:04 -07:00
ryan-crabbe-berri
0dc203bd65
feat(ui): cut the users page over to the /ui/users path route (#30334)
ViewUserDashboard renders from app/(dashboard)/users/page.tsx via useAuthorized
and useTeams instead of the legacy ?page=users switch arm. The keys and setKeys
props were declared but never destructured by the component, so they are removed
from the interface rather than wired into the wrapper.
2026-06-12 18:19:31 -07:00
ryan-crabbe-berri
8b9a90e0fc
feat(ui): migrate agents and router-settings to path routes (#30323)
* feat(ui): cut agents and router-settings over to path routes

Both pages depended on a slice of the legacy shell's lifted state, now
replaced with React Query hooks in their route wrappers: agents pulls
teams from useTeams, and router-settings feeds the Fallbacks model
dropdown from useAllProxyModels. The shell's modelData copy only
populated after visiting the Models page in the same session, so the
dropdown was empty on a fresh load of router-settings; the hook fixes
that as a side effect of the cutover.

* refactor(ui): delete the dead modelData prop chain

AddFallbacks fetches its own model list when its modal opens and never
reads the models prop, so the whole shell modelData -> GeneralSettings
-> Fallbacks -> AddFallbacks chain fed a prop nobody consumed;
RouterSettings declared it without using it at all. Remove the chain
and the router-settings wrapper's useAllProxyModels adaptation that
was feeding it. Also corrects this PR's earlier claim: the Fallbacks
dropdown was never broken by the empty shell state, because the
component self-fetches.
2026-06-12 18:01:00 -07:00
ryan-crabbe-berri
d258e022d1
feat(ui): cut admin-panel, logging-and-alerts, model-hub-table, and usage over to path routes (#30268)
admin-panel pulls proxySettings from the shared useProxySettings query
hook (dropping the last reader of the legacy page's copy), the model
hub wrapper keeps the admin-vs-public branch as an early return, and
the usage wrapper feeds NewUsagePage from the useTeams and
useOrganizations query hooks instead of the lifted switch state.
new_usage maps to the /usage segment while the old ?page=usage report
keeps its legacy arm, asserted in the unit test so the two cannot be
confused.
2026-06-12 16:16:27 -07:00
ryan-crabbe-berri
40301820e7
feat(ui): migrate caching, cost-tracking, transform-request, ui-theme, and logs to path routes (#30267)
* feat(ui): cut caching, cost-tracking, transform-request, ui-theme, and logs over to path routes

Completes the simple-leaf portion of the page-by-page App Router
migration. All five legacy switch arms passed only identity props
(accessToken/userRole/userID, plus token/premiumUser for caching and
logs), all of which useAuthorized() provides, so each route wrapper is
a thin useAuthorized() + render. MIGRATED_PAGES routes the sidebar and
redirects the legacy ?page= URLs; the e2e fixture picks all five up in
the migration smoke and sidebar specs automatically.

* refactor(ui): colocate caching, cost-tracking, transform-request, and ui-theme components

Each had the legacy switch as its only importer. caching takes its
whole closure (cache_dashboard, cache_health, cache_settings,
response_time_indicator); CostTrackingSettings moves as the
cost-tracking components folder; the transform-request and ui-theme
single-file panels move under their routes. view_logs stays at
src/components: six other pages (guardrails monitor, tool policies,
pass-through, MCP toolsets, usage) import it. Suppressions re-keyed.

* chore: retrigger ci

e2e_ui_testing failed on three specs unrelated to this PR's pages
(team-info tabs, MCP create form) and local_testing_part1 on
test_batch_completions; all pass on the pre-merge commit and none
touch files in this diff.
2026-06-12 15:35:15 -07:00
ryan-crabbe-berri
2893f9b67b
feat(ui): migrate policies, guardrails, prompts, tool-policies, and skills to path routes (#30263)
* feat(ui): cut policies, guardrails, prompts, tool-policies, and skills over to path routes

Continues the page-by-page App Router migration. All five legacy switch
arms passed only accessToken/userRole, so each route wrapper is a thin
useAuthorized() + render. skills keeps a claude-code-plugins alias in
MIGRATED_PAGES because the old switch matched both page ids, mirroring
the api_ref/api-reference precedent.

* refactor(ui): colocate the prompts panel under its route

The new route wrapper was its only importer, so the 32-file folder
moves wholesale into (dashboard)/prompts/components; tree-escaping
relative imports (networking, molecules, common_components) become
@/components aliases and the suppressions baseline is re-keyed.
policies, guardrails, claude_code_plugins, and ToolPoliciesView stay
at src/components: each has consumers on other pages (playground
selectors, AI Hub, public model hub), so their shared/page splits go
in the colocation follow-up.

* fix(ui): move the PromptsPanel file along with its folder

@/components/prompts resolved to the prompts.tsx FILE next to the
prompts/ folder, not the folder itself; the colocation moved only the
folder, so the wrapper's ./components import and the panel's
./prompts/* imports both broke and next build failed. Move the panel
in as components/index.tsx and fix its now-escaping relative imports.
Caught by next build; tsc --noEmit missed it because incremental mode
reused a stale tsbuildinfo.

* test(ui): lock skills alias resolution in legacyKeyForPathname

Both skills and claude-code-plugins map to the skills segment, and
sidebar highlighting depends on first-match-wins returning the sidebar
key; assert it so a future reorder of MIGRATED_PAGES cannot silently
break highlighting. Mirrors the api_ref/api-reference assertion.
Flagged by Greptile.
2026-06-12 13:11:54 -07:00
Sameer Kankute
7519e37d26
feat(proxy): enforce key/team guardrails on bedrock passthrough routes (#30194)
* feat(proxy): enforce key/team guardrails on bedrock passthrough routes

/bedrock/... passthrough routes silently skipped all guardrail hooks because
CallTypes.allm_passthrough_route had no entry in guardrail_translation_mappings.
Add a dispatcher (LlmPassthroughRouteHandler) registered for that call type that
routes to BedrockPassthroughGuardrailHandler for Bedrock Converse endpoints; wire
post_call_success_hook into both the JSON and AWS event-stream response paths in
common_request_processing, including full de-anonymization for streaming responses
with proportional text distribution across original event-stream delta frames.

* refactor(proxy): address greptile feedback on bedrock passthrough guardrails

Move botocore event-stream logic from proxy/ to BedrockPassthroughGuardrailHandler.de_anonymize_converse_stream; _handle_event_stream_allm_passthrough_route becomes a thin provider dispatcher. Pass custom_headers through _handle_non_streaming_allm_passthrough_route so early-return guardrail responses include x-litellm-call-id and related headers.

* style: run black on handler and common_request_processing

* refactor(proxy): dedupe non-streaming passthrough guardrail handling

Replace the inline JSON/eventstream block in the streaming-request branch
with a call to _handle_non_streaming_allm_passthrough_route so both paths
share one implementation and cannot diverge.

* fix(bedrock): preserve trailing bytes when re-encoding converse stream

The event-stream re-encoder only emitted parsed frames, so any trailing
bytes left after the parse loop (truncated/corrupt final frame, or fewer
than 16 bytes after the last complete frame) were silently dropped from
the de-anonymized output. Capture and re-append them so the transformer
never truncates the stream.

* fix(proxy): guard non-dict post-call hook return on bedrock passthrough JSON path

* fix(proxy): guard malformed JSON body on bedrock passthrough guardrail path

* fix(proxy): close guardrail bypass via tool result text and default-mode post-call guardrails on bedrock passthrough

Pre-call extraction only read top-level Converse text blocks, so blocked
content placed under toolResult.content[].text was forwarded to Bedrock
without the key/team guardrail seeing it. Extraction now walks nested tool
result text and write-back mutates the owning block in place.

Post-call buffering for passthrough used _has_post_call_guardrails, which
excludes event_hook=None guardrails. Those guardrails run at post_call, so
their output processing was skipped and the raw upstream body was returned.
Add a passthrough-specific predicate that counts them.

* refactor(proxy): route bedrock event-stream de-anonymization through llm passthrough dispatcher

Remove the hardcoded bedrock provider guard from common_request_processing
by delegating event-stream de-anonymization to LlmPassthroughRouteHandler,
which resolves the provider from the existing handler registry. Keeps
proxy/ provider-agnostic and reuses the same dispatch path as the input
and output guardrail handlers.

Also log instead of silently dropping the result when post_call_success_hook
returns a non-dict on the JSON and event-stream passthrough paths.

* fix(proxy): close guardrail bypass on bedrock invoke passthrough routes

Pre-call extraction and post-call output processing only handled Converse
shapes, so /bedrock/model/{modelId}/invoke and invoke-with-response-stream
returned unguarded. An authenticated caller could move blocked content into
an InvokeModel payload and skip the key/team guardrail entirely.

Non-Converse Bedrock routes now fall back to the generic passthrough handler,
which scans the full request and response payloads so blocking guardrails
still run, matching how other passthrough providers are guarded.

* fix(proxy): keep non-bedrock passthrough streams streaming under post-call guardrails

* fix(bedrock): scan non-text converse blocks for passthrough guardrails

Key/team guardrails on bedrock converse passthrough only saw top-level
text blocks, so a caller could hide prompt content in toolUse.input or
toolResult.content[].json and have it forwarded to Bedrock without the
configured guardrail inspecting it, bypassing blocking guardrails by
default. Walk those arbitrary-JSON subtrees and write masked values back
in place. Extend the non-streaming converse response path to the
equivalent model-output fields (toolUse.input, reasoningContent text and
citationsContent text) while leaving structural values such as reasoning
signatures and citation sources untouched.

* fix(bedrock): make passthrough guardrail string collection iterative and type-safe

Rewrite _collect_strings with an explicit stack so it no longer recurses,
satisfying the recursive-function CI guard, and widen the holder container
type so mypy accepts indexing JSON nodes by str or int keys.

* fix(proxy): scope passthrough post-call guardrail buffering to the request

Buffering the Bedrock event stream into a single non-streaming response was
gated on whether any post_call guardrail existed globally, so every
converse-stream request lost streaming once any post_call guardrail was
registered, even for keys that did not reference it. Mirror the gate used by
post_call_success_hook (should_run_guardrail against the request's merged
guardrails) so only requests whose key/team actually trigger a post_call
guardrail are buffered.

* fix(bedrock): guardrail non-text converse stream deltas on passthrough

de_anonymize_event_stream only routed delta.text through the post-call guardrail, so model output streamed in reasoningContent.text, toolUse.input or citationsContent.content[].text was forwarded raw and skipped masking/blocking. Collect every user-visible text field per contentBlockDelta, concatenate per logical stream so split mask tokens still reassemble, run them through the hook, then redistribute the guardrailed text back into the matching delta fields. This brings streaming coverage in line with the non-streaming Converse output handler.

* refactor(proxy): keep bedrock event-stream content-type detection in llms

Move the vnd.amazon.eventstream content-type check out of the proxy
passthrough path into BedrockPassthroughGuardrailHandler via the
LlmPassthroughRouteHandler dispatcher, so proxy code stays
provider-agnostic. Also patch the actually-called
_has_post_call_guardrails_for_passthrough in the malformed-body
regression test instead of the unused _has_post_call_guardrails.

* fix(proxy): forward upstream headers on bedrock guardrail passthrough responses

Mirror the non-guardrail passthrough path by merging the upstream
response headers (via get_response_headers) into the guardrailed
non-streaming and event-stream responses, so headers like
x-amzn-requestid survive when a post-call guardrail rewrites the body.
Drop the stray fastapi HTTPException import from the SDK-tree handler
test in favor of a local sentinel exception.

* fix(bedrock): scan tool definitions and additional request fields for passthrough guardrails

Converse passthrough guardrails only scanned system and message content, so
a key holder could route blocked or PII text through toolConfig tool names,
descriptions and input schemas or through additionalModelRequestFields, all
of which are still forwarded to Bedrock. Collect strings from those fields
too so key/team guardrails inspect and rewrite them, matching how the
chat-completions path forwards tool definitions to guardrails.

* fix(bedrock): log instead of silently dropping passthrough guardrail edge cases

* test(local): skip httpbin timeout probe when the service returns 5xx

local_testing_part1 was failing on test_post_delay_exceeds_per_request_timeout_raises
because httpbin.org/delay/10 intermittently answers 503 instead of delaying, so
HTTPHandler.post raised MaskedHTTPStatusError rather than the expected Timeout. The
test already means to skip when httpbin is unavailable, but its guard only probed
GET /get and ignored a server error on the delay endpoint. Treat a 5xx from httpbin as
'service unavailable' and skip, which is outside this repo's control, while still
asserting Timeout when httpbin genuinely delays.

* fix(proxy): set content-type on buffered bedrock passthrough event-stream responses

* fix(bedrock): skip passthrough output write-back when guardrail returns no texts

* fix(proxy): apply response-headers hook on guardrailed bedrock passthrough responses

* refactor(bedrock): import event-stream crc32 from binascii not botocore internals

* fix(proxy): scope bedrock passthrough stream buffering to de-anonymizable endpoints

Only buffer a passthrough event stream into a non-streaming response when the
resolved provider and endpoint actually have an event-stream guardrail handler
that can rewrite frames (Bedrock converse-stream). Other Bedrock event-stream
endpoints such as invoke-with-response-stream keep streaming, since the Converse
handler leaves their frames untouched and buffering would silently break the
streaming contract for no content change.

* test(ui-e2e): re-issue deep-link navigation when auth bootstrap drops the page param

navigateToPage deep-links to /ui?page=<page> then proceeds once the network
settles, but a fresh load can race the auth bootstrap: the app momentarily
treats the session as anonymous, bounces through /ui/login, and returns to the
default Virtual Keys page with the ?page= query param dropped. The helper never
checked where it actually landed, so any single bounce left callers asserting
against the wrong page and timing out (mcpServers, modelHub, addModel).

Confirm the requested page is what rendered and re-issue the navigation when it
was clobbered; auth is warm by the second load so the param sticks. Migrated
path routes are left alone since they intentionally leave the legacy root.

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-06-12 07:23:29 -07:00
ryan-crabbe-berri
2d576b5695
feat(ui): migrate mcp-servers, search-tools, tag-management, vector-stores, and memory to path routes (#30261)
* feat(ui): cut mcp-servers, search-tools, tag-management, vector-stores, and memory over to path routes

Continues the page-by-page App Router migration. All five legacy switch
arms passed only accessToken/userRole/userID, so each route wrapper is a
thin useAuthorized() + render. MIGRATED_PAGES routes the sidebar and
redirects the legacy ?page= URLs; the e2e fixture picks all five up in
the migration smoke and sidebar specs automatically.

* refactor(ui): colocate MemoryView under the memory route

The legacy switch was its only importer, so the three files move
wholesale into (dashboard)/memory/components. mcp_tools, SearchTools,
tag_management, and vector_store_management stay at src/components for
now: each has importers on other pages (teams, playground, guardrails),
so their colocation needs a shared/page split as a follow-up.
2026-06-11 18:17:06 -07:00
ryan-crabbe-berri
3ad385a8a4
feat(ui): migrate budgets, workflows, and guardrails-monitor to path routes (#30236)
* feat(ui): cut budgets, workflows, and guardrails-monitor over to path routes

Continues the page-by-page App Router migration (#30185, #30226). All
three legacy switch arms passed only accessToken, so each route wrapper
is a thin useAuthorized() + render. MIGRATED_PAGES routes the sidebar
and redirects the legacy ?page= URLs; the e2e fixture picks all three
up in the migration smoke and sidebar specs automatically.

* refactor(ui): colocate budgets, workflows, and guardrails-monitor components

budgets and workflow_runs were imported only by the legacy switch, so
they move wholesale into their route folders; the budgetItem type
hoists into the shared useBudgets hook, which owns the API response
shape, so the hooks layer no longer imports from a page folder.
GuardrailsMonitor keeps LogViewer, mockData, and MetricCard at the
shared src/components home because ToolDetail and ToolPolicies import
them; the rest moves. eslint suppressions are re-keyed accordingly.

* fix(ui): restore MetricCard test-utils path and merge duplicate import

MetricCard.test.tsx got the moved-tree depth rewrite before being moved
back to src/components/GuardrailsMonitor, leaving a five-level path
that escapes the project root; the suite failed at import. Also merge
the two imports from useBudgets in budget_panel.tsx. Both flagged by
Greptile.
2026-06-11 14:27:40 -07:00
ryan-crabbe-berri
a2c916fb45
feat(ui): migrate projects and access-groups to path routes (#30226)
* feat(ui): cut projects and access-groups over to path routes

Same recipe as playground (#30185): MIGRATED_PAGES entries route the
sidebar and redirect the legacy ?page= URLs, the switch arms are
deleted, and the e2e fixture grows two entries. Both components were
already zero-prop and self-fetching via React Query hooks, so the
route wrappers are trivial.

* refactor(ui): move Projects and AccessGroups components into their route folders

Both folders were imported only by the legacy switch, so they colocate
wholesale under (dashboard)/{projects,access-groups}/components. Their
React Query hooks stay in the shared (dashboard)/hooks layer. eslint
suppressions are re-keyed to the new paths.

* test(ui): enable enable_projects_ui in e2e global setup

The projects migration smoke clicks the Projects sidebar link, which
only renders when the enterprise-gated enable_projects_ui setting is
on; the seeded e2e database starts with it off, so the locator timed
out in both e2e_ui_testing jobs. CI already launches the proxy with
LITELLM_LICENSE for premium UI coverage, so flip the setting in
globalSetup via the same /update/ui_settings call the admin UI toggle
makes, failing loudly if the PATCH is rejected.

* test(ui): use Playwright request context instead of raw fetch in global setup

The frontend lint bans raw fetch() outside src/lib/http/; the e2e
convention for proxy API calls is Playwright's APIRequestContext, as
in routerSettings.spec.ts.
2026-06-11 13:20:21 -07:00
ryan-crabbe-berri
530c0b2326
feat(ui): migrate playground to path routing and colocate its files (#30185)
* feat(ui): cut playground over to the /ui/playground path route

Follows the api-reference recipe: the sidebar and deep links route
llm-playground to the path route, ?page=llm-playground redirects, and
the legacy switch arm is deleted. The route's page.tsx was already the
real implementation, so no view extraction was needed.

* refactor(ui): move playground-owned files into its route folder

Per the (dashboard) README convention, page-owned code lives in the
page's folder: chat_ui/compareUI/complianceUI components, the chat
hooks, and the playground-only llm_calls helpers move under
(dashboard)/playground/. Modules with non-playground consumers (chat
message primitives; fetch_models, chat_completion, responses_api) stay
at their lowest common ancestor in src/components/{chat_ui,llm_calls}
because legacy pages still import them. eslint-suppressions entries are
re-keyed to the new paths so the grandfathered baseline still applies.

* test(ui): teach sidebar e2e spec about migrated path routes

The sidebar spec asserted ?page=<key> for every item, which the
playground cutover correctly broke: the sidebar now links to
/ui/playground and the legacy URL redirects there. Drive the expected
URL from the migration fixture (now a page-id -> segment map) so
future cutovers only add a fixture entry. Also wrap one import line
in AgentBuilderView.tsx that the move left unformatted; the changed-
files prettier check flagged it.
2026-06-11 12:07:17 -07:00
ryan-crabbe-berri
6ae8a509f0
test(ui): data-driven App Router migration E2E smoke (default + server-root-path) (#29974)
* test(ui): add a data-driven App Router migration E2E smoke

Add a growing Playwright smoke for migrated pages: for each segment it deep-links
to the path route, asserts the URL and that the dashboard shell rendered, then
clicks off to a legacy page and asserts navigation still works. Driven by
e2e_tests/fixtures/migratedPages.ts, so adding a page is one line.

Runs in two situations against the same proxy: the default mount (npm run
e2e:migration) and a non-root SERVER_ROOT_PATH mount (npm run e2e:migration:root).
globalSetup now logs in at `${SERVER_ROOT_PATH}/ui/login` so the admin storage
state is valid under a prefix. Seeded with api-reference; append the rest as their
migrations merge.

* test(ui): support headed slow-motion + watch pauses in the migration smoke

Honor SLOWMO in the server-root-path config (the default config already did),
and add an env-gated E2E_WATCH_MS pause so a headed run lingers on each state.
Both are no-ops by default, so CI behavior is unchanged.

* test(ui): make the migration smoke a sidebar-click user journey

Rework the smoke from deep-linking to a real navigation journey: start at the
landing page, click the migrated page in the sidebar (expanding submenus for
nested items), assert the path route rendered, reload it (the check a wrong
server_root_path breaks), bounce to a legacy page and back, and — once two pages
are migrated — navigate directly between two migrated pages. Verifies via URL +
shell render, driven by the same fixture list.

* test(ui): address review on the migration smoke

Escape ROOT and segment before interpolating them into RegExp URL matchers so a
future segment containing regex metacharacters can't silently widen the match.
Make the server-root-path config fail fast when SERVER_ROOT_PATH is unset instead
of silently re-running the default mount and passing without exercising the prefix.

* test(ui): drop unused watch helper and fix stale smoke README

* test(ui): run the migration smoke under a server root path in CI

* test(ui): harden + instrument the server-root-path proxy reboot in CI

* test(ui): run the server-root-path migration smoke as its own CI job

Replace the in-place proxy reboot in e2e_ui_testing with a dedicated
e2e_ui_testing_server_root_path job that boots the proxy once with
SERVER_ROOT_PATH=/litellm, matching how every other proxy variant in the
config gets its own job rather than killing and relaunching the live proxy.

The reboot was failing deterministically: after pkill -9 and relaunch the
prefixed proxy never came back up on :4000 (connection refused), so the smoke
never ran. The readiness step that was supposed to surface the cause could
never reach its boot-log tail because CircleCI runs steps under bash -eo
pipefail and the preceding `curl -sv ... | tail` aborted the step with curl's
exit 7. Booting the proxy as the job's own background step lets any boot crash
land in that step's log instead of being swallowed.

The default e2e_ui_testing job is unchanged aside from dropping the reboot,
prefixed-readiness, and prefixed-smoke steps; the migration smoke still runs at
the root mount there via the default Playwright config.
2026-06-09 10:40:01 -07:00
Mateo Wang
4ec4ab99d0
feat(mcp): per-server env vars with global + per-user scopes (#28917) 2026-06-05 20:15:11 -07:00
ryan-crabbe-berri
4a5644d51e
refactor(ui): centralize proxy base URL resolution into tested resolver (#29793)
* refactor(ui): centralize proxy base URL resolution into tested resolver

The API base URL join logic was hand-rolled inside networking.tsx and
re-derived inline at hundreds of call sites, with no test coverage and a
latent double-slash bug when the base carried a trailing slash. This pulls
the join into a single pure resolveApiBase() with full unit coverage and
routes the existing resolution through it, also de-duplicating the env
precedence ladder that was copied in two places.

* test(ui): assert root-path redirect joins prefix exactly once

The existing toContain check accepts a doubled separator; tighten it to a
strict prefix match plus a no-double-slash assertion so a regression in the
resolveApiBase origin+SERVER_ROOT_PATH join is caught end-to-end.
2026-06-05 11:53:26 -07:00
ryan-crabbe-berri
7edf3a9cb5
style(ui): run prettier --write across the dashboard (#29622)
Formatting-only pass; no logic changes. Brings the UI into compliance
with .prettierrc so the new format-check CI job passes
2026-06-04 11:37:54 -07:00
yuneng-jiang
54ed5a4eb5
fix(e2e): tolerate trailing slash in SERVER_ROOT_PATH login redirect (#29369)
The Next.js admin UI is exported with trailingSlash: true, so the proxy
serves /ui/login at /ui/login/index.html and 308s /ui/login → /ui/login/.
The waitForURL predicate used endsWith("/ui/login"), which never matched
the canonicalized URL and timed out after 15s.

This was masked until the build artifacts were regenerated against the
AuthContext fix: the prior bundles still hit the racy redirect path that
fired before proxyBaseUrl was populated, producing /ui/login (no prefix,
no proxy round-trip, no trailing slash) which fortuitously satisfied the
predicate. The first PR to ship the corrected bundle exposed the
assertion bug.

Switch the predicate to includes("/ui/login"); the prefix assertion below
still validates the SERVER_ROOT_PATH preservation that is the actual
contract under test.
2026-05-30 20:00:33 -07:00
ryan-crabbe-berri
a9cc6ed68c
test(e2e): cover PROXY_LOGOUT_URL redirect on Logout (#29080)
* test(e2e): cover PROXY_LOGOUT_URL redirect on Logout

Env-gated spec mirroring the existing serverRootPathRedirect pattern:
when the proxy is booted with PROXY_LOGOUT_URL set, clicking Logout in
the navbar must navigate to that external URL. The standard run_e2e.sh
exports an empty value so the rest of the suite is unaffected; this
spec self-skips unless the env var is populated.

* test(e2e): run PROXY_LOGOUT_URL spec in the suite + harden logout assertions

Boot the e2e proxy with PROXY_LOGOUT_URL set (job-level env in CircleCI and
run_e2e.sh) so proxyLogoutUrl.spec.ts actually runs instead of self-skipping.
Nothing else in the suite performs a logout, so this only affects the behavior
under test.

Harden the spec to verify the logout flow rather than a URL substring:
- wait for /sso/get/ui_settings before clicking so logoutUrl is populated
  (otherwise window.location.href = "" silently reloads same-origin)
- assert a token cookie exists first, and is cleared after logout
- locate the dropdown via getByRole instead of internal antd CSS classes
- stub the external destination and assert on URL origin + path prefix

* test(e2e): assert exact PROXY_LOGOUT_URL on logout redirect

Replace the origin + startsWith(pathname) checks with a single normalized
href comparison. With PROXY_LOGOUT_URL=https://www.example.com the path was
"/", so startsWith("/") matched any path and left path/query/hash
unchecked. Comparing normalized hrefs pins scheme, host, port, path, query
and hash while still tolerating the browser's trailing-slash/default-port
normalization.
2026-05-30 18:19:04 -07:00
ryan-crabbe-berri
37e6e2da1c
test(e2e): assert internal-user navbar identity is scoped to that user (#29077)
* test(e2e): assert internal-user navbar identity is scoped to that user

The existing login.spec.ts only checks the admin's navbar identity.
This adds the symmetric check for the internal user — verifying the
account button + dropdown surface the internal user's email, id, and
role, and that no admin-scoped values leak through.

* test(e2e): harden navbar identity test per review feedback

Locate the user dropdown panel by a data-testid on the popupRender div
instead of Ant Design internal + Tailwind class names, so styling
refactors no longer risk breaking the identity-scoping assertions.
Source the seeded user emails/ids from shared constants (match seed.sql)
instead of hardcoding them inline.
2026-05-30 00:29:30 -07:00
ryan-crabbe-berri
892838963c
test(e2e): cover Internal User create-key flow when in no teams (#29083)
* test(e2e): cover Internal User create-key flow when in no teams

The seeded e2e-internal-user is in two teams, so the "no team" branch
of the Create Key modal — where the team dropdown must render empty —
was unreachable. Seeds a noteam@test.local user and adds a spec that
logs in fresh, opens the modal, and asserts the dropdown has zero
options.

* test(e2e): harden no-team dropdown assertion + add with-teams counterpart

Replace the one-shot count() check with a settled-empty assertion: wait for
the dropdown's loaded "No teams found" state before asserting zero options,
so the test can't pass on a transient empty frame while the team-options
request is still in flight.

Add internalUserWithTeams.spec.ts as the differential partner; it logs in as
the seeded e2e-internal-user (two team memberships) and asserts the dropdown
lists exactly those teams. Without it, the no-team spec's zero-options
assertion would still pass against a regression that empties the dropdown for
every user.
2026-05-30 00:26:28 -07:00
ryan-crabbe-berri
5eafe1c1fc
test(e2e): cover navbar Logout flow as proxy admin (#29076)
* test(e2e): cover navbar Logout flow as proxy admin

The Logout button under the navbar User dropdown was an uncovered
manual-QA step. This test signs in as admin, opens the dropdown,
clicks Logout, then navigates to a protected page and asserts the
redirect to /ui/login — proving the session was cleared.

* test(e2e): fix logout dropdown trigger and account-menu selector

The button never rendered the literal text "User" (it shows initials +
display name), and the antd Dropdown uses trigger={["click"]}, so the
synthetic mouseover/mouseenter never opened the popup. Open it with a real
click on the button's aria-label ("Account menu — ...").
2026-05-29 14:44:38 -07:00
ryan-crabbe-berri
fcd5760891
test(e2e): cover Internal User key modal, team info, key page (#29074)
* test(e2e): cover Internal User key modal, team info, key page

Three previously-uncovered manual-QA paths for the Internal User role:

- Create Key modal — confirm the team dropdown is populated with the
  user's teams (verifying the role-scoped UI flow exists).
- Team info page — confirm the Settings/Members tabs are hidden for a
  regular team member; only the read-only tabs render.
- Virtual Keys page — confirm the proxy's internal litellm-dashboard
  team keys never leak into an internal user's table.

* test(e2e): share clickTeamId helper, strengthen key-filter assertion

Address review feedback on the Internal User e2e spec:
- Extract clickTeamId into helpers/navigation.ts; import in both
  internalUser and teams specs instead of duplicating it.
- Anchor the litellm-dashboard absence check on the user's own seeded
  key so it cannot pass vacuously against an empty table.
- Drop redundant dismissFeedbackPopup calls (navigateToPage already
  dismisses internally).
2026-05-29 14:36:27 -07:00
ryan-crabbe-berri
10bda4456a
test(e2e): cover Internal Viewer nav, key, and team-info gating (#29075)
* test(e2e): cover Internal Viewer nav, key, and team-info gating

Three previously-uncovered manual-QA paths for the Internal Viewer role:

- Nav only renders the read-only sections; admin-only items
  (Internal Users, Organizations, Models + Endpoints) stay hidden.
- Virtual Keys page hides Create New Key, and the key detail view
  hides Regenerate / Reset Spend / Delete actions.
- Team info page hides Members and Settings tabs for the viewer.

* test(e2e): scope viewer nav to sidebar, strengthen tab assertions

Address review feedback on the Internal Viewer e2e spec:
- Scope the nav test to the sidebar complementary landmark and match
  items by link role + accessible name. The prior CSS nav, aside
  selector grabbed the top bar (the sidebar is a complementary
  landmark, not a <nav> tag), so the assertions never hit the real
  nav links.
- Land via navigateToPage so the networkidle wait settles the
  role-gated nav before asserting.
- Assert the Virtual Keys tab is visible (was only commented).
- Use toHaveCount(0) for hidden team tabs to match the nav block;
  tabs are conditionally rendered, not CSS-hidden.
- Drop redundant dismissFeedbackPopup calls (navigateToPage already
  dismisses internally).
2026-05-29 14:36:06 -07:00
ryan-crabbe-berri
2bfbf14882
test(e2e): cover Team Admin view + member + key flows (#29072)
* test(e2e): cover Team Admin view + member + key flows

Adds a new spec exercising the previously-uncovered team-admin manual-QA
items: viewing all team keys (including other members'), adding a member,
removing a member, and creating a team key with All Team Models. Also
seeds a dedicated invitee user so the add-member test can run in parallel
with the proxy-admin invite test without colliding on the team roster.

* test(e2e): harden team-admin member specs per review feedback

Address Greptile feedback on the Team Admin spec:
- locate the delete action via getByTestId("delete-member") instead of
  the fragile svg/img .last() selector
- match the seeded removable member by user_id (members_with_roles stores
  no email, so the roster renders user_id)
- assert exact success-toast strings rather than broad regexes that could
  match unrelated "success" text
2026-05-28 23:19:16 -07:00
ryan-crabbe-berri
5699a06413
test(e2e): cover AI Hub make-public flow and public model_hub_table (#29071)
Some checks are pending
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Waiting to run
Unit Tests: Proxy DB Operations / auth-checks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / budgets (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / custom-logging (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / db-and-spend (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / key-generation (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / logging-misc (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-runtime (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-server-core (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / schema-migration (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-utils (push) Blocked by required conditions
Unit Tests: Security / security (push) Waiting to run
* test(e2e): cover AI Hub make-public flow and public model_hub_table

Three previously-uncovered manual-QA paths land in one spec:

- Admin opens "Select Models to Make Public", advances through the
  multi-step modal, and verifies the success toast.
- AI Hub tab strip exposes Model Hub / Agent Hub / MCP Hub / Skill Hub
  — note the manual-QA "Claude Code Plugin Marketplace" label was
  renamed to Skill Hub; the test pins the current name.
- Anonymous /ui/model_hub_table loads with the master key as `?key=`
  and renders the Model Hub tab. Agent Hub / MCP Hub tabs are
  conditional on public data and are not asserted here.

* test(e2e): harden AI Hub make-public + public hub assertions

Address Greptile review:

- Make-public test now asserts "Select All (N)" with N>=1 before clicking,
  so a missing-seed-data run surfaces immediately instead of timing out
  on the disabled Next button or the success toast.
- Public model_hub_table test dismisses the feedback popup before the
  tab visibility assertion, matching the ordering used by navigateToPage
  so a popup race can't mask the tab mid-evaluation.

* docs(e2e): explain admin vs public AI Hub tab asymmetry

Greptile flagged the all-4-tabs assertion as a potential CI flake,
inferring from the public-page comment that Agent Hub / MCP Hub might
be data-conditional in the admin view too. They aren't — ModelHubTable
renders all four tabs unconditionally for admins. Document the asymmetry
inline so future readers (and future review passes) don't re-derive it.
2026-05-27 21:15:40 -07:00
ryan-crabbe-berri
bc31c570f0
test(e2e): cover add-MCP-server flow via discovery → custom form (#29070)
* test(e2e): cover add-MCP-server flow via discovery → custom form

The "Add MCP server" manual-QA step was uncovered. This adds a test
that opens the discovery modal, jumps into the custom-server form,
fills name + Streamable HTTP transport + a placeholder URL + None
auth, submits, and verifies both the success toast and the new row.

* test(e2e): apply greptile fixes to MCP add-server test

- Anchor the auth-type Select via its enclosing Collapse panel
  ("Authentication") instead of the placeholder text. The Form.Item has
  no label prop, so the previous `hasText: /auth type/i` filter was
  matching via "Select auth type" placeholder copy — fragile.
- Document the intentional lack of teardown, matching the pattern used
  in addModel.spec.ts: the e2e runner discards the DB per invocation.

Addresses Greptile P2s on PR #29070.

* test(e2e): scope MCP row assertion to the servers table

Scope the post-create row lookup to `table tbody` so the form modal's
`server_name` input — which still holds the timestamped value during
its close animation — can't satisfy the assertion before the server
actually lands in the list.

* docs(e2e): note MCP coverage scope and link to tracker

This spec only smoke-tests the happy-path Streamable HTTP + None auth
flow. Add a top-of-file comment pointing at E2E_COVERAGE.md so future
contributors can see what's still uncovered (other transports, all
auth types, edit/delete, BYOK, tool list/call, access groups).
2026-05-27 21:15:31 -07:00
ryan-crabbe-berri
9cac0471ae
test(e2e): cover Team-BYOK add-model flow as proxy admin (#29068)
* test(e2e): cover Team-BYOK add-model flow as proxy admin

The team-only model + team assignment was an uncovered manual-QA path.
This adds a premium-gated test that toggles Team-BYOK, picks the seeded
E2E Team CRUD, submits, and verifies the model lands in All Models with
the team alias attached.

* test(e2e): apply greptile fixes to Team-BYOK test

- Add the 2s networkidle settle that the sibling addModel tests use —
  networkidle fires before the All Models table finishes re-rendering,
  so the search input was racing with the render.
- Assert on `models-results-count` before inspecting the table body so
  an empty search result fails with a clear "expected results count"
  message instead of timing out on a missing row.

Addresses Greptile P2s on PR #29068.

* test(e2e): harden Team-BYOK test against flake and stale state

- Add before/after cleanup that deletes any Cohere model already scoped
  to e2e-team-crud via /v2/model/info + /model/delete, so Playwright
  retries and local reruns don't accumulate rows.
- Pick the team from the dropdown by role/option name instead of a
  global getByText match — avoids matching a previously-rendered tag
  elsewhere in the form.
- Scope the "created successfully" assertion to .ant-notification so a
  stale toast from an earlier test in the same browser context can't
  vacuously satisfy it.
- Tighten the All Models assertion: require a single row that contains
  BOTH the cohere model name AND the e2e-team-crud alias, so the
  team-less wildcard from the sibling "Add wildcard route" test can't
  satisfy the check.
2026-05-27 16:05:27 -07:00
ryan-crabbe-berri
b0ea013042
test(e2e): cover add-fallback flow in Router Settings (#29069)
* test(e2e): cover add-fallback flow in Router Settings as proxy admin

The Router Settings → Fallbacks → Add Fallbacks flow was an uncovered
manual-QA path. This adds a test that opens the modal, picks a primary
+ fallback from the seeded mock models, saves, and verifies both render
in the fallback table.

* fix(e2e): make router-fallback test idempotent and pick antd options by text

- Match `.ant-select-item-option` by text instead of `getByTitle(...)` —
  FallbackGroupConfig uses `options=` (not <Select.Option> children), so
  no `title` attribute is emitted and the title-based selector hangs.
- Add before/after hooks that wipe any fallback for fake-openai-gpt-4 via
  /config/update so retries and local reruns don't trip on leftover state.
- Tighten the success assertion to a single tbody row containing BOTH the
  primary and the fallback names — pre-existing rows can no longer
  vacuously satisfy the check.
- Fix the stale "Three tabs" comment to "Four tabs".

Addresses Greptile P2s on PR #29069.

* fix(e2e): keyboard-select fallback models + correct cleanup endpoint

- Replace mouse-based option clicks with click-to-focus + type + Enter.
  FallbackGroupConfig's Selects use `options=` and a custom
  getPopupContainer, so locating options via `.ant-select-dropdown`
  hit several races: DOM-clicks left antd's popup state stale (the
  primary popup then intercepted the fallback click), `getByRole`
  matched always-mounted hidden options, and pointer stability fought
  the open animation. Typing into the showSearch input narrows the
  listbox to one option and Enter selects it cleanly.
- Assert on dialog-side state changes (the active tab adopts the
  primary model name; the chain helper shows "1/10 used") instead of
  popup contents — these reflect the actual selection landing.
- Cleanup helper now hits /get/config/callbacks (the real endpoint;
  /get/callbacks returns 404), so the before/after reset actually
  clears prior router_settings.fallbacks state.
2026-05-27 15:52:19 -07:00
ryan-crabbe-berri
7cae5dc08a
test(ui): e2e cover team model edit + admin identity in navbar (#28652)
* test(ui): e2e cover team model edit + admin identity in navbar

Adds two Playwright tests as part of the manual-QA → e2e migration:
"Edit team model selection" exercises the Settings tab Models multi-select
+ Save Changes flow on a seeded team, and the existing login test now
opens the User dropdown and asserts the role and User ID render — guarding
against regressions where login succeeds but the auth context is empty.

Resolves LIT-3093

* test(ui): restore seeded models in team-edit test so retries don't fail

The 'Edit team model selection' test removed fake-anthropic-claude from
E2E_TEAM_CRUD_ID without restoring it. CI runs with retries: 2 and the seed
script runs once before the suite, so a flake on this test would fail the
retry at the "tag is visible" assertion. Wrap the test in try/finally and
restore the seeded models via /team/update before and after.

* test(e2e): fail loudly if team/update restore call fails

Surfaces the real cause when the master key is wrong or the proxy is
unreachable, instead of silently leaving the team in a stale state and
failing later on the visibility assertion.

* fix(e2e): match navbar account button by aria-label, not non-existent "User" text

The previous trigger filter (hasText: /^User$/) didn't match the rendered
UserDropdown button — its text is the displayName ("Account" for the
master-key admin, an email for SSO users), never "User". The evaluate
call then timed out after 15s in CI. Use the stable aria-label prefix
the component always emits, and click directly since the dropdown is
configured trigger=["click"] (the synthetic hover was unnecessary).
2026-05-27 14:45:22 -07:00
ryan-crabbe-berri
73e9071311
refactor(ui): extract auth state into AuthContext (#28910)
Some checks are pending
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Waiting to run
Unit Tests: Proxy DB Operations / auth-checks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / budgets (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / custom-logging (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / db-and-spend (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / key-generation (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / logging-misc (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-runtime (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-server-core (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / schema-migration (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-utils (push) Blocked by required conditions
Unit Tests: Security / security (push) Waiting to run
* refactor(ui): extract auth state into AuthContext

Move auth state (token, userID, userRole, accessToken, premiumUser, userEmail,
disabledPersonalKeyCreation, showSSOBanner) out of src/app/page.tsx into a
new AuthProvider at src/contexts/AuthContext.tsx. Wrapped at the root layout
so login/onboarding/dashboard routes all have access via useAuth().

Day 1 foundation for the App Router migration: migrated (dashboard)/X/page.tsx
route entry points won't have a parent passing props, so shared auth state
must live in a context they can read from.

Sub-components are unchanged — they still receive accessToken/userID/userRole
as props from page.tsx (which now reads them from useAuth()). Only the
page.tsx → top-level-page-component handoff is de-drilled; deeper prop
drilling is left for the per-page migration to address.

Net change: -86 lines from page.tsx (state + two effects moved), +5 in
layout.tsx (provider wrap), new AuthContext.tsx (~140 lines), test update
to wrap CreateKeyPage in AuthProvider.

Fixes LIT-3366
Part of LIT-3128

* fix(ui): await getUiConfig before clearing authLoading

The AuthContext refactor flipped authLoading to false synchronously on mount
while letting getUiConfig() run fire-and-forget. On SERVER_ROOT_PATH deployments
this races the unauthenticated login-redirect effect: the redirect fires with
proxyBaseUrl still at its module-init value, sending users to /ui/login instead
of {SERVER_ROOT_PATH}/ui/login.

Restores the original sequencing inside AuthProvider's mount effect and adds a
Playwright spec wired into the existing SERVER_ROOT_PATH workflow matrix. The
spec delays the config endpoint via page.route() to make the race deterministic
across CI runners.
2026-05-26 17:53:03 -07:00
ryan-crabbe-berri
f75a7c6b22
fix(model-edit): allow clearing custom pricing on wildcard models (#28719)
* fix(model-edit): allow clearing custom input/output cost on wildcard deployments

A user-set pricing override on a `/model/*` wildcard deployment could not
be removed: clearing the Input/Output Cost fields in the UI succeeded
visually, but the next read still showed the old values because both
`litellm_params` and `model_info` (mirrored via `SPECIAL_MODEL_INFO_PARAMS`)
retained the original rates.

UI: when the pricing field is touched but left empty, send `null` instead
of dropping it from the payload so the backend sees the clear intent. The
cache-read-cost fallback now guards against `null` as well as `undefined`
so a cleared input cost cannot silently wipe the cache-read override.

Backend: `update_db_model` honors explicit-null clears, but ONLY for
`SPECIAL_MODEL_INFO_PARAMS` (the 4 pricing fields). Restricting the
null-clear path prevents a team-scoped caller from using this codepath to
null out privileged fields like `team_id` or access groups.

Tests cover both clear paths (`litellm_params` and `model_info`), the
SPECIAL_MODEL_INFO_PARAMS mirror, PATCH semantics for omitted fields, and
the security guard that non-pricing nulls don't reach the merged dict.

Resolves LIT-3250

* fix(model-edit): run null-clears after both merges, not interleaved

The previous version cleared `model_info` from inside the litellm_params
merge block, but the subsequent `model_info.update(...)` re-injected the
old pricing because the UI's PATCH carries the full model_info blob with
the stale values still in it. Move the explicit-null clear pass to after
both merges so a model_info passthrough cannot resurrect cleared fields.

Adds a regression test for the realistic UI submit shape (both blobs in
the patch, model_info still holding the old pricing).

* test(e2e): clear-custom-pricing flow with create/delete cleanup

Covers the dashboard model edit form's pricing-clear flow end-to-end:
seeds a deployment with custom input/output pricing, drives the UI to
clear both fields, asserts the outgoing PATCH sends explicit nulls,
and confirms via /v2/model/info that the override is gone from both
litellm_params and model_info.

The dashboard DB persists across this suite, so beforeEach creates a
uniquely-named deployment and afterEach POSTs /model/delete to leave
the DB clean regardless of test outcome.

* fix(model-edit): extend pricing clear to cache_read and cache_write costs

Pre-existing parallel of the wildcard input/output cost bug: cleared
cache_read_input_token_cost and cache_creation_input_token_cost overrides
silently persisted because the UI omitted the key (delete or fallback) and
the backend null-clear allowlist did not cover them.

- types/router.py: add cache_read_input_token_cost and
  cache_creation_input_token_cost to SPECIAL_MODEL_INFO_PARAMS, so they are
  mirrored between litellm_params and model_info by Deployment.__init__ and
  honoured by the null-clear loop in update_db_model.
- model_info_view.tsx: emit explicit null for touched-but-empty cache_read
  and cache_write fields. Preserve the input_cost->cache_read mirror only
  when cache_read itself was not touched.
- model_management_endpoints.py: update the allowlist comment.
- Tests: three new unit tests for cache clear paths and a preserve check;
  the e2e spec now seeds, clears, and asserts null PATCH + key-absence for
  all four pricing fields.
2026-05-26 09:37:23 -07:00
ryan-crabbe-berri
07bcd2c19e
test(e2e): forward LITELLM_LICENSE to UI e2e proxy (#28398)
* test(e2e): forward LITELLM_LICENSE to UI e2e proxy

The UI e2e job ran without LITELLM_LICENSE, so premium_user was always
false in the issued login JWT and premium-gated UI surfaces (Team-BYOK
Model switch, etc.) couldn't be driven through the UI. Forward the env
var from run_e2e.sh and the CircleCI e2e_ui_testing job, and add a
sanity test that decodes the admin storage state token and asserts
premium_user=true so the wiring fails loudly if it ever regresses.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Update ui/litellm-dashboard/e2e_tests/tests/proxy-admin/license.spec.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-05-21 18:17:03 -07:00
ryan-crabbe-berri
f82ff7ee2a
test(ui-e2e): admin key creation with a specific proxy model (#28365)
* test(ui-e2e): add admin key creation with a specific proxy model

Adds Playwright coverage for creating a key (no team) scoped to a single
proxy model, complementing the existing All-Proxy-Models test. Uses a
DOM-dispatched click on the antd dropdown option since the popup
animation can render the option outside the viewport.

* test(ui-e2e): verify scoped key works against mock /chat/completions

Extend the "Create a key with a specific proxy model" test to extract
the new key from the success modal and POST to /chat/completions for
the scoped model, asserting 200 and the mock response body. Without
this the test could pass even if the model selection failed to register.
2026-05-20 13:37:17 -07:00
ryan-crabbe-berri
5a00cb1592
test(e2e): migrate runner to uv, add All Proxy Models key test (#28313)
* chore(e2e): migrate runner to uv, add All Proxy Models key test

Switches the local e2e runner (run_e2e.sh) from poetry to uv to match
the rest of the repo and CI. Adds a Playwright test for creating an
admin key with no team selected (all-proxy-models flow), a SLOWMO env
hook for headed debugging, and a MIGRATION_TRACKING.md doc that maps
the manual UI QA checklist to e2e tests so future migration work has
a single source of truth.

* chore(e2e): address greptile feedback

- Remove MIGRATION_TRACKING.md (docs belong in litellm-docs repo)
- playwright.config.ts: fall back to 0 when SLOWMO is non-numeric
  (parseInt returns NaN, which Playwright accepts silently)
- run_e2e.sh: add --frozen to uv sync for CI determinism
2026-05-20 12:35:29 -07:00
shivam
a7a3f0a19d
Address health pagination review feedback
Made-with: Cursor
2026-04-29 16:49:25 -07:00
shivam
0b9d06a509
Fix model e2e result count locator
Made-with: Cursor
2026-04-29 16:25:57 -07:00
Yuneng Jiang
9b74ff3ef7
remove unnecessary cleanup helper
The database is freshly seeded on every test run via seed.sql,
so per-test cleanup is not needed.
2026-04-13 17:29:49 -07:00
Yuneng Jiang
cce7163348
fix CI: replace data-testid selectors with text/role-based selectors
The data-testid attributes added to React components are not present
in the CI-built UI output. Switch to using getByRole and getByText
selectors which work with the rendered DOM regardless of build cache.
2026-04-13 17:29:49 -07:00
Yuneng Jiang
5e07c1cbc9
address greptile review feedback (greploop iteration 1)
Add cleanup helper to delete models created during tests, preventing
stale data accumulation across repeated test runs.
2026-04-13 17:29:49 -07:00
Yuneng Jiang
4f364a8138
[Test] UI - Models: Add E2E tests for Add Model flow
Add E2E tests covering:
- Test connection with bad credentials shows failure modal
- Adding a specific model and verifying it appears in All Models table
- Adding a wildcard route and verifying it appears in All Models table
- Verifying model dropdown shows provider-specific models (existing test updated)

Added data-testid attributes to UI components to support stable test selectors.

Tests verified passing 3/3 consecutive runs with zero flakiness.
2026-04-13 17:29:49 -07:00
Ryan Crabbe
152d6898ab
test(e2e): drop cleanup from edit team model test
Reviewer flagged that cleanup failures were silently swallowed and
suggested asserting `delete.ok()`. While thinking through the fix, the
actual question turned out to be "does the cleanup matter at all?" —
and the answer is no.

The e2e runner (`run_e2e.sh`) spins up a fresh postgres container per
invocation and tears it down at the end, so every local and CI run
starts with an empty DB. Playwright retries share the same DB but each
attempt creates a new model with a unique `Date.now()` name and only
queries its own model, so orphans from failed attempts never collide
with later attempts or other tests. Nothing else in the suite reads
the all-models table.

Keeping the cleanup would also turn every write test into an implicit
delete test, coupling responsibilities and inflating runtime — which
is probably why `teams.spec.ts` (create a team), `keys.spec.ts`
(update key limits), etc. all leave their entities in place. Matching
that convention, drop the try/finally block and the `createdModelId`
tracking. 12 lines removed, no behavior change.
2026-04-13 17:05:42 -07:00
Ryan Crabbe
44614c43c6
test(e2e): add edit team model TPM/RPM limits test
Covers the full write-path flow for team-scoped models on the Models +
Endpoints page: create via /model/new, click the row to open the detail
view, click Edit Settings, change TPM/RPM, click Save Changes, assert
the new values render back. Cleans up via /model/delete in finally so
reruns stay deterministic.

Requires store_model_in_db: true in the fixture general_settings so the
proxy accepts /model/new and /model/delete. No existing test in the
dashboard e2e suite reads the all-models table or hits the model CRUD
endpoints, so enabling the flag has no cross-test impact.
2026-04-13 16:27:39 -07:00
Yuneng Jiang
2e0af3795a
fix(e2e): broaden Copy Key button regex to match both modal versions
Some checks failed
Unit Tests: Proxy DB Operations / proxy-db (auth-checks, tests/proxy_unit_tests/test_auth_checks.py tests/proxy_unit_tests/test_user_api_key_auth.py, 20, 8) (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-db (key-generation, tests/proxy_unit_tests/test_key_generate_prisma.py, 30, 0) (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-db (remaining, tests/proxy_unit_tests --ignore=tests/proxy_unit_tests/test_key_generate_prisma.py --ignore=tests/proxy_unit_tests/test_auth_checks.py --ignore=tests/proxy_unit_tests/test_user_api_key_auth.py, 20, 8) (push) Has been cancelled
Unit Tests: Security / security (push) Has been cancelled
On case-sensitive Linux CI, the old regenerate_key_modal.tsx from main
can coexist with the new RegenerateKeyModal.tsx after merge. The old
modal renders "Copy Virtual Key" while the new one renders "Copy Key".
Use /Copy.*Key/ to match both.
2026-04-09 23:36:35 -07:00
Yuneng Jiang
89320a955c
fix(e2e): remove flaky banner check and increase regenerate key timeout 2026-04-09 23:21:01 -07:00
Yuneng Jiang
839d9bd5f3
refactor(ui): polish regenerate key success view
- Label the key block with a small "Virtual Key" caption so the gray
  box is clearly the key container.
- Move the Copy Key action to the modal footer as a primary button
  with icon; inline copy icon next to the key is removed.
- Swap the button to "Copied" with a check icon on success instead of
  firing a notification — less noisy and keeps feedback in place.
- Disable clicking outside the modal to close (maskClosable=false) so
  users must explicitly dismiss via Close or X.
- Enlarge the key text and let its container span the full modal
  width.
- Tests updated accordingly, including a new test for the copied-state
  swap and the "Virtual Key" label.
2026-04-09 20:14:01 -07:00