litellm/ui/litellm-dashboard
tin-berri 5f2986a1f3
feat(complexity_router): calibrate the classifier rubric with worked examples, selectable per router (#36578)
* feat(complexity_router): calibrate the classifier rubric with worked examples

The built-in rubric stated its tier boundaries as prose alone, and prose
calibrated to consumer chat puts "non-trivial code, multi-step technical work"
at the top of the scale. That is the median request in developer and agent
traffic, so ordinary engineering read as top-tier and the router paid for the
most expensive model on it.

Adds calibration examples to the rubric, selected by a new
classifier_llm_config.rubric preset. The agentic preset (now the default)
anchors routine installs, builds, multi-file edits, and standard debugging at
MEDIUM; the chat preset omits those anchors for deployments serving only
conversational traffic. Both share the same tier criteria, the trust-boundary
paragraph, and the context-window closing line, so this moves where the
boundary sits without changing the taxonomy.

Both presets render byte-identical to the strings a prompt sweep scored, and a
test pins that, so the measured accuracy describes what a router sends.

* feat(ui): pick the classifier rubric preset on an auto-router

Adds a Rubric dropdown to the auto-router's classification panel, so the
agentic and chat presets are selectable rather than config-file only. The
prompt editor prefills from the selected preset, since prefilling agentic text
for a router on chat would show examples its classifier never receives.

The picker is disabled while a custom prompt is set, and the payload builder
drops the preset in that case: a custom prompt is the classifier's whole system
role, so the backend rejects the two together. The builder records the default
preset explicitly, so a later change to which preset is default cannot silently
move an existing router.

* fix(complexity_router): mark an unchosen rubric preset with None, not model_fields_set

The mutual-exclusion check read model_fields_set to tell an explicit preset
from the default. That flag does not survive serialization, and this config is
dumped and handed straight back to ComplexityRouter by /auto_router/test_routing,
where a dump re-states every field. So a custom-prompt classifier saved fine and
then failed validation on preview, rejecting on the second pass what it accepted
on the first.

The preset is now optional, with None meaning the default, matching how None
already means the built-in rubric for system_prompt on the same model. The
default lives in one place, DEFAULT_RUBRIC_PRESET, resolved where the prompt is
assembled. The dashboard stops sending a copy of the default it displays, so a
router nobody configured follows the default rather than pinning today's value,
and UI-built routers behave the same as hand-written config.

Regenerates schema.d.ts, which was left stale by an earlier description edit.

* feat(complexity_router): grandfather existing routers onto the uncalibrated rubric

An unset preset now means LEGACY, the rubric exactly as it shipped before
calibration examples existed, so upgrading cannot move the tier decisions or the
bill of a router that is already running. Config-file routers get this for free
since they name no preset, and a stored config that never had one reads the same
way.

New routers still get the calibrated rubric: switching a classifier to LLM
stamps the agentic preset, because a classifier being configured for the first
time has no prior tier behaviour to preserve. The picker offers legacy so an
existing router's state is representable and opening the form cannot silently
upgrade it.

Each preset is pinned byte-identical to the text the prompt sweep scored,
legacy included, which is what proves an existing router's prompt did not move.

Also collapses the preset data from a NamedTuple with group wrappers and
per-preset frozensets into plain text blocks in a MappingProxyType, matching how
the tier criteria next to it are already stored: 21 lines of prompt text no
longer cost 190 lines of constructors. Tiers are format placeholders so
tier_labels still reach the examples.

* refactor(complexity_router): name the field classification_rubric

`rubric` alone did not say what it selects, and the field sits beside
`system_prompt`, which genuinely is the whole classification prompt. The name
now says which of the two an operator is reaching for: the rubric the built-in
prompt is assembled from, not the prompt itself.

Renames the config field, the query param, the enum, and the dashboard label to
match, and moves the preset text to classification_rubrics.py.

* test(ui): set the preset the mutual-exclusion case is meant to drop

The rename left classification_classification_rubric in the custom-prompt case,
so its input never carried a preset and the assertion held for the wrong reason:
it proved an absent preset stays absent, not that a set one is dropped. A
normalizer that forwards the preset whenever one is set passed with the typo and
fails without it.

tsc reports the typo as TS2353; the earlier sweep grepped for the source file
and not the test, so it went unseen.

* test(ui): scope the role-gate assertions to each page's own endpoint

The memory, workflows, and guardrails-monitor page tests asserted that a denied
role fires no request at all. Their names, and the assertion on the very next
line, say the intent is narrower: the page must not fetch its own data.

Resolving whether a caller is an org admin goes through /organization/list for
every role, since deciding org-admin-for-any-org needs the list, and the route
scopes rows per caller. That legitimate request fails a blanket no-fetch
assertion, so all three files went red on staging for a reason unrelated to
what they test.

Drops the blanket assertion and keeps the scoped one. Bypassing the gate in
memory/page.tsx still fails five tests, so the narrower assertion continues to
catch a genuinely broken gate.

* fix(complexity_router): document that an unset rubric keeps the legacy prompt

The field said 'Leave unset for agentic' while an omitted rubric resolves to
LEGACY, so the OpenAPI schema an operator reads promised calibrated routing
where they got the uncalibrated one.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-13 12:22:20 -07:00
..
public refactor(ui): migrate MCP, callback, guardrail, SSO, and search tool logos to the shared Logo component (#34169) 2026-07-21 22:22:54 +00:00
scripts chore(ui): add filename, size, JSX-handler, prefer-const, and antd lint rules (#34341) 2026-07-22 19:34:35 -07:00
src feat(complexity_router): calibrate the classifier rubric with worked examples, selectable per router (#36578) 2026-08-13 12:22:20 -07:00
tests refactor(ui): replace hand-rolled query-param routing with nuqs (#35871) 2026-08-05 13:29:58 -07:00
.env.development hidden dashboard routing, dev and build env files 2025-10-06 19:21:34 -07:00
.env.production remove next env 2026-04-18 16:45:32 -07:00
.npmrc chore(build): move the Admin UI toolchain to Node 24 (#35801) 2026-08-04 12:36:07 -07:00
.nvmrc chore(build): move the Admin UI toolchain to Node 24 (#35801) 2026-08-04 12:36:07 -07:00
.prettierignore feat(ui): generate dashboard API types from the proxy OpenAPI spec (#29816) 2026-06-05 17:20:01 -07:00
.prettierrc added and ran prettier autoformatter 2025-10-04 18:19:48 -07:00
build_release_ui.sh build release ui script 2026-01-17 18:16:14 -08:00
build_ui.sh chore(build): move the Admin UI toolchain to Node 24 (#35801) 2026-08-04 12:36:07 -07:00
build_ui_custom_path.sh chore(build): move the Admin UI toolchain to Node 24 (#35801) 2026-08-04 12:36:07 -07:00
CLAUDE.md test(ui): tier the MCP create tests into unit and integration 2026-08-03 16:22:54 -07:00
components.json refactor(ui): switch shadcn primitives from Radix to Base UI (#32124) 2026-07-07 09:55:41 -07:00
eslint-budgets.json chore: make no-console max 12 2026-08-05 10:45:56 -07:00
eslint-suppressions.json refactor(ui): migrate guardrails monitor table to shared DataTable (#36709) 2026-08-13 09:20:48 -07:00
eslint.config.mjs chore(ui): add filename, size, JSX-handler, prefer-const, and antd lint rules (#34341) 2026-07-22 19:34:35 -07:00
knip.json fix(ui): register type-test files as knip entry points 2026-08-10 15:42:09 -07:00
next.config.mjs chore(ui): preserve console.warn in prod builds to match lint allow-list 2026-07-03 14:51:14 -07:00
package-lock.json build(deps): bump nanoid to 3.3.18 to clear osv-scan 2026-08-13 16:38:24 +00:00
package.json refactor(ui): make illegal DataTable prop combinations unrepresentable 2026-08-10 15:19:39 -07:00
postcss.config.js feat(ui): shadcn migration foundation: Tailwind v4, shadcn init, antd cascade fix (#31995) 2026-07-02 19:02:27 -07:00
README.md
tsconfig.json test(e2e): move Admin UI Playwright suite to tests/e2e/ui (#34196) 2026-07-22 19:43:10 +00:00
tsconfig.tsbuildinfo fix: net prompt-caching savings against the cache-write premium (#36452) 2026-08-10 18:52:03 -07:00
vitest.config.ts refactor(ui): make illegal DataTable prop combinations unrepresentable 2026-08-10 15:19:39 -07:00

This is a Next.js project bootstrapped with create-next-app.

Getting Started

First, run the development server:

npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev

Open http://localhost:3000 with your browser to see the result.

You can start editing the page by modifying app/page.tsx. The page auto-updates as you edit the file.

This project uses next/font to automatically optimize and load Inter, a custom Google Font.

Learn More

To learn more about Next.js, take a look at the following resources:

You can check out the Next.js GitHub repository - your feedback and contributions are welcome!

Deploy on Vercel

The easiest way to deploy your Next.js app is to use the Vercel Platform from the creators of Next.js.

Check out our Next.js deployment documentation for more details.