Commit graph

4874 commits

Author SHA1 Message Date
Yuneng Jiang
daa15ba074
chore(ui): remove unmounted UsageIndicator component 2026-07-15 17:09:24 -07:00
Tin Chi Lo
031922eec6 feat(mcp): discover-by-default issuer (trust-on-first-use) + stale-clear on url/auth_type change + UI
Discover the issuer from the upstream and persist it trust-on-first-use (fill-empty-only,
frozen thereafter), so admins do not have to type it; an admin-configured issuer always wins
and is never overwritten. Re-pointing the server url now clears the discovered issuer and
endpoints (matching the existing auth_type-change clearing) so a new upstream re-discovers
instead of anchoring on the previous upstream's issuer. Adds the issuer to the per-user OAuth
token identity so re-pointing it purges stale tokens. Surfaces the issuer as an optional,
auto-discovered, overridable field in the create and edit MCP server forms.

C901 gate shows +1 vs staging; that is inherited from the #33317 stack base (delta 0 against
2026-07-15 16:59:01 -07:00
Yuneng Jiang
056f85a71e
feat(ui): show exact license expiration date in usage cards 2026-07-15 16:52:21 -07:00
devin-ai-integration[bot]
ff4a40f017
feat(ui): add reusable BetaBadge and use it for Projects sidebar item (#33449)
* fix(ui): remove New badge from Projects sidebar item

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

* feat(ui): add reusable BetaBadge and use it for Projects sidebar item

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

* feat(ui): rename disableShowNewBadge flag to disableShowBadges and make BetaBadge respect it

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

* style(ui): use blue for BetaBadge to match existing New badge

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

* revert(ui): keep disableShowNewBadge localStorage key to preserve existing user opt-outs

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

* chore: remove non-functional md artifacts from PR

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

---------

Co-authored-by: ryan <ryan@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-15 16:04:29 -07:00
devin-ai-integration[bot]
b907378f02
feat(guardrails): forward optional metadata on POST /guardrails/apply_guardrail (#33067)
Clients calling the standalone apply_guardrail endpoint had no way to pass
per-request configuration to custom guardrail implementations. This adds an
optional metadata field to ApplyGuardrailRequest and forwards it to
CustomGuardrail.apply_guardrail via request_data, only when the client sends
it. The messages guard is aligned to the same is-not-None semantics so an
explicitly-sent empty list is forwarded instead of silently dropped.

The Admin UI's Guardrail Test Playground gains an optional Metadata JSON
input (validated client-side) wired through applyGuardrail in networking.tsx,
so parameterized guardrails can be exercised from the dashboard.

Tests cover metadata alone, metadata with messages, explicit empty values,
the omitted-field passthrough, and the UI panel's parse/error behavior

Co-authored-by: Yassin Kortam <yassin@berri.ai>
2026-07-16 01:36:17 +03:00
Yuneng Jiang
719e1bfec0
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/chat-ui-first-message-bug-4ca43c 2026-07-15 13:48:38 -07:00
Yuneng Jiang
2b9b681e12
fix(ui/chat): resolve chat routes at render time so navigation works under server_root_path
CHAT_ROUTES was built once at module load via migratedHref, capturing an
empty server root path before the UI-config bootstrap sets it. Under
SERVER_ROOT_PATH every chat route came out unprefixed, so router.push
hard-navigated to a 404; during a send that page unload also aborted the
streaming request, so the first message of a new conversation never
rendered and only a second send appeared to work. Compute the routes at
render time via getChatRoutes(), update the send URL with a shallow
history.pushState instead of a router navigation, and source the active
conversation id from the hook's local state so it propagates without a
router round-trip
2026-07-15 13:48:27 -07:00
devin-ai-integration[bot]
9c59e2ae55
feat(ui): move Caching from Experimental into Developer Tools (#33432)
Co-authored-by: ryan <ryan@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-15 12:47:11 -07:00
tin-berri
1bb69a17fc
Merge pull request #33346 from BerriAI/litellm_mcp_token_storage_ttl_cap
fix(mcp): cap per-user OAuth token cache TTL at the token's own lifetime
2026-07-15 12:32:56 -07:00
Yassin Kortam
a643dd0820
feat(proxy): push-based OTLP billable-request metering for enterprise deployments (#31592)
* feat(proxy): push-based OTLP billable-request metering for enterprise deployments

Adds opt-in, license-gated metering that counts 2xx HTTP requests to LLM
inference, MCP, and A2A endpoints and exports them over mutual TLS to a global
OpenTelemetry Collector for request-based billing.

A pure ASGI middleware (BillableRequestMetricsMiddleware) classifies each
request by route and records one count per 2xx response via an injected
recorder. The recorder (BillingMetricsRecorder) owns a dedicated OTEL meter
provider and an OTLP/gRPC exporter authenticated with client certificates, kept
isolated from the global meter provider so a customer's own OTEL metrics are
untouched. The recorder is built only when a valid LITELLM_LICENSE is present
and the cert material is configured; otherwise the middleware is a transparent
pass-through.

Deployment identity rides on the mTLS client certificate rather than the
payload, so the secret license key is never sent as an attribute or header; only
the license org id travels as a resource attribute for cross-checking.

Resolves LIT-4089

* fix(proxy): align billable-request metering with the global collector

- switch the exporter to OTLP/HTTP with a TLS client certificate. The
  collector front end terminates mutual TLS and validates the client cert
  against our CA; server verification uses the system trust store, so the
  CA env var is now an optional override for private collectors
- resolve the metrics recorder on the first request via a factory instead
  of at import time, so deployments that provide the license and cert env
  vars through the YAML config's environment_variables export correctly
- close the metering bypass: classify /images/edits, /images/variations,
  /v1/messages, /v1/videos, video remix, /v1/ocr and Gemini generateContent
  as billable, and gate LLM routes to POST so GET reads (list videos, fetch
  a response) do not bill. Verified live: the collector count matches the
  UI usage page successful_requests exactly, with failures excluded on both
  sides

* fix(proxy): wrap enterprise billing import in try-except per code-quality gate

The check_unsafe_enterprise_import gate requires every import from an
enterprise-pathed module to be guarded. Annotate the factory with the
middleware's BillingRecorder protocol so no enterprise type import is
needed at type-check time

* chore: satisfy strict lint gates in billing modules

- builtin generics per UP006 (dict/tuple instead of typing.Dict/Tuple)
- noqa the deliberate blind catch that keeps metering from breaking startup
- sort proxy_server import blocks split by the guarded enterprise import

* fix(proxy): bill provider passthrough, search, and rag routes

Route-inventory audit against LiteLLMRoutes.llm_api_routes found more
SpendLogs-producing surfaces the classifier missed: provider passthrough
(/bedrock, /vertex-ai, /cohere and the rest of mapped_pass_through_routes),
/v1/search and vector-store search, and the rag ingest/query routes. All are
counted by the dashboard usage page, so missing them undercounts billing.

The passthrough prefix list is read from LiteLLMRoutes so new providers are
picked up without touching this module. /langfuse is excluded: it forwards
observability traffic and writes no SpendLogs row. Known limitation recorded
in the PR: /v1/realtime is a websocket flow the HTTP middleware does not see

* fix(proxy): bill MCP and A2A requests by protocol transport routes only

The billable-request classifier matched the whole /v1/mcp prefix, so
management and discovery reads such as GET /v1/mcp/tools and GET
/v1/mcp/server counted as billable MCP requests, while real MCP tool
calls on the /{server}/mcp and /toolset/{name}/mcp aliases were missed
because their route handlers rewrite the ASGI scope only after this
middleware has already classified the original path. Classify MCP by the
concrete transport surface (the /mcp streamable-HTTP and SSE sub-app plus
the single-segment server and toolset aliases) and exclude the /v1/mcp
management API. Apply the same shape to A2A, which had the identical
issue: only the /message/send invoke route bills, not /v1/a2a/discover or
the .well-known agent-card reads.

* fix(proxy): harden billable-request classification and recorder lifecycle

Exact-match Anthropic /v1/messages so OpenAI Assistants thread-message
routes no longer bill, add Google Interactions create routes, guard
recorder.record() so a broken exporter can never fail a served request,
lock lazy recorder resolution against concurrent first requests, and
disable metering on empty-string env config instead of accepting a
blank endpoint

* chore(ui): regenerate eslint metrics after staging merge

* docs(proxy): state the lower-bound billing contract in middleware comments

* fix(proxy): bill mcp-rest tool calls and bare a2a agent invokes

POST /mcp-rest/tools/call executes a tool and fires the same MCP spend
logging as the /mcp transport, and POST /a2a/{agent_id} is the JSON-RPC
invoke route whose method (message/send or message/stream) travels in
the body; both returned 2xx without being recorded

* fix(proxy): flush billable-request counts on proxy shutdown

PeriodicExportingMetricReader buffers up to one export interval of
counts; without a final flush every restart silently dropped them. The
factory registers the recorder it builds and proxy_shutdown_event pops
and flushes it, bounded by a 5s timeout so a dead collector cannot
stall shutdown

* fix(proxy): stop billing bare a2a task RPCs and close the shutdown race

POST /a2a/{agent_id} multiplexes JSON-RPC methods off the request body. Only
message/send and message/stream write a SpendLogs row; tasks/get, tasks/cancel
and the pushNotificationConfig RPCs are forwarded upstream and write none.
Classifying the bare path as billable counted those task RPCs and pushed the
metric above the dashboard's successful-request count. Since a path-only
classifier cannot read the body, the bare route no longer bills; the explicit
/message/send routes still do. Missing a bare-path invoke undercounts, which is
the only direction this metric is allowed to drift. The /mcp transport keeps
billing every method because its list path logs a SpendLogs row too.

The billing middleware also sat outside InFlightRequestsMiddleware, and it
records after the inner app returns. A request could therefore be counted as
drained while its record() had not yet run, letting proxy_shutdown_event flush
and stop the exporter underneath it. Registering it before the in-flight
tracker nests it inside, so wait_for_drain covers the record

* test(proxy): stub the OTLP exporter in the recorder-build test

test_premium_with_full_config_builds_recorder built a real MeterProvider, so
the shutdown flush resolved collector.example and opened a TLS connection from
a unit test. The exporter is now stubbed, and a getaddrinfo spy asserts nothing
resolves the collector host so the stub cannot be quietly dropped later

* fix(helm): truncate the helm.sh/chart label to 63 bytes

Kubernetes caps a label value at 63 bytes and .Chart.Version is unbounded. CI
publishes branch builds as 0.0.0-branch-<branch>-<sha>, so helm.sh/chart
rendered as a 64 byte value and the API server rejected every labeled resource
with "must be no more than 63 bytes", including the migrations Job. The
litellm-helm chart already guards this through a litellm.chart helper; this
adds the same helper here.

Swept the rest of the chart for label and name values built from unbounded
input. .Chart.Version appeared only in this label. The remaining candidates all
derive from .Release.Name, which helm itself caps at 53 characters, so they
cannot overflow; three of them are selector labels feeding immutable Deployment
matchLabels, where adding trunc would risk churn for no gain. They are left
alone deliberately.

Verified with a new helm-unittest suite, tests/chart_label_tests.yaml, which
overrides chart.version per test:

  helm unittest -f 'tests/*.yaml' helm/litellm      # 13 passed
  helm unittest -f 'tests/*.yaml' helm/litellm-helm # 54 passed

The truncation cases fail against the previous helper. Reproduced the original
overflow by rendering with the real branch version and measuring the label:

  helm template rel helm/litellm -f helm/litellm/tests/values/required.yaml \
    | grep helm.sh/chart   # 64 bytes before, 63 after

* feat(proxy): accept inline PEM for the billing-metrics mTLS credentials

LITELLM_BILLING_METRICS_CLIENT_CERT, _CLIENT_KEY and _CA_CERT took a filesystem
path. ECS injects Secrets Manager values as environment content and cannot mount
them as files, so a licensed deployment there could not turn metering on.

Each variable now takes either a path or the PEM itself. Inline PEM, detected by
the "-----BEGIN" prefix, is written once when the recorder is built into a 0700
temp dir as a 0600 file, and the config points at that path. The OTLP exporter
still only ever sees paths. A write failure disables metering through the
existing failure-as-None path rather than raising, and path-valued variables are
passed through untouched, so nothing changes for deployments that mount files.

The mixed case works too: mount the CA, inject the client credentials

* feat(helm): add first-class billingMetrics values to the componentized chart

Turning enterprise billable-request metering on meant hand-rolling the env vars
and the cert volume through gateway.extraEnv and gateway.volumes. This adds a
top-level billingMetrics block, off by default, consumed only by the gateway
since that is the component serving billable traffic.

When enabled it renders LITELLM_BILLING_METRICS_ENDPOINT plus the two cert paths
and mounts secretName read-only at /etc/litellm/billing-mtls. caSecretName is
optional and only needed for private collectors whose server certificate is not
on the public web PKI; when set it mounts at /etc/litellm/billing-mtls-ca and
adds the CA env var. exportIntervalMs is passed through only when set.

Enabling without secretName or with an empty endpoint fails the render with a
named message rather than producing a gateway that silently never exports.

The generic gateway.volumes, gateway.volumeMounts and gateway.extraEnv paths are
untouched and still compose with this, so existing overlays keep working.

The chart has no values.schema.json and no README, so there is nothing further to
update. Verified with a new helm-unittest suite:

  helm unittest -f 'tests/*.yaml' helm/litellm      # 23 passed
  helm unittest -f 'tests/*.yaml' helm/litellm-helm # 54 passed

* feat(terraform): billing-metrics variables for the aws and gcp templates

* feat(helm): add billingMetrics values to the classic chart

The componentized chart just gained a first-class billingMetrics block; this
mirrors it in litellm-helm so enabling enterprise billable-request metering no
longer means hand-rolling the env vars and the cert volume through envVars and
volumes.

When enabled the proxy Deployment renders LITELLM_BILLING_METRICS_ENDPOINT plus
the two cert paths, and mounts secretName read-only at /etc/litellm/billing-mtls.
secretName defaults to litellm-billing-metrics-mtls, the conventional name, so
enabling the block is enough once that Secret exists. caSecretName is optional
and only needed for private collectors whose server certificate is not on the
public web PKI; when set it mounts at /etc/litellm/billing-mtls-ca and adds the
CA env var. exportIntervalMs is passed through only when set.

The env entries render after envVars and extraEnvVars, so a user-supplied
LITELLM_BILLING_METRICS_ENDPOINT cannot silently redirect the export under
Kubernetes last-wins duplicate-env semantics; this is the same ordering the
migrations Job relies on for DISABLE_SCHEMA_UPDATE.

Enabling with an emptied secretName or endpoint fails the render with a named
message rather than producing a proxy that silently never exports.

The generic volumes, volumeMounts, envVars and extraEnvVars paths are untouched
and still compose with this, so existing overlays keep working. The chart has no
values.schema.json; README parameters and a setup section are updated.

  helm unittest -f 'tests/*.yaml' helm/litellm-helm  # 68 passed (54 + 14 new)
  helm lint helm/litellm-helm                        # 0 failed

* test(helm): pin that the migrations job never mounts the billing cert

The componentized chart's suite asserts the backend Deployment stays clear of the
billing wiring, since only the gateway serves billable traffic. The classic chart
has no backend, but it does have a second pod: the migrations Job, which renders
its own env from envVars and extraEnvVars. Nothing today wires the billing
include into it, and nothing stopped a future edit from doing so.

Asserts absence of the env, and that the Job grows no volumes or volumeMounts at
all. Both are notExists rather than notContains because the Job renders neither
key by default, so a notContains would fail on an unknown path instead of
checking the absence it looks like it is checking.

* fix(helm): meter the backend too, it serves the MCP transport

Scoping billingMetrics to the gateway was wrong. Applying each component's own
route allowlist to the proxy app shows the split is 75 billable routes on the
gateway and one on the backend: /{mcp_server_name}/mcp, the named-server MCP
transport, which writes a SpendLogs row on success. Metering only the gateway
would have silently dropped every MCP transport call from the counter, an
undercount proportional to a customer's MCP traffic.

The backend deployment now renders the same env and mounts the same read-only
cert secret. The migrations job still gets neither; it runs prisma and serves no
traffic, and a test pins that.

  helm unittest -f 'tests/*.yaml' helm/litellm      # 25 passed
  helm unittest -f 'tests/*.yaml' helm/litellm-helm # 69 passed

This also aligns the chart with the terraform templates, which inject the
credentials into both components.

* fix(proxy): never log billing credential values when they fail to resolve

Accepting inline PEM turned the cert env vars into secret-bearing values, but
the disable warning still echoed them. A value that is neither a readable path
nor `-----BEGIN`-prefixed PEM, for example a key with a preamble or a malformed
secret, fell through to the path branch and was written to the proxy logs
verbatim, exposing the client certificate or private key to anyone who can read
them.

The warning now names the offending environment variables and tells the operator
what a valid value looks like, without ever printing one

* Revert "fix(helm): truncate the helm.sh/chart label to 63 bytes"

This reverts commit 4f7f706a63.

Version hygiene belongs to the pipeline that mints chart versions, not to the
chart. The build workflow now caps the version slug so litellm-<version> fits
the 63 byte label budget, which removes the overflow at the source rather than
silently truncating a value operators use to identify the build.

Drops the litellm.chart helper, restores the direct helm.sh/chart printf, and
removes tests/chart_label_tests.yaml. Both chart suites stay green:

  helm unittest -f 'tests/*.yaml' helm/litellm      # 20 passed
  helm unittest -f 'tests/*.yaml' helm/litellm-helm # 69 passed

* feat(helm): default billingMetrics.secretName to the conventional name

The componentized chart required an explicit secretName while the classic chart
defaults to litellm-billing-metrics-mtls. Both now default to it, so the common
path is to create that Secret with tls.crt and tls.key and set enabled: true.

The required() guard stays, and with a default it now only fires when someone
explicitly blanks the override, which the tests pin from both sides

* feat(proxy): log once when billing metrics are actually enabled

build_billing_metrics_recorder returned None silently when the deployment was
not licensed, while every other disable path logged a warning. An operator
reading logs could not tell "metering active" from "metering off because this
component never saw the license", and a component can carry the cert mount and
the billing env and still meter nothing. That is the undercount direction the
metric is not allowed to drift in.

A successful build now emits one info line naming the collector endpoint and the
export interval; neither the certificate contents nor the license appear. The
unlicensed path logs at debug rather than warning, because unlicensed is the
common case and a warning there would be noise on every OSS proxy

* fix(terraform): fail the plan on a partial billing-metrics config

Each PEM secret is created only when its own variable is non-empty, so setting
billing_metrics_endpoint with a certificate but no key applied cleanly and left
the proxy logging "missing config" and never exporting. Silent non-export is the
undercount direction this metric must not drift in, and every other surface
fails fast on a half-configured metering block.

Both templates now carry a lifecycle precondition requiring the client
certificate and its key together whenever the endpoint is set. It lives on the
gateway task definition (aws) and the gateway Cloud Run service (gcp) rather
than on the secret resources, because those are themselves count-gated on the
PEM being present and would never evaluate in the failing case. Cross-variable
`validation` blocks would need terraform 1.9; versions.tf pins >= 1.6, and
preconditions work there.

ca_cert_pem stays optional, so an empty value still falls back to the system
trust store.

  endpoint  cert  key           result
  ""        any   any           metering off, no secrets created
  set       set   set           metering on
  set       missing either      plan fails

Verified each row with `terraform console` against the condition, and reran
`terraform fmt -check` and `terraform validate` in both directories

* docs(terraform): record why the billing guard sits on the gateway resource

The precondition cannot live on the cert secret, which is count-gated on
the cert itself and so has zero instances in exactly the case the guard
must catch. That makes the guard's correctness depend on this resource
staying unconditional, which nothing else records and no test enforces

* fix(terraform): guard the backend against a partial billing config too

The precondition only sat on the gateway, but the backend receives the billing
endpoint as well, because it serves the named-server MCP transport and meters
it. A targeted apply of just the backend task or service would therefore skip
the guard entirely and provision a component holding a billing endpoint with no
credentials to use it, which is the silent never-export failure the guard exists
to prevent.

Both templates now carry the same precondition on the backend resource. The
condition and truth table are unchanged; ca_cert_pem stays optional.

  terraform fmt -check and terraform validate clean in both directories

* docs(team): document mcp_rpm_limit in update_team docstring

* chore(ui): regenerate schema.d.ts for update_team docstring change
2026-07-15 12:12:52 -07:00
Tin Chi Lo
2f15b5fb1d fix(mcp): cap per-user OAuth token cache TTL at the token's own lifetime
token_storage_ttl_seconds previously won outright over the token's expires_in, so a TTL longer than the token's lifetime kept the Redis fast path serving an expired bearer until eviction, while the stored refresh_token sat unused because refresh only runs on the DB read-through

The configured TTL is now capped at expires_in minus the expiry buffer. Shorter TTLs and servers without the field behave exactly as before, and the TTL still applies verbatim when the upstream reports no expires_in. The dashboard tooltips on the create and edit forms are updated to describe the capped behavior
2026-07-14 20:52:39 -07:00
Yuneng Jiang
46939ea7a7
refactor(ui): drop unused accessToken prop from PluginTable 2026-07-14 20:00:28 -07:00
Yuneng Jiang
e6c776d508
refactor(ui): drop whole-row navigation on the skills table
Only the name cell and the overflow menu act on a row, matching the unified
table pattern; the previous table navigated on any row click
2026-07-14 19:52:43 -07:00
Yuneng Jiang
56655218d0
refactor(ui): migrate vector stores, prompts, and skills tables onto shared DataTable
Rewrites the three Batch A tables from hand-rolled TanStack + tremor renderers
into thin DataTable consumers with a separate ColumnDef module each, matching
the Guardrails and Tags migrations. Row actions move into a per-row overflow
menu (edit/copy/delete for vector stores; copy for everyone plus admin-gated
delete for prompts and skills). The vector stores parent gains an isLoading
flag resolved on every exit path so the table shows the shared skeleton
instead of flashing the empty state. Table files are renamed to PascalCase
and stale eslint bulk-suppressions for the rewritten files are pruned.
2026-07-14 19:49:09 -07:00
mateo-berri
887fc0a73c Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_google_interactions_cost 2026-07-14 18:45:01 -07:00
Yuneng Jiang
6f19c7b2b3
fix(ui): resolve tags loading state when accessToken is null 2026-07-14 18:32:30 -07:00
Yuneng Jiang
1e4c27dd62
test(ui): exercise click suppression on disabled tag actions 2026-07-14 18:23:46 -07:00
Yuneng Jiang
da06429524
refactor(ui): truncate tag name/description and mute disabled tag names 2026-07-14 18:20:22 -07:00
mateo-berri
f1a5054a16 fix(interactions): bill only interaction creation, not GET polls 2026-07-14 18:11:20 -07:00
Yuneng Jiang
07deb66218
refactor(ui): migrate tags table onto shared DataTable 2026-07-14 18:05:39 -07:00
yuneng-jiang
f8c49f51cc
refactor(ui): migrate guardrails table onto shared DataTable (#33303)
* feat(ui): migrate guardrails table onto shared DataTable

Move the guardrails list onto the shared DataTable + cell library as the
proof-of-concept for the simple-tables design migration, following the Teams
reference pattern.

Split the table into a thin container (guardrail_table.tsx) and column defs
(guardrailTableColumns.tsx): client-side sort defaulting to created_at desc, a
search + refresh toolbar, IdCell / DateCell / StatusBadge cells, real provider
logos, a rich empty state, and skeleton loading rows. Row actions move into a
per-row overflow menu; deletion stays disabled for config-file guardrails, now
surfaced as a disabled menu item instead of a greyed trash icon. Detail view
and the delete modal remain owned by GuardrailsPanel.

Restyle the "Add New Guardrail" control to the shared Button + dropdown menu.

Update the regression tests for the menu-based actions and drop the now-stale
eslint suppression entry that the rewrite eliminated.

* fix(ui): match guardrails table to the design

Address design-review feedback on the guardrails migration:

- Drop the search + refresh toolbar. The original table had neither and the
  SimpleTable design has no toolbar; the container now just renders the sorted
  table and its empty state.
- Give the Guardrail ID cell the design's hover affordance by rendering it with
  the shared IdentityCell (monospace, chevron on hover) instead of the blue
  IdCell pill.
- Stop pinning the actions column. Pinning added a sticky divider that the
  design and the Teams table don't have; it is now a plain right-aligned menu
  column, matching Teams.

* fix(ui): match loading skeleton row height to loaded rows

The compact skeleton row did not carry the h-8 height that real compact
rows get, so loading rows rendered shorter than loaded ones and the table
height jumped when data arrived. Mirror the same size-based height on the
skeleton row in the shared DataTable so every compact table loads at a
stable height

* test(ui): drop stale onGuardrailUpdated from guardrails table baseProps

The prop was removed from GuardrailTableProps when the toolbar went away;
the test baseProps still listed it. Harmless at the call site since it is
spread rather than an object literal, but dead and worth removing

* fix(ui): remove dead edit_guardrail_form after guardrails migration

The guardrails table migration dropped the last import of EditGuardrailForm,
which knip flags as an unused file. The form was already unreachable before
the migration: the table wired a delete button only, and nothing ever called
handleEditClick to open the modal, so the import was the sole thing keeping
the file referenced. Delete it and prune its now-stale eslint suppression
entry. Guardrail editing is unchanged and lives in the detail view
(GuardrailInfoView)
2026-07-14 17:35:27 -07:00
mateo-berri
598fa9d64d feat(pricing): add gemini-omni-flash-preview with video output token pricing 2026-07-14 14:04:51 -07:00
devin-ai-integration[bot]
ffe0c4c185
fix(proxy)!: enforce user budget on team keys (read-time + reservation) with UI opt-out (#32005)
* fix: enforce user budget on team keys

User budget was skipped when the key belonged to a team, letting
users exceed their personal budget by going through a team key.

Remove the team_object guard in _user_max_budget_check so user
budgets are always enforced. Add skip_user_budget_on_team_key
general_settings flag to opt back into the old behavior.

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

* fix: update test to expect user budget enforcement on team keys

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

* fix(proxy): enforce user budget on team keys in reservation path and expose skip flag in UI

Extends the read-time fix so the optimistic budget reservation also reserves the user spend counter for team-scoped keys, register skip_user_budget_on_team_key in ConfigGeneralSettings so /config/field/update accepts it, and surface it as a Boolean toggle on the Admin UI General Settings table via allowed_args in /config/list.

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

* test: assert budget_exceeded ProxyException in personal budget test

Tighten the broad pytest.raises(Exception) so the test only passes when
the auth flow rejects with a budget_exceeded ProxyException, and switch
the new ConfigGeneralSettings field to Optional[bool] to match the
surrounding annotation style

* fix: revert to bool | None to stay under UP045 strict budget

---------

Co-authored-by: Krrish Dholakia <krrishdholakia@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: ryan-crabbe-berri <ryan@berri.ai>
2026-07-14 13:21:32 -07:00
yuneng-jiang
2166608eb8
feat(ui): left-anchor the Create Key and Create Team CTAs (#33248)
Move the Create New Key and Create Team buttons out of the page header's
right-side action slot. On Teams the button now sits in the tab bar's left
slot, separated from the three tabs by a vertical rule, so the CTA and tabs
read as one left-anchored cluster. On Keys, which has no tabs, the button
anchors left on its own row beneath the title.
2026-07-14 11:39:36 -07:00
Krrish Dholakia
b200d664ee
feat(ui): add adaptive routing settings to Auto-Router v2 (#33146) 2026-07-13 21:29:58 -07:00
devin-ai-integration[bot]
001457af8b
fix(keys): persist key_type so the UI shows correct key scope instead of "All Proxy Models" (#33115)
* fix(ui): derive key model scope so SCIM/management/read-only keys stop showing 'All Proxy Models'

key_type is not persisted on a key (the proxy maps it to allowed_routes and
drops it), so the keys tables only inspected the models list and rendered
'All Proxy Models' for any key with an empty models array, including SCIM,
Management and Read-only keys that cannot call a single model.

Add deriveKeyModelScope(allowed_routes) and render 'No model access' with a
scope tooltip for those recognized scopes; unrestricted, AI-API and custom
keys keep the existing model-list rendering.

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

* refactor(ui): move key_scope helper to components root

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

* feat(keys): persist key_type on virtual keys so the UI reads scope directly

Add a nullable key_type column to LiteLLM_VerificationToken (root, proxy,
and proxy-extras schemas plus an additive migration) and stop dropping the
value in handle_key_type, so management/read_only/llm_api/default keys store
their type alongside the derived allowed_routes. Surface it on the key read
and create response models. The dashboard now prefers the persisted key_type
for the no-inference buckets and keeps the allowed_routes derivation as the
fallback for keys created before the column existed (key_type null), so no
backfill is required.

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

* style(keys): use PEP604 X | None for new key_type annotations to satisfy ruff UP045 budget

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

* fix(keys): add key_type column to LiteLLM_DeletedVerificationToken

The deleted-token archive model inherits key_type from the verification
token, so regenerate/delete flows write key_type into
LiteLLM_DeletedVerificationToken. Add the column (all schemas + migration)
so the archive insert does not fail with FieldNotFoundError.

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

* fix(migrations): regenerate key_type migration via runbook (canonical ADD COLUMN)

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

---------

Co-authored-by: ryan <ryan@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-13 18:08:43 -07:00
tin-berri
384bbf7fc2
Merge pull request #33113 from BerriAI/litellm_mcp_oauth_error_relay
fix(mcp): relay upstream OAuth token and DCR rejections instead of a generic 500
2026-07-13 18:06:47 -07:00
yuneng-jiang
e35f5d2b73
feat(ui): rebuild the Teams table on the shared DataTable (#33128)
* feat(ui): rebuild the Teams table on the shared DataTable

The Your Teams tab moves off the Ant Design table onto the shared DataTable that the Virtual Keys page uses, following the new dashboard design. It gains server-side sort, pagination and filtering, a toolbar with a filter drawer and a columns menu, and a per-row actions menu

Sorting is wired only to the columns /v2/team/list can actually order by (team_alias, created_at); Spend / Budget and Updated stay unsorted because the endpoint silently ignores those fields. The design's "Created by" column is dropped since the team object has no such field, and the drawer's "Has keys" filter is dropped for the same reason. The Resources cell shows members, models and keys as colored pills, and the actions menu keeps the existing Edit, Copy team ID and Delete behaviors, with Edit and Delete gated to Admin

The teams grid gets its own unit tests in TeamsPage/TeamsTable.test.tsx. Teams.tsx keeps the create-team modal, delete modal, detail view and tabs, now refreshing the list through React Query invalidation instead of a manual refetch

* fix(ui): match Teams loading skeletons to the rendered row height

The default twoLine and chips skeleton shapes rendered the Team and Resources cells shorter than the loaded row (a real row measures 55px, the old skeleton ~49px), so the loading state looked visibly squat. Give the Team column a custom renderSkeleton that mirrors the two-line IdentityCell (measured 54px) and the Resources column one that mirrors the pills, and mark the hidden Rate Limits column as two-line so it matches when shown

* fix(ui): keep team admins' Members tab by deriving is_team_admin from the selected team

The redesign computed is_team_admin from useTeam(selectedTeamId), but that hook returns teamInfoCall's nested { team_info: { members_with_roles } } shape, so the top-level members_with_roles read was always undefined and is_team_admin was always false. For a non-proxy-admin team admin that hid the Members, Member Permissions and Settings tabs in the team detail view, which broke the team-admin add/remove member e2e tests. Pass the Team object up from the table instead (/v2/team/list returns it with a top-level members_with_roles), matching the pre-redesign behavior; proxy admins were unaffected because is_proxy_admin already granted access

Also point the Delete-a-team e2e at the new kebab: open the row actions menu, then click Delete team, rather than clicking the old inline delete icon
2026-07-13 17:47:14 -07:00
Tin Chi Lo
b70de76df3 Merge origin/litellm_internal_staging (#32980 bridge refresh envelope) into litellm_mcp_oauth_error_relay
The refresh-envelope helpers merge cleanly alongside the faults package. The upstream invalid_grant
special case for bridge refreshes moves inside the post-call except branch (its old location after a
second raise_for_status would be unreachable under the call-time-raise structure this branch
introduced) and now keys off the classified fault; _upstream_oauth_error is dropped since the
classifier already parses the RFC 6749 error field with total accessors
2026-07-13 17:31:17 -07:00
ryan-crabbe-berri
539bc30e04
refactor(ui): migrate callback debounce sites to react-pacer with regression tests (#33043)
* refactor(ui): standardize debounce waits behind shared DEBOUNCE_WAIT_MS constant

* build(ui): bump @tanstack/react-pacer from 0.2.0 to 0.22.1

* refactor(ui): migrate straightforward value debounces to react-pacer

* refactor(ui): migrate callback debounce sites to react-pacer with regression tests

* chore(ui): restore trailing newline in eslint-suppressions.json

* test(ui): mock all pacer debounce hooks in VirtualKeysTable test

* fix(ui): update merged debounce tests for OldTeams to Teams rename
2026-07-13 15:38:34 -07:00
yuneng-jiang
07d2a03dbf
fix(ui): render the sidebar scrollbar with shadcn ScrollArea (#33124)
* fix(ui): render the sidebar scrollbar with shadcn ScrollArea

The sidebar navigation scrolled through a native overflow-y-auto container, so the browser drew its default scrollbar. It now scrolls through the shadcn ScrollArea primitive so the thumb matches the rest of the dashboard

Switching to ScrollArea surfaced a latent styling gap. The Base UI scroll-area, tabs, and separator primitives rely on data-horizontal and data-vertical Tailwind variants that resolve to [data-orientation="horizontal"] and [data-orientation="vertical"], and those variants ship in shadcn's shared stylesheet. The project never imported it, so the classes matched nothing and the scrollbar collapsed to zero width. This adds shadcn as a devDependency and imports shadcn/tailwind.css, which also repairs the vertical tabs and separator styling. See shadcn-ui/ui#9196 for the upstream tracking issue

* refactor(ui): inline the Base UI data-* variants, drop the shadcn dep

The earlier fix imported shadcn/tailwind.css through the shadcn devDependency, which pulled 219 packages and tied the CSS build to shadcn's package exports (an open Turbopack-breaking bug, shadcn-ui/ui#10931). shadcn's model is that we own the components, so the custom variants those components depend on belong in our own stylesheet rather than a runtime dependency. This inlines the nine data-* custom variants and the no-scrollbar utility that the Base UI primitives reference into globals.css, and removes the shadcn package.
2026-07-13 15:35:08 -07:00
ryan-crabbe-berri
20d021eb6a
refactor(ui): migrate straightforward value debounces to react-pacer (#33042)
* refactor(ui): standardize debounce waits behind shared DEBOUNCE_WAIT_MS constant

* build(ui): bump @tanstack/react-pacer from 0.2.0 to 0.22.1

* refactor(ui): migrate straightforward value debounces to react-pacer
2026-07-13 14:45:05 -07:00
yuneng-jiang
da5ed97ff2
fix(ui): drop w-full from page-content wrappers to remove 32px horizontal overflow (#33118)
Several dashboard pages wrap their content in a div styled w-full mx-4, so the
element's width is 100% of the scrollable main while mx-4 adds 16px of margin on
each side. That makes the margin-box 100% + 32px wide, which overflows main by
exactly 32px. Because main uses overflow-y-auto its overflow-x computes to auto,
so the overflow surfaces as a horizontal scrollbar along the bottom of the whole
content area under the pagination

The wrapped block is already full width without w-full, so removing that one
token keeps the layout and drops the overflow to 0. This is the same fix already
applied to the Virtual Keys page in #33112, extended to the remaining pages that
share the wrapper: Models + Endpoints, Tag Management, Organizations, Vector
Stores, AI Hub, and Logging & Alerts
2026-07-13 14:33:31 -07:00
yuneng-jiang
f448ea5762
fix(ui): address Virtual Keys redesign review nits (#33112)
* fix(ui): address Virtual Keys redesign review nits

Restore sorting by budget on the merged Spend / Budget column. The column now
uses a new DataTableMultiSortHeader whose chevron opens a menu offering Spend
and Budget in both directions plus Reset, so the progress-bar cell stays merged
while the sort field becomes an explicit choice. Sorting is server-side, so the
chosen field id (spend or max_budget, both accepted by /key/list) flows straight
through as sort_by

Fill the DataTable to its container width when column resizing is on. The table
width was pinned to the sum of column widths, so hiding columns left an empty
gutter on the right. It now keeps that width as a minimum and stretches to 100%
on underflow while still scrolling on overflow, which also covers the same gap
in TeamVirtualKeysTable since both share the component

Drop the dark background box behind the page-header icon so the Virtual Keys
header reads like the Teams header, and pull the 4-line inline filter lambda in
SearchSelect out into a named matchesQuery helper

Extends the DataTable and VirtualKeysTable tests to cover the new multi-field
sort menu (field id maps to sort_by, active indicator, reset) and the
fill-to-container width

* fix(ui): emphasize the active field in the Spend / Budget sort header

The merged Spend / Budget header always read "Spend / Budget" regardless of
which field drove the sort, so after picking Budget descending there was no way
to tell what was sorted without reopening the menu. The header now builds its
label from the sort fields and emphasizes whichever one is active (bold,
full-strength text) while muting the other, so the sorted column reads at a
glance alongside the direction chevron. Drops the now-redundant title prop since
the label is derived from the fields

* fix(ui): remove w-full so the keys page content stops overflowing by 32px

The virtual keys content wrapper used "w-full mx-4", which sets the width to
100% of the parent and then adds 16px of horizontal margin on each side, so its
margin-box came to 100% + 32px and overflowed the scrollable main region by
exactly 32px. That surfaced as a horizontal scrollbar along the bottom of the
whole content area, under the pagination. A block div is already full-width, so
dropping w-full lets mx-4 inset it correctly with no overflow

* fix(ui): darken the clickable Key cell on hover so it reads as clickable

The Key cell was the click target that opens the key detail, but hovering only
faded the chevron in with no change to the cell itself, so there was no cue that
the area was clickable. Give the cell a subtle muted background and a pointer
cursor on hover. The button spans the full cell (a negative inline margin plus a
matching width offset so the hover fill reaches both cell edges while the title
stays aligned with the other columns)
2026-07-13 14:02:27 -07:00
ryan-crabbe-berri
3a42011350
build(ui): bump @tanstack/react-pacer from 0.2.0 to 0.22.1 (#33041)
* refactor(ui): standardize debounce waits behind shared DEBOUNCE_WAIT_MS constant

* build(ui): bump @tanstack/react-pacer from 0.2.0 to 0.22.1
2026-07-13 13:38:15 -07:00
Tin Chi Lo
f90382584b fix(mcp): relay upstream OAuth token and DCR rejections instead of a generic 500
An upstream token endpoint rejection (e.g. Google requiring client_secret even for PKCE web clients) escaped exchange_token_with_server as a raw httpx.HTTPStatusError, which the global exception handler turned into an opaque 500 Internal server error in the create-flow UI. The RFC 6749 section 5.2 error body the IdP sent (error, error_description, error_uri) is now relayed with the upstream's own 400/401 status; rejections outside the section 5.2 contract map to 502 so a broken upstream is not misattributed to the caller. The same relay covers the non-bridge DCR registration arm, and a 200 token response without a usable access_token now answers 502 instead of a KeyError 500. The catch wraps the post call itself because litellm's AsyncHTTPHandler raises MaskedHTTPStatusError at call time, which also made the pre-existing bridge-relay status check unreachable in production. The dashboard's token exchange error message now composes error and error_description so the form shows the IdP's reason
2026-07-13 13:16:45 -07:00
yuneng-jiang
fa09cde3c0
feat(ui): rebuild the Virtual Keys table on the shared DataTable (#32991)
* feat(ui): rebuild the Virtual Keys table on the shared DataTable

Replaces the hand-rolled Tremor table and bespoke toolbar/pagination on the admin
Virtual Keys page with the shared DataTable: server-side sort, paginate, and
filter, a sticky scrolling body, a search plus column-visibility plus filters
toolbar, a right-side filter drawer, and a rows-per-page footer. A page header
with the existing key icon carries the Create New Key action.

Adds reusable, shadcn-default building blocks for the tables migrating onto the
DataTable next: shared IdentityCell, ModelsCell, and SpendBudgetCell in
shared/table_cells, plus a shared PageHeader. The models cell reveals overflow in
a hover tooltip and the spend/budget cell uses the Meter primitive.

All data and domain logic is preserved, including the useKeys query, team and org
alias resolution, the user popover, and the KeyInfoView detail swap. The rich
async Team/Org/Alias filters move into the drawer, and the toolbar search maps to
the key-alias substring search. Status now also reflects key expiry alongside
blocked and SCIM-blocked.

The VirtualKeysTable tests are updated to the new markup and extended with focused
coverage for each new shared cell

* fix(ui): address Virtual Keys redesign review feedback

Fold the status badge into the clickable Key cell and drop the separate Status
column so a key's alias, secret, and status read as one unit. The Key cell is
now the single click target that opens the key detail; the whole-row click is
removed

Migrate the filter drawer off AntD to shadcn. A new Combobox composed from
Popover and Input backs the Team, Organization, and Key Alias filters, keeping
search and the alias infinite-scroll

Show $0.00 for zero spend instead of a hyphen, and extend the shared DataTable
with badge, chips, and meter skeleton shapes so the loading state matches the
loaded cells (status pill, model chips, spend meter) rather than uniform bars

Fix key sorting: the Key column sent its column id "key" as sort_by, which
/key/list rejects with 400. It now sorts by the backend field key_alias

* fix(ui): use the shadcn base combobox and refine the keys filters and skeletons

Replace the hand-rolled filter combobox with the supported shadcn Base UI combobox
(ui/combobox, added via the CLI and reused through a small SearchSelect wrapper).
Its vended input-group and textarea deps are written for React 19 (plain functions
with ref-as-prop); this app is on React 18, where those subcomponents drop the refs
Base UI passes for focus and anchoring, so InputGroupInput, InputGroupButton, and
ComboboxTrigger are adapted to forwardRef. Those ui/ files now diverge from the
registry, and a future shadcn add would overwrite the adaptation until the app moves
to React 19. Adds class-variance-authority, which input-group needs

Give loading skeletons a per-column renderSkeleton escape hatch on the shared
DataTable and mirror the Key cell exactly (alias line, secret, status pill), so
skeleton rows match the real rows instead of being shorter and simpler

Resolve the automated review: the toolbar search and the drawer Key Alias filter
both mapped to the key-alias query, so the search silently overrode the drawer value
while its chip stayed visible. Consolidate to a single alias search in the toolbar
(placeholder now "Search by key alias…") and drop the redundant drawer field. Re-add
coverage for the Created By column's alias-over-email display

Refine the Team and Organization filters: they match on name and id, so the labels
read "Team" and "Organization" rather than "... ID", each option shows the name with
the id on a muted second line instead of "name (id)", and the active-filter chip
shows the friendly name

* chore(ui): drop duplicate class-variance-authority, use the repo cva package in input-group
2026-07-13 12:49:01 -07:00
ryan-crabbe-berri
aa9dcb43cf
refactor(ui): standardize debounce waits behind shared DEBOUNCE_WAIT_MS constant (#33040) 2026-07-13 12:19:57 -07:00
ryan-crabbe-berri
7fce761cde
fix(ui): respect litellm_key_header_name in BYOK credential save and workflow runs fetches (#33103) 2026-07-13 12:19:38 -07:00
ryan-crabbe-berri
e6d916b82e
fix: show and allow editing team model aliases after team creation (#33047)
* refactor(ui): rename OldTeams component file to Teams

* fix: show and allow editing team model aliases after team creation

* fix(ui): mark team model_aliases as nullable to match the prisma schema
2026-07-13 09:31:10 -07:00
ryan-crabbe-berri
d0428cdd53
ci(ui): report only error-level knip findings in CI (#32971) 2026-07-12 21:27:55 -07:00
Krrish Dholakia
26ab730bfa
feat(router): soft-floor adaptive mode for complexity router (#32947)
* feat(router): soft-floor adaptive mode for complexity router

Let complexity_router_config.adaptive=true Thompson-sample across the
union of tier pools with a tier-distance penalty, and wire the existing
adaptive post-call bandit so mis-tiered requests can still recover.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(router): reattach adaptive hooks for hybrid complexity

Finalize was wiping every AdaptiveRouterPostCallHook and only
re-registering standalone auto_router/adaptive_router deployments,
so complexity adaptive=true never received bandit updates.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(router): drop unnecessary hybrid docstrings

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(router): attribute adaptive feedback

Credit user reactions to the model that produced the previous response while keeping current-response signals on the serving model

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(router): tune hybrid cold defaults

Use the cost-weighted policy that beat equal-pool complexity in the full bakeoff, and make the committed harness compare identical tier pools

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(router): preserve hybrid cold quality floor

Sample only unobserved models in the classified tier until feedback exists, then apply adaptive scoring without mis-penalizing models shared across tiers

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(router): bound feedback context cache

Cap retained session feedback so unique session IDs cannot exhaust router memory

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(router): preserve exhaustion signals

Include tool-result exhaustion in adaptive feedback and clear strict lint regressions blocking CI

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(router): remove stale owner cache

Remove obsolete attribution state, tighten the embedded router type, and keep the test diff focused on adaptive behavior

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(router): centralize hook cleanup

Use the callback manager to discover and remove adaptive hooks across every registered callback list

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-11 21:56:33 -07:00
Abhimanyu Kapur
8d358574ea
Merge pull request #32978 from BerriAI/litellm_auto_router_qol
fix(auto_router): filter embedding models in complexity tab dropdowns, require all tiers, inline validation
2026-07-11 19:49:30 -07:00
Abhimanyu Kapur
0e90f61e48 fix(auto_router): inline error for missing LLM classifier model
Selecting the LLM classifier without picking a model only surfaced a
toast on submit; the classifier model select now gets the same red
outline and helper text as the tier and embedding selects once a submit
attempt has failed.
2026-07-11 19:21:51 -07:00
Abhimanyu Kapur
d6883d15b0 fix(auto_router): flag name field and tier fields together on empty submit
Clicking Add Auto Router with the name empty returned early with only a
toast, so blank tier selects never got their inline error state. The
empty-name branch now sets showValidationErrors and triggers antd
validation on the name field, so every unfilled mandatory field is
flagged at once. Adds a regression test for the tab component.
2026-07-11 19:08:07 -07:00
Abhimanyu Kapur
3464b5e7df fix(auto_router): reset inline validation errors when switching router type 2026-07-11 19:03:48 -07:00
Abhimanyu Kapur
0de308a5b8 fix(auto_router): filter embedding models out of tier selects, require all tiers, add inline validation
The Add Auto Router complexity tab let chat models fill the embedding-model
slot (and vice versa) since neither dropdown filtered on ModelGroup.mode, and
submit only required at least one of the four tiers instead of all four. Adds
getMissingTiersError alongside the existing getSemanticConfigError, and
highlights unfilled tier/embedding selects inline once a submit attempt fails.
2026-07-11 18:18:37 -07:00
ryan-crabbe-berri
5fc1a3c671
refactor(ui): convert endpoint usage charts to shadcn/recharts (#32723)
* refactor(ui): convert endpoint usage charts to shadcn/recharts

Adds a LineChart wrapper to the shared charts kit, mirroring the
BarChart/AreaChart composition with connectNulls and curveType props,
and converts EndpointUsageBarChart and EndpointUsageLineChart from
tremor to the shared wrappers. Both endpoint chart tests now assert on
real recharts SVG output instead of tremor mocks.

* refactor(ui): drop unused endpointData prop from EndpointUsageLineChart

* fix(ui): point endpoint chart test type imports at the UsagePage types alias after colocation move
2026-07-11 18:16:18 -07:00
ryan-crabbe-berri
b6dbda48f9
refactor(ui): colocate the mcp-servers view, keeping the shared mcp_tools surface (#32968)
* refactor(ui): colocate the usage view, keeping the shared usage components

Split for the usage (UsagePage) segment. Most of the folder is the usage page's
own view, but four pieces are reused elsewhere and stay in @/components/UsagePage:
TopKeyView (old-usage), KeyModelUsageView and value_formatters (activity_metrics),
and the shared types (activity_metrics, chartUtils). The other 21 files move into
usage/_components, preserving the folder structure.

The external consumers import only the retained files, so they are untouched. The
moved files' imports of the retained files become @/components/UsagePage paths,
other escaping relative imports are absolutized, and lint suppressions are re-keyed
for moved files only. No behavior change.

* refactor(ui): colocate the mcp-servers view, keeping the shared mcp_tools surface
2026-07-11 18:01:41 -07:00
ryan-crabbe-berri
8c5473f198
feat(ui): adopt openapi-react-query ($api) and convert useCustomers (#32949)
* feat(ui): adopt openapi-react-query and convert useCustomers to $api

Add openapi-react-query and expose $api = createQueryClient(fetchClient)
alongside fetchClient. Rewrite useCustomers as
$api.useQuery("get", "/customer/list", {}, { enabled, select }), which
derives the query key from method + path (dropping the hand-written
createQueryKeys entry and the manual key) and forwards the request signal
for cancellation. The response type still flows from schema.d.ts as
CustomerResponse[]. Tests assert the path, the admin/token enabled gate,
and the empty-body select fallback.

* test(ui): read the last render's options in useCustomers helper

The lastCallOptions helper was named for the last call but read
mock.calls[0]. Harmless while each test renders once, but it would
silently assert against first-render options if a test ever re-renders.
Read the final call instead.
2026-07-11 18:01:33 -07:00