Commit graph

6288 commits

Author SHA1 Message Date
Mateo Wang
5012083b72
feat(vertex_ai): let operators drop chosen anthropic-beta values on the Vertex AI pass-through routes (#47185)
* feat(vertex_ai): let operators drop chosen anthropic-beta values on the Vertex AI pass-through routes

* fix(vertex_ai): drop configured betas sent through x-pass-anthropic-beta and only fail beta requests on a malformed setting

The drop list now runs on the final upstream headers, after x-pass- expansion, through an
upstream_headers_filter on pass_through_request, so x-pass-anthropic-beta cannot carry a
dropped value to Vertex. A malformed setting now answers 500 only for requests that carry
anthropic-beta or x-pass-anthropic-beta, so Gemini and discovery calls keep working.

* test(vertex_ai): accept the new upstream_headers_filter argument in the existing Vertex route tests

* fix(vertex_ai): refuse a malformed drop-betas list at boot and match list entries written with spaces

* fix(vertex_ai): split drop-betas list entries on commas like the header
2026-10-18 12:51:27 +00:00
Mateo Wang
68a8b12ab3
fix(proxy)!: end a chat completions stream with an error event when a guardrail blocks mid-stream (#46953)
* fix(proxy): end a chat completions stream with an error event when a guardrail blocks mid-stream

A streaming guardrail that raised an HTTPException after the first chunk re-raised it inside the SSE generator, so the 200 body was cut off with no finish_reason, no error event and no [DONE]. Build the error frame through sse_error_payload instead, keeping the status, message and structured detail, and encode values JSON cannot represent as strings so the frame always goes out.

* fix(proxy): keep early guardrail blocks on their HTTP status and end mid-stream errors with [DONE]

A guardrail block before the first chunk is raised again, so its response stays the
400 event-stream body it is on main. A mid-stream error event now carries the failure
hook's replacement error when one is returned, is followed by data: [DONE] on
OpenAI-format streams, and leaves the Gemini route's raise unchanged. Error details
JSON cannot encode are written as strings instead of breaking the event.

* fix(proxy): keep a per-chunk guardrail block on the first chunk on its event-stream 400

* fix(proxy): treat a keepalive ping as a started stream so a later guardrail block ends in-band

* fix(proxy): encode non-JSON guardrail details in early stream error frames

An early guardrail block whose detail holds a value JSON cannot encode sent
400 headers and then cut the body. The error frame now writes such a value as
its string form, the same as the mid-stream event. The keepalive test now runs
on a virtual clock instead of real sleeps.

* fix(proxy): show an in-band stream error event as an error in the CLI, Python client, and dashboard playgrounds

ChatClient.completions_stream raises StreamError on a data frame carrying an error, so litellm-proxy chat prints the block reason and keeps the partial reply out of history. The prompt playground and the Interactions playground read the same frame through a shared streamErrorMessage helper. Adds the audit's integration cells for chat stream error events, and makes the endpoint tests' failure-hook mocks return None so they check the real error payload.

* fix(proxy): print stream error text verbatim in the CLI and read split error events in the prompt playground

The CLI printed the server's error message as Rich markup, so a closing tag like [/pii] in a guardrail message crashed lite chat. The prompt playground now buffers partial lines across reads, ChatClient raises only on a non-empty error, StreamError redacts keys like the other client exceptions, and StreamError is exported from litellm.proxy.client

* fix(proxy): print CLI stream errors without Rich highlighting or emoji codes

* test(proxy): cover the CLI's plain-text 400 error print

* test(proxy): annotate the new client stream test locals as Final

* fix(proxy): send the failure hook replacement on Responses streams and before the first chunk

* fix(proxy): end a Responses stream with response.failed when it fails on a built-in exception

* fix(proxy): keep a failure hook replacement before the first chunk in-band so bare stream routes still answer it

A non-HTTP failure whose hook returns an HTTP error was raised before the first
chunk, which create_response turned into the hook's status but which a bare
StreamingResponse route such as /queue/chat/completions answered as a 200 with
zero bytes. Only the original HTTP errors raise now. The replacement goes out as
the stream's error event, which create_response still answers with the hook's
status as a JSON error.

Adds audit round 2's integration cells for failure hook replacements on chat and
Responses streams, built-in exceptions on Responses streams, and the Python client.

* test(proxy): cover a queue stream whose failure hook replaces a failure before the first chunk

The audit cell posts to /queue/chat/completions with a callback that raises before the first chunk and a failure hook that answers 403. It checks for a 200 event stream whose only event is the replacement, then one [DONE], with the callback's own message absent
2026-10-17 18:00:12 -07:00
ishaan-berri
4a8b47542c
feat(ui): show gateway URL and agent setup instructions after creating a key (#47199)
* feat(ui): show gateway URL and agent setup instructions after creating a key

Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>

* fix(ui): make the selected agent button visibly distinct in dark mode

Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>

* fix(ui): hide agent instructions by default and use a generic title

Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>

* fix(ui): use one shared agent instructions card with Claude Code and Codex logos

Co-Authored-By: Ishaan Jaffer <155045088+ishaan-berri@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-17 17:34:03 -07:00
Mateo Wang
75faaeab64
feat(count_tokens)!: configurable token_count_framing and token_count_model, count source in the response (#46777)
* feat(count_tokens): configurable token_count_framing and token_count_fallback_model, count source in the response

Count endpoints now say where each number came from in litellm_count_source
(provider, fallback_model, local_estimate) and name the fallback model in
litellm_count_fallback_model. When the provider answers 400 or 404 for a model
it cannot count, the proxy counts the same request with token_count_fallback_model.
token_count_framing adds a per-model constant to local estimates. Both settings
ship in the cost map for Bedrock Claude Sonnet/Haiku 5.5, Bedrock Kimi K3, and
Databricks Kimi K3 and DeepSeek V4 Flash, and can be overridden in model_info.

* test(count_tokens): classify the new cost map keys in the schema generator and expect count_source in exact-shape tests

* test(count_tokens): e2e matrix against real inference and integration wire tests for count source, fallback and framing

* fix(count_tokens): keep the provider error when local counting is disabled, name the fallback error, keep settings in cooldown

Moves the cost map lookup off the event loop

* ci(cost map): bound token_count_framing to non-negative and token_count_fallback_model to non-empty

* fix(count_tokens): keep a failed fallback's own status, ship fallbacks only where they count, add system.ai framing

A fallback model that answers 404 returns the requested model's error unchanged, and any other fallback
failure answers with its own status and message. The eu, apac, au and jp Sonnet and Haiku 5.5 entries
no longer ship the Opus 5.5 fallback, which AWS serves only in us-east-1. A configured empty
token_count_fallback_model turns the shipped fallback off. Config framing accepts whole-number floats and
rejects negatives. The Databricks system.ai Kimi K3 and DeepSeek V4 Flash names carry the same framing as
their serving endpoint names, and the e2e matrix runs on Bedrock Haiku 5.5 too.

* test(databricks): list the system.ai Kimi K3 and DeepSeek V4 Flash names with their published DBU rates

* fix(count_tokens): skip a token count fallback model for an hour after it answers 404 on that deployment

A us. or global. Sonnet 5.5 deployment outside us-east-1 paid two extra Bedrock
calls and about 200 ms on every count, since Opus 5.5 cannot count there either.
The Kimi K3 e2e also runs the large system prompt shape now.

* test(count_tokens): use the shared integration port allocator in the fallback wire test

* fix(cost map): keep the Databricks system.ai entries to framing only, so their spend stays as today

* test(count_tokens): cover the fallback 404 skip, inference profile base_model, shipped framing, and deployment-scoped token count settings

Adds integration cells for a fallback model that answers 404, an application inference profile ARN deployment whose base_model carries the shipped fallback, and the shipped Kimi K3 framing with token_count_framing 0 turning it off. Adds unit tests that keep token count settings off the shared backend cost map key and that store a token count setting on PATCH only when the operator changed it

* fix(count_tokens): keep the counter's own 400 and the provider's error when a framed estimate cannot read the body

A 400 the OpenAI or Gemini counter returns before calling the provider is the caller's answer, so it no longer tries the fallback model or a framed estimate. When the framed local estimate cannot read a content block, the client gets the provider's error instead of a 500. Adds /audit round 2's integration tests for the fallback leg's converted body, the Anthropic envelope on fallback failures, and both framing cases.

* fix(count_tokens): answer the provider's error whenever a framed estimate raises

A tool schema the local estimate cannot format, such as a boolean subschema or an array with no items, raised AttributeError or KeyError and answered a 500 on a framed deployment whose provider had already answered 400. Any estimate failure now answers the provider's own error.

* fix(count_tokens): load the custom tokenizer inside the guarded framed estimate

A framed deployment whose custom_tokenizer could not load answered a 500 or 503 where the provider had already answered 400, because the tokenizer was selected outside the estimate's error handling. Selecting it inside answers the provider's error, logs a warning with the estimate's error, and still raises the Rust extension's fork-guard errors.

* fix(count_tokens): look up framing under the deployment's custom_llm_provider

* fix(count_tokens): share one tokenizer download per worker and bound the wait after a provider error

A framed deployment whose custom_tokenizer is not cached locally made every
count wait out the Hugging Face hub's whole retry cycle when the hub could not
be reached, and a burst of those loads held the default thread limiter that
every count's cost map lookup also needs. Counts on a worker now share one
download per minute, a failed download is reused for the rest of that minute,
and after a provider error a count waits at most 5 s before answering the
provider's error. The swallowed-estimate warning logs only the error type.

* fix(count_tokens): keep one tokenizer download per key running and load the local route per request

* test(count_tokens): type the scripted counter and wait on events instead of wall time where possible

* fix(count_tokens): run framed tokenizer loads on their own capped threads and answer a tokenizer panic with the provider error

Framed loads used anyio's default 40 threads, so a hub that drops packets let about 40 hanging loads stall every count on the worker. Loads now run on a per-worker pool of 4, and a count that finds 4 loads running answers the provider's error at once. Any finished load is reused for a minute after it finishes, so a load that fell back to tiktoken is not retried on every count. A pyo3 PanicException from the tokenizer, while loading or counting, now answers the provider's error instead of a plain 500

* feat(proxy): count tokens with token_count_model in its pinned region

token_count_model {model, region} replaces token_count_fallback_model. Bedrock Sonnet and Haiku 5.5 count with Opus 5.5 in us-east-1, or Melbourne for au. profiles, and a failure names the region to enable

* fix(count_tokens): use the renamed count model constant in the e2e test and list token_count_model with the object keys

* fix(proxy): keep the upstream error envelope when adding the region hint

* test(proxy): annotate the token count setting echo test

* ci(cost map): require non-blank token_count_model strings and test the region rules

* fix(count_tokens): answer the token_count_model's error even when framing is set

* fix(count_tokens): keep the provider error for Google contents instead of a framed estimate

* fix(count_tokens): estimate Google contents the route translated to messages

* fix(count_tokens): keep the provider error for Google contents with non-text parts

* test(proxy): drive tokenizer wait and cost map tests by events and a skippable loop clock

* fix(count_tokens): check Google text-only parts without a KeysView comparison basedpyright flags

* feat(proxy): count Bedrock Kimi K3 tokens exactly with its pinned tokenizer

* test(bedrock): keep the Kimi K3 image check in-process

* fix(count_tokens): join the region hint to an upstream message that already ends with a period

Also pins that a blank token_count_model in config falls through to the cost map default

* fix(count_tokens): count Gemini tool-call turns and Responses tools, and stop tokenizer retries from stalling counts

The Google countTokens route estimates contents with functionCall, functionResponse and thoughtSignature parts again, and still answers the provider error for media, thought and unknown parts

The Kimi K3 exact count renders Responses API and Anthropic tools through the chat tool translation the Vertex count already used, now shared from the prompt factory, and falls back to the estimate when the Converse body cannot carry a tool

A failed Kimi K3 tokenizer load is retried in the background without making counts wait, and a load still running after the wait logs a warning

* fix(count_tokens): count Kimi K3 tools the way Bedrock inference sends them

A flat Responses API function or custom tool becomes the chat tool the
Responses route sends to inference, and every other tool goes to the
Converse body as is, so hosted tools Bedrock drops are left out of the
count instead of being counted or turning it into an estimate.

* test(count_tokens): annotate new token counter test locals with Final

* fix(count_tokens): keep shipped count regions inside the deployment's geography

A plain Bedrock Sonnet or Haiku 5.5 deployment in a non-US region no longer sends its count prompt to us-east-1. Global count models and configured token_count_model keep their pinned region. Also expect litellm_count_source in the Vertex Llama count test main added

* fix(count_tokens): resolve count settings off the event loop
2026-10-17 17:11:27 -07:00
Shivam Rawat
c58fc0c996
fix(rag)!: use team provider credentials and allow viewer and JWT team access (#45497)
* fix(rag): use team provider credentials and allow viewer and JWT team access

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

* test(rag): assert persisted managed store data excludes team credentials

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

* test(jwt): run real team model checks in the route denial test

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

* fix(rag): scope team provider credentials to openai vector stores

Bedrock and other providers kept their pre-PR credential and region
resolution: a team bedrock deployment's region no longer overrides the
caller's region on RAG query. Query tests now fake the provider at the
HTTP boundary with a real vector store registry.

* fix(rag): use only the team's own OpenAI deployment for RAG credentials

* fix(rag): ignore caller credential fields the query search never uses when picking team credentials

* fix(rag): treat blank credential fields as absent when picking team credentials

* test(rag): delete the stores the integration ingests create before the owned proxy exits

* fix(proxy): let a restricted key use the team deployment its wildcard or model covers

A key restricted to gpt-4o on a team whose own OpenAI deployment is openai/*
never resolved that deployment's credentials, because the key allowlist was
matched against the deployment's public name literally. The allowlist now
matches a wildcard on either side the way request authorization does, through
the auth layer's own helper, so /rag, /files and /batches resolve the team
deployment for every key that may call a model on it.

* fix(batches): read a restricted key's batch input through its team deployment

The batch access check downloaded the input JSONL with the proxy's OPENAI_API_KEY while the upload and the create used the team deployment, so a key restricted under a team openai/* deployment could not create a batch when the two keys belong to different OpenAI projects. The read now resolves the team deployment the way the create does when no model pins it

Adds the audit cells for keys restricted under the team wildcard on /rag, /files and /batches, and for the rag-vs-chat split of a key restricted to a global exact-name deployment

* fix(rag): load deployments oldest first, reject file_id on stores that cannot take one, and show ingest errors in the dashboard

* fix(rag): pick the team deployment with the lowest id, return 400 for caller mistakes, keep the ingest id in errors, and show partial success in the dashboard

* fix(rag): pick the team's oldest OpenAI deployment so adding one never moves the team

* fix(rag): keep config file team deployments first and try JWT teams in sorted order

A team's config file OpenAI deployments now win in file order, as on the
base, before its DB ones oldest first, so upgrading never moves a team that
has both. A JWT in several teams tries them in sorted order, so every worker
resolves the same team.

* fix(rag): return 400 for Bedrock, S3 Vectors and Gemini store config mistakes on ingest

* fix(rag): keep forwarded provider keys on /rag/query and mixed JWT team claims working

A forwarded x-api-key now counts as a caller credential, so the /rag/query
search uses it over the team deployment as at the merge base. Sorting the
JWT team claim compares entries as text, so a stray number or null no
longer turns into a 401. A Gemini store with no proxy key is a 500 again,
and a non-string vector_store_id keeps the merge base's 400 message.

* fix(rag): let a forwarded provider key win over team credentials on files, batches and ingest

* fix(rag): keep team credentials when a request carries its own key or api_base

A forwarded x-api-key or a body api_key now replaces only the key of the team
deployment, keeping its api_base, as chat completions does. A body
litellm_credential_name no longer turns team credentials off on /v1/batches.

* fix(batches): keep the team key on the team host when a request sends only api_base

A request api_base now replaces the team deployment's api_base only when the
request also brings its own api_key, and a non-string api_key or api_base is
ignored, as on the merge base.

* fix(files): ignore a blank request api_key or api_base next to team credentials

* fix(files): answer 400 for a request api_key that cannot be sent as an HTTP header

* fix(rag): honor a body drop_params for a forwarded key that cannot be an HTTP header

* fix(rag): read the request field for drop_params only on multipart ingest bodies

* fix(rag): type the ingest body read for drop_params

* fix(batches): count a model-routed batch's input with the deployment key it is created with

* fix(batches): reject a batch create whose x-litellm-model headers name different models

* fix(rag): treat a null provider and whitespace store credentials as unset when picking team credentials

* refactor(files): type the team deployment the credential resolver ranks

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-17 23:51:48 +00:00
Misbah Syed
8b442f00fc
fix(proxy): count the whole minor line in the upgrade notice for patch releases (#46942)
* fix(proxy): count the whole minor line in the upgrade notice for patch releases

A patch release's notes only list its backports, so v1.105.1 showed
"0 new features, 1 fix, and 0 other updates" even though upgrading from
v1.104.x brings everything in v1.105.0 too. When the latest release is a
patch, also fetch the recent release list and sum the stable releases on
the same major.minor line (skipping rc/dev prereleases and drafts).

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

* fix(proxy): use tuples for release lists and sync the dashboard API types

Satisfies the LIT001 type-discipline gate and regenerates the
latest_release_info description in schema.d.ts.

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

* test(proxy): cover the bad release-list payload and non-semver latest tag

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

* fix(proxy): type the release-line Counters so basedpyright sees no unknowns

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

* fix(proxy): fetch earlier patches of the minor line by tag

Reading the first page of /releases could miss the line's .0 release when
prereleases push it off the page, and one unrelated release with a null
body failed validation for the whole list. Fetch each earlier patch tag
directly instead (v1.105.0 .. v1.105.N-1), skip tags with no stable
release, and treat a null release body as empty notes.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:49:57 -07:00
yuneng-jiang
4bafebbcdc
fix(ui): take the dashboard's user role and identity from the server (#47025)
* fix(ui): take the dashboard's user role and identity from the server

Adds an internal GET /management/v1/users/me that returns the user id, role
and email the proxy resolved for the calling key. The dashboard now reads
identity from it instead of the session cookie's claims, so its navigation and
role gating always match what the backend authorizes. The layout waits on it
behind the existing loading screen, and a failed lookup clears the session and
returns to login. Sessions that must change their password can also read it

* fix(ui): sign out when the current user lookup fails and wait for it on chat

The dashboard layout now holds the loading screen and signs out when
/management/v1/users/me fails, instead of mounting the shell and looping back
to loading as each consumer refetched the failed query. The chat layout also
waits for the identity so its panels never query without a user id. The
lookup goes through the typed client with the regenerated schema.d.ts

* fix(ui): send the session's auth header on the first user lookup and keep chat closed until authorized

The first lookup runs before AuthProvider registers the custom header name, so it now reads auth_header_name from the session cookie. Chat also waits for isAuthorized, so a failed lookup no longer mounts the shell with an empty user id

* fix(ui): type the dashboard callers of the now typed user id

useAuthorized used to hand back the cookie's user id untyped, which hid six strict type errors in its callers. The id is now string | null, so the production build failed. Callers handle the null case, and query key filters accept the boolean blocked filter

* fix(ui): look up the current user only after the proxy URL is discovered

Pages outside the dashboard shell, like chat and connect, could start the lookup before UI config discovery set the proxy base URL. On a split-origin deployment both attempts then hit the UI origin and a valid session was signed out

* fix(ui): keep a loaded identity while UI config discovery reruns

Pausing the lookup by passing a null token switched the query key, so on the dashboard the identity the layout had already loaded vanished until the shell's own UI config request finished. The key now stays the same and only the fetch waits for discovery
2026-10-17 14:39:33 -07:00
devin-ai-integration[bot]
4273996985
fix(spend_logs): persist user email in spend log metadata so it shows in UI request logs (#41150)
* fix(spend_logs): persist user email in spend log metadata so it shows in UI request logs

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

* fix(spend_logs): update payload expectations for user email metadata

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

* test(gcs_pubsub): add user_api_key_user_email to the spend log fixture

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

* fix(spend_logs): drop untyped metadata key args from the debug log to keep the type gate flat

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

* fix(spend_logs): pass metadata keys to the debug log as a lazy %-style argument

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

* feat(spend_logs): store user email in spend logs only behind store_user_email_in_spend_logs

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

* revert(spend_logs): store user email in spend logs by default, drop store_user_email_in_spend_logs

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

* fix(spend_logs): store an empty user email as null, narrow the expected payload test diff

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

---------

Co-authored-by: jesus <jesus@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-17 13:26:09 -07:00
ryan-crabbe-berri
f6ae25939e
feat(guardrails): add num_retries to generic_guardrail_api for checks lost after connect (#46965)
* feat(guardrails): retry generic_guardrail_api checks lost after connect with num_retries

* fix(guardrails): close each guardrail retry connection before the next attempt and cap num_retries at 5

A failed retry's fresh client stayed open until every later retry finished, so a check that kept timing out held one idle client per retry. Each retry now closes its own client as soon as it ends, and num_retries is capped at 5 so the retry chain stays shallow

* refactor(guardrails): type the generic_guardrail_api inbound header sanitizer as a mapping

Keeps the type check gate at its merge-base count after the num_retries constructor params added unknown kwargs errors in the Qohash subclass

* chore(guardrails): regenerate OpenAPI snapshot and UI types for num_retries, allow the retry client

The guardrail retry opens its own client on purpose to get a new connection and closes it after the attempt, so the per-request async client check allowlists generic_guardrail_api.py
2026-10-17 12:22:41 -07:00
joshua-berri
6d0d0bb68b
feat: add Nace Drex decisions provider (#46523)
* feat: add Nace Drex decisions provider

* fix: expose Nace in dashboard provider metadata

* ci: include Nace tests in the provider shard

* fix: complete Nace provider catalog and preserve OpenRouter tests

* test: mark Nace test directories as packages

* fix: share Drex score parsing across decision providers

---------

Co-authored-by: Joshua Valluru <326636767+joshua-berri@users.noreply.github.com>
2026-10-16 18:07:50 -07:00
yuneng-jiang
37b3a697c7
fix(ui): preserve key settings drafts during background refreshes (#41350)
* fix(ui): preserve key settings drafts during background refreshes

* fix(ui): refresh untouched key settings while preserving edits

* fix(ui): keep team-change MCP clears across key refreshes

clearMcpGrants now marks the MCP fields dirty, so a background refresh with
keepDirtyValues no longer restores the grants a team change just cleared.
Also adapts the draft tests to the onSubmit(values, dirtyFields) signature
on main and asserts a refresh does not mark untouched fields dirty, since the
team admin member-key path rejects saves with non-budget dirty fields.
KeyEditView becomes a one-line arrow to stay under the max-lines limit.

* fix(ui): keep MCP tool defaults out of a key's edited fields

MCPToolPermissions writes non-delete tool defaults for unrestricted servers
on mount. Marking that write dirty made a team admin's budget-only save on a
member key get rejected for editing mcp_tool_permissions. onChange now says
whether a write is a default or a user edit, and the key editor dirties only
edits.

The edit session is now keyed by token or token_id, matching the form's own
fallback, so switching between keys that only carry token_id starts fresh.

* fix(ui): keep a new MCP server's default tool limits across refreshes

Default tool writes for a server the user just added in this edit session
are part of that edit, so they are marked dirty and survive a background
refresh. Defaults written for an already saved server stay clean, so a team
admin's budget-only save on a member key is still accepted.
2026-10-16 17:20:46 -07:00
jesus-berri
8efb99b4ac
feat(ui): shareable URL state for usage views and users-to-usage deeplinks (#43425)
* feat(ui): shareable URL state for usage views and users-to-usage deeplinks

The Usage page now keeps the view, global view tab, date range and entity filters in the
URL, so links can be bookmarked and shared. Relative presets are stored as range=7d and
keep rolling, custom ranges as UTC from/to. Invalid params are dropped, and a link to a
view or user the viewer cannot access shows a toast and falls back to the default view.

Internal Users rows get a "View usage" action that opens usage filtered to that user, and
the per-user usage views link back to the user's record. A "Copy Share Link" button sits
next to "Ask AI"

* fix(ui): clean usage links for sessions without the organizations query

The organizations query only runs for premium sessions, and a disabled query stays
pending forever, so the URL cleanup never ran for other sessions. Gate on isLoading
instead, and give the access denied toast an id so a re-run effect shows it once

* fix(ui): type-check usage entity filters and address review findings

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

* fix(ui): keep team usage available to internal users

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

* style(ui): format users table usage action

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

* fix(ui): sync date picker preset with the current usage range

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

* fix(ui): restore dashboard AGENTS.md from main and drop unused usage state

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

* fix(ui): share custom usage date ranges as calendar days

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

* fix(ui): keep per-user agent usage page across rerenders

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-10-16 16:42:29 -07:00
ryan-crabbe-berri
cc24e7cfa4
refactor(ui): validate model edit prices in the zod schema (#46978)
Each price field now parses its per 1M text into a per token number or null inside the
schema, so the save handler gets clean values and the hand-rolled conversion goes away.
Negative or non-numeric prices now show a field error instead of being saved
2026-10-16 15:25:21 -07:00
ryan-crabbe-berri
9cf09c18fe
fix(providers): route Azure Responses past dated api versions, count Grok reasoning tokens on azure_ai, refresh provider sets after register_model, fix gpt-6.1-sol Azure rows (#46377)
* fix(azure): add responses_api_version so the Responses route stops using the deployment chat api_version

* fix(azure_ai): fold Grok reasoning tokens into completion_tokens like the xai provider

* fix(utils): refresh provider model sets after register_model

* fix(cost-map): align azure gpt-6.1-sol reasoning effort flags with the openai row

* test(e2e): cover Azure Responses on a deployment pinned to a dated chat api_version

* fix(utils): keep router deployment ids out of provider model sets

register_model now refreshes the provider sets, so the per-request and
router registrations keyed by a deployment id put those opaque ids into
wildcard model listings. Those callers now pass
add_to_provider_model_sets=False.

* fix(azure): carry responses_api_version through the chat to Responses bridge

completion() dropped responses_api_version from the litellm_params it hands
the Responses bridge, so azure/responses/<model> on a dated chat api_version
still built the 404ing /openai/responses URL. Also cover the auto-router
marker never forwarding the new param.

* fix(azure): stop listing a wrong set of reasoning efforts when none is rejected

The message said only low, medium and high are supported, which is wrong for
models that also take xhigh such as gpt-6.1-sol.

* test(azure): use the latest grok and gpt-5.4 rows in the new azure tests

* fix(proxy): list each wildcard-expanded model once

A provider set can hold a model both bare and provider-prefixed, which now
happens after any priced request registers its openai/<model> key, so the
openai/* listing showed that model twice.

* fix(azure): route Responses past dated GA api_versions by default

A deployment pinned to a dated GA api_version such as 2024-10-21 for chat now sends Responses calls (create, get, delete, cancel, input_items and the azure/responses/ bridge) to the default Responses version, because those dated versions have no Responses route on Azure and 404. responses_api_version still overrides it, -preview, v1, latest and preview versions are kept, an api-version in the api_base query still wins, and chat keeps api_version as before

* chore(azure_ai): mark the grok chunk_parser dict as an override contract

* fix(azure): rewrite only the route's /openai segment when moving a call to v1

The v1 rewrite in the Azure URL builder replaced every "/openai" substring in the path, so a gateway
api_base like https://apim.example.com/openai-gw/team turned into /openai/v1-gw/team/openai/v1/responses.
Dated GA Responses calls now land on the v1 route by default, which made this reachable for them too,
so the rewrite now only touches the last /openai path segment.

The Responses version resolver no longer duplicates the builder's default for an unset api_version.

Tests cover the gateway api_base case, the env AZURE_API_BASE query-string precedence, and the e2e suite
now has one test for the dated GA default and one where only responses_api_version can make the call work

* test(integration): cover Azure Responses api_version, Grok reasoning spend, wildcard listing and reasoning_effort none

Wire tests pin the Azure route and query for chat, Responses and the azure/responses bridge on a dated GA api_version, and check responses_api_version never reaches the request body. Cost cases check azure_ai Grok folds reasoning tokens into completion_tokens and spend, streaming and not. A config-driven proxy checks /v1/models next to an openai/* wildcard lists each id once and never a deployment id. Azure gpt-6.1-sol rejects reasoning_effort none before reaching upstream and drops it with drop_params

* fix(utils): list ai21 chat and vertex code models under their provider wildcard

register_model now files ai21 chat models into ai21_chat_models, which models_by_provider never read, so a registered ai21 chat model dropped out of ai21/* listings. models_by_provider now reads ai21_chat_models and the vertex code chat and code text sets too.

* fix(main): keep caller-chosen model names out of provider wildcard listings

---------

Co-authored-by: yuneng-jiang <yuneng@berri.ai>
2026-10-16 15:23:46 -07:00
ryan-crabbe-berri
0dac61a555
fix(guardrails): accept Gemini tools in generic_guardrail_api, add opt-in forward_messages_as_sent, name the error type on fail-open (#46338)
* fix(guardrails): accept Gemini tools and Anthropic content blocks in generic_guardrail_api, name the error type on fail-open

GuardrailToolParam required a type key, so Gemini native tools
(function_declarations, googleSearch) failed validation inside the hook
and either returned a 500 or skipped the scan when fail_on_error was
false. The type key is now optional and left out of the forwarded tool
when absent, so an echoing guardrail server cannot write type: null into
the provider request.

structured_messages was re-validated against the chat completion message
types, which silently emptied tool_result, tool_use, image and document
content blocks to content: [] and dropped undeclared keys before the
guardrail saw them. The request model now forwards rows as opaque
mappings, which also retires the write-back shim that restored the
original rows for echoed-back entries.

The fail-open critical log line now carries error_type so a timeout, a
refused connection and an upstream HTTP status can be told apart without
the traceback.

* fix(httpx): retry aiohttp server disconnects the way httpx does

aiohttp raises ServerDisconnectedError when a kept-alive connection is
closed by the peer before a response arrives, which is what a load
balancer idle timeout shorter than AIOHTTP_KEEPALIVE_TIMEOUT produces.
The transport mapped it to httpx.ReadError, which the handler's
connection-error retry does not cover, so the request failed instead of
being resent on a fresh connection. It now maps to
httpx.RemoteProtocolError, the error httpx itself raises for the same
condition, so the existing retry fires.

* test(httpx): build the stub server reply in one bytes literal

* fix(httpx): resend a dropped connection's request as sent and keep a streamed resend readable

The connection-error retry closed its fresh client before a streamed body was read,
so an aiohttp disconnect now billed a second upstream request and still failed. It
also skipped masking on a status error from the resend, resent PATCH and DELETE as
POST, dropped multipart files and ignored the handler's ssl_verify.

The retry now resends the request the first attempt built, on a client with the
handler's TLS settings, closes that client when a streamed body is closed, and masks
a status error the same way the first attempt does.

* fix(guardrails): log HTTPStatusError, not the masking wrapper, as the fail-open error_type

* fix(guardrails): resend a generic guardrail check cut before any response

A load balancer that closes an idle keepalive connection made the guardrail POST
fail with "Server disconnected", because #46336 only resends POST on ConnectError.
AsyncHTTPHandler.post now takes a keyword-only idempotent flag, defaulting to
False, that gives a RemoteProtocolError the same single fresh-client resend PUT
and DELETE get. Only GenericGuardrailAPI.apply_guardrail opts in, since a
guardrail check is a pure verdict over the request. Every other POST keeps the
#46336 rule

The fail-open log line now puts error_type at the end so the pre-PR text stays
contiguous for existing alert matchers

test_rows_reach_the_guardrail_verbatim now reads the body off a real local
guardrail server instead of mock call args

* fix(http): keep a mid-body server disconnect a ReadError and move socket tests to integration

A ServerDisconnectedError raised while reading a response body now stays
httpx.ReadError, as it was before this PR. Only a disconnect before any
response maps to RemoteProtocolError, which is the case the resend rule is
about. Stream readers that end on ReadError, like the SAP stream iterator,
keep their old behavior

The loopback-server disconnect, TLS and guardrail resend tests move to
tests/integration/sdk/test_disconnect_resend_wire.py, since tests/unit is
in-process only. Unit keeps in-process MockTransport checks for the idempotent
POST resend, masking on the resend, streamed and buffered client closing, the
guardrail resend, and the guardrail receiving rows verbatim

* fix(guardrails): resend a reset guardrail check and keep typed echoes on texts

An idempotent POST now resends once on any network error after connect
(ReadError, WriteError, RemoteProtocolError), so a guardrail behind a load
balancer that answers a stale keepalive with a TCP reset rides it out too.
That makes the aiohttp ServerDisconnectedError remap unnecessary, so it goes
back to ReadError. This stops PUT and DELETE from stacking litellm's resend on
top of aiohttp's own idempotent retry (4 sends instead of 2) and stops Vigil
Guard from replaying its analyze POST on a disconnect

Every resend now runs inside the method's own error mapping, so a timeout on
the resend is a litellm.Timeout like the first attempt and a status error is
masked once by the caller

A guardrail server that parses rows with its own message types and echoes them
back without the keys those types lack (annotations, tool_calls[].index) is
read as an echo again, so its texts redaction reaches the LLM. A row only counts
as an echo when no value changed and every key the chat completion types
declare is kept

The wire tests for the PUT/DELETE resend, its masking and its TLS settings now
run on the httpx transport, where litellm's resend is the only retry, and the
POST and guardrail tests cover both transports and both cut styles

* test(guardrails): black box wire tests for generic guardrail shapes, fail-open error type and idle cut resend

Integration tests against an owned proxy and scripted peers. Gemini native tools reach the guardrail and Gemini as sent, the guardrail sees chat rows and an Anthropic image block as sent, an echoing guardrail leaves a /v1/messages request unchanged, the fail-open log line names ConnectError, Timeout or HTTPStatusError, and a guardrail check cut on an idle pooled connection is resent once on each worker. Each fails at the merge base and passes at the head

* fix(guardrails): check echoed rows without recursion and keep pytest.raises to one statement

* refactor(guardrails): move the dropped-connection guardrail resend out of this PR

The resend on a dropped or hung generic guardrail call, and the shared AsyncHTTPHandler retry rework behind it, move to a follow-up PR that adds a per-guardrail num_retries setting. This PR keeps the request and response shape fixes and the error_type on the fail-open log line

* test(guardrails): pick the closed guardrail port from the integration port allocator

Reading a port back from a port-0 bind trips TQ012, so the fail-open connect error case now takes its unused port from allocate_port

* feat(guardrails): forward structured_messages as sent only behind forward_messages_as_sent

generic_guardrail_api keeps main's OpenAI-format rows and echo comparison by default. Setting forward_messages_as_sent: true on the guardrail sends rows as the client sent them and treats an echo that only drops undeclared keys as unchanged

---------

Co-authored-by: yuneng-jiang <yuneng@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-16 14:52:35 -07:00
Yassin Kortam
3637dd711f
feat(ui): add Authorization URL, Token URL and Scopes fields for OAuth Delegate and True Passthrough MCP servers (#46941)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-16 14:48:42 -07:00
yuneng-jiang
5bcced0187
feat(ui): redesign the playground chat around a single composer and inspector (#46934)
* feat(ui): add clean-slate playground chat prototype

* feat(ui): replace the playground chat with the redesigned composer and inspector

* fix(ui): close the playground chat gaps found in self-review

Resend only complete text exchanges, so empty replies, MCP direct calls and
transcriptions no longer leak into chat history. A stopped request can no longer
append output, attach its gateway trace to a newer reply or clear the newer
request's loading state. MCP selections are only pruned once both the server and
toolset lists load. The MCP tool picker lists toolsets again and calls the server
that backs the chosen tool, and a saved model the current key cannot use is
cleared when the model list loads, matching the old chat

Also vendors the scroll-fade-x utility the attachment primitive uses

* fix(ui): resolve playground toolset tools per server and drop late session ids

A toolset now lists only its own tools, each resolved on the server the toolset
names, so the args form and the call always target the same tool. A saved
toolset no longer asks for tools with its own id while the toolset list loads,
and a failed tool list says so instead of claiming the server has no tools

A stopped request can no longer overwrite the Responses session id, starting a
new chat drops a request that is still running, embeddings replies are no longer
resent as chat history, and a saved model missing from an already cached model
list is cleared too. A failing toolset list falls back right away instead of
retrying while the MCP pickers show loading

* fix(ui): guard playground sends and label inspector stats

Pressing Enter twice no longer sends the same message twice, an attachment the
new endpoint cannot take is dropped when the endpoint changes, a stopped
Responses request can no longer show its code output on a newer reply, and a
reply that broke mid-stream is no longer resent as history

The inspector's speed, cost and routing stats get the same icons as the reply
footer, and the footer's metrics now line up with its copy and Inspect buttons

* fix(ui): write playground key settings only through the encoded storage path

Key source, virtual key and custom headers now go through their own write loop, separate from the plain settings, so a key value can never take the plain sessionStorage branch

* fix(ui): restore playground BYOK connect and reconnect

Picking a BYOK server for a direct tool call now shows a key button next to the
server picker, so the key can be connected without leaving the MCP endpoint. A
connected BYOK server gets a Replace key action in the MCP dialog, also shown
when its tools fail to load, and saving a key reloads that server's tool list

* revert: restore the single playground settings storage loop

Reverts ca4e742115. The CodeQL finding covers the same encoded sessionStorage handling the old chat used, so it is left as is

* test(ui): type the BYOK credential mock without deep instantiation

* fix(ui): let users back out of replacing a BYOK key

The replace-key form in the MCP dialog now has Cancel, and it resets when another server is picked or the dialog closes, so a working key is never forced to be overwritten

* refactor(ui): name the MCP dialog row focus handler

* fix(ui): accept exact custom models, tags and decimal temperatures in the playground

A typed model or tag that is a prefix of a listed one could not be used,
because Enter picked the partial match and the custom action only showed
when nothing matched. The custom value is now a list option whenever no
exact match exists. Temperature gets an editable input beside the slider
so values finer than the 0.1 step can be sent again.

* fix(ui): stop claiming no guardrails ran when the gateway sent no headers

Direct MCP calls, A2A agents and any request that fails before headers
arrive built their trace from an empty header set, so the inspector said
no guardrails or policies ran. The gateway fields are now left unset in
that case and the inspector says the endpoint does not report them.

* fix(ui): leave policies unknown when the gateway headers are hidden

The proxy exposes x-litellm-applied-guardrails to other origins but not
x-litellm-applied-policies, so a dashboard on another origin read every
request as having run no policies. Policies are now unknown unless the
call id header is readable, and the inspector says so.
2026-10-16 14:04:36 -07:00
ryan-crabbe-berri
c7b115b56a
feat(ui): add the 1 hour cache write cost to the add model and model edit forms (#46830)
* feat(ui): add the 1 hour cache write cost to the add model and model edit forms

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

* test(e2e): cover both cache write tiers in the add model and edit forms

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

* test(ui): hoist the expected pricing payload out of the inline assertion

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

* refactor(ui): share the per-token conversion between both cache write tiers

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

* refactor(ui): derive the touched pricing field type from the pricing field list

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

* refactor(ui): collapse the duplicated pricing source keys into one map

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

* fix(ui): convert add model pricing once for all selected models and describe the 1 hour fallback

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

* test(e2e): type the cache write tier deployment read back and fill the 1 hour field

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

* style(e2e): keep the cache write tier spec on the dashboard prettier config

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-10-16 13:44:24 -07:00
Yassin Kortam
f02449ad38
feat(sso): retain the SAML assertion at login as the saml2 subject token for oauth2_id_jag (#46745)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-16 12:16:56 -07:00
Yassin Kortam
bca7650e8f
feat(mcp): accept an ID-JAG as an RFC 7523 jwt-bearer grant on the gateway token endpoint (#46748)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-16 12:16:55 -07:00
Rick
ecbea93381
perf(ui): page the member tables and look up budget rows once per render (#26667)
* perf(ui): look up a team member's budget row once per render

TeamMemberTab resolved each row's membership with Array.find() over
team_memberships inside every extra column's render and sortValue, so a
render walked the full membership list once per cell. Build the user_id ->
membership map once per team_memberships change and read from it instead.

* perf(ui): page the member tables instead of rendering every row

MemberTable rendered one row per member, so a team or an org with a couple of
thousand members paid for every row on each render. Its DataTable now runs
client-side pagination, like the other list tables in the dashboard.

* fix(ui): keep the member table on its page when the roster is refetched

* test(ui): cover a team member's spend and budget refreshing after a refetch

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-16 08:01:06 +00:00
yuneng-jiang
0620f1a515
fix(ui): validate the Add Model form when Test Connect is clicked (#46834)
* fix(ui): gate Add Model's Test Connect on the form's own validation

Test Connect stays disabled until the Add Model form would pass validation,
with a tooltip saying why. Provider password, text and textarea inputs now
render empty after a successful add resets the form, and an empty prepared
request shows a plain message instead of a destructuring crash

* fix(ui): validate the Add Model form when Test Connect is clicked

Test Connect stays enabled and, on click, runs the same validation Add Model
runs. An incomplete form now shows the inline required errors instead of
opening a crashing connection test, and a valid form tests as before. This
replaces the disabled-button gate, which read a validity flag that went stale
after programmatic form updates

* test(ui): cover a blank required Azure API Base on Test Connect
2026-10-16 04:44:58 +00:00
devin-ai-integration[bot]
77c22a4c01
feat(proxy): add agent_access_default_deny for least-privilege agent access (#43979)
* feat(proxy): add require_key_agent_access_defined for least-privilege agent access

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

* test(proxy): align A2A model visibility assertions with supported routes

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

* fix(proxy): keep A2A model entries on their existing routes

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

* test(proxy): point the listing model at the harness upstream

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

* fix(proxy): coerce require_key_agent_access_defined like the settings validator

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

* test(proxy): inject agent access lookups instead of patching the handler

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

* refactor(proxy): rename require_key_agent_access_defined to agent_access_default_deny

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-10-15 16:49:26 -07:00
devin-ai-integration[bot]
1ddaa54793
fix(mcp): scope gateway OAuth consent to x-mcp-servers and add explicit scope setting (#44957)
Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-15 15:58:34 -07:00
Krrish Dholakia
0a17da59fd
fix(ui): place Home Create New Virtual Key button like the Virtual Keys page (#46778)
Co-authored-by: Krrish Dholakia <krrishdholakia@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-15 15:57:37 -07:00
Mateo Wang
7090588bdf
feat(gemini)!: auto-continue gemini-4-argon long decodes with continuationToken (#46240)
* feat(gemini): continue MAX_TOKENS decodes with continuationToken across chat, streaming, and interactions

* fix(gemini): continue long decode on CONTINUATION and cancel interactions at /cancel

* feat(gemini): add public gemini-4-argon cost-map rows for AI Studio and Vertex AI

Add gemini/gemini-4-argon and vertex_ai/gemini-4-argon alongside the existing
early-access row, carrying the same limits and capabilities. The AI Studio row
mirrors the early-access entry; the Vertex row differs only in provider and
pricing source. Prices follow once Google publishes them.

Extend the mapped metadata test to cover all three ids, that each routes to its
provider, that its backup copy matches, and that the public rows carry the same
limits as the early-access row.

* feat(gemini): raise gemini-4-argon to the 1M long decode cap and report initial_prompt_tokens across hops

* chore(prices): build on the merged gemini-4-argon rows and only raise the long decode cap

* fix(gemini): follow continuation hops in a loop instead of recursion

The unary hop collectors and the stream iterator called themselves once per continuation request, which the recursion check in CI rejects and which would hit the interpreter's call depth on a very long output. They now loop, and a test follows more hops than the interpreter allows nested calls

* fix(gemini): bill Google Search grounding from every continuation request

Google sends search grounding once, on the request that ran the search, and does not repeat it on a continuation request. The merged response kept only the last request's grounding, so usage.web_search_requests and the per-search fee dropped every earlier search. Unary merges and the stream iterator now carry the grounding of every request in the chain

* fix(gemini): continue long gemini-4-argon answers on streams and Vertex AI

A continued stream stopped after the first chunk of its second request. The stream wrapper asks the iterator for a fresh start before every chunk, and that reset it to the first request's finished stream. The iterator now reads from the newest request's stream

On Vertex AI the continuation request went to the v1 API version, which rejects continuationToken with a 400. A request that carries the token now goes to v1beta1, which accepts it, including a token that v1 issued

On a stream, initial_prompt_tokens came from a usage chunk in the middle of the stream, which reports a smaller prompt count than the last chunk. It is now the count the first request's stream ends with, the same number a unary call reports

continuation_token is no longer forwarded on the Interactions API, which answers 400 for the field. It is dropped there as it was before this branch

* chore(gemini): name the type errors the dual-mode stream iterator trips

The stream iterator holds a sync or an async line iterator in one untyped attribute. Pointing it at a continuation request's stream gave the type checker a concrete union for it, so six existing lines that only ever run in one of the two modes started to report errors

This adds a rule-named suppression with a reason on each of those lines and changes no code. Splitting the class into a sync and an async iterator would remove them and is left for a follow-up

* fix(gemini): continue only when Google ends a request with CONTINUATION, and stop streaming mid-answer tokens

A long decode now sends the next request only when Google's finish reason is CONTINUATION, so an answer cut at the caller's own max_tokens returns in one request with its continuation_token instead of drawing a rejected extra request

A request for more than one choice is never continued, since Google rejects a continuation with more than one candidate

Chat-format streams no longer carry the tokens Google sends mid-answer. Only a stream that ends cut off carries a continuation_token, on its last content chunk

The response cache key now includes continuation_token, so a resend with a token is not served the earlier segment

* fix(gemini): keep one response id across a continued answer

A continued chat stream changed its chunk id each time the next request to Google started, and a unary answer came back under the last request's id. Every chunk and the unary answer now carry the first request's id, including the chunk that ends a cut-off stream

A request whose n or max_tokens arrives as a string is sent once and never continued, where it used to fail with a 500 on the comparison. An empty continuation_token no longer changes the cache key

* test(gemini): add integration cells for long decode continuation

Covers continued answers on chat, /v1/messages and /v1/responses, streamed and not, on the Gemini API and Vertex AI against a scripted Google, plus concurrency, outage and worker restart runs

Also moves the Google stand-in cancel route to /cancel, the URL this PR now calls for Interactions cancel, and checks the cancel test posts there exactly once

* fix(vertex_ai): fetch the access token again before each continuation request

A long Gemini 4 answer on Vertex AI can run for over half an hour of follow-up requests, and every one of them reused the bearer token fetched before the first request. Once that token expired Google answered 401 and the caller got a cut-off answer with finish_reason length

Each Google request in a chain now asks for the token again, which is a cache hit until the token nears expiry, on the streamed and unary paths, sync and async. A refresh that fails counts as a failed follow-up, so the caller still gets the answer so far with its continuation token. A caller's own Authorization in extra_headers still wins on every request, and the Gemini API key path is unchanged

* fix(vertex_ai): start each continuation stream with an empty parser buffer

When a Google stream ended on an unfinished frame, its leftover text stayed in the stream parser and every frame of the next continuation stream was appended to it. None of them could be decoded, so the whole continuation was billed and dropped without an error

Each continuation stream now starts with a fresh buffer, and a test covers a stream that ends on an unfinished frame, sync and async

* fix(vertex_ai): treat a continuation stream with no readable frame as a failed follow-up

A continuation stream that answered 200 and then sent Google's error frame first, sent a body that is not JSON, or dropped before its first frame ended the caller's stream with `stop` or an error and no token, so a cut answer looked complete or was lost. It now ends the way a refused follow-up does, with the answer so far, `finish_reason` length and that request's token

Every continuation stream request now also gets the caller's per-request timeout, which #46349 added to the first one, so a follow-up that never answers ends at that timeout the same way

* fix(vertex_ai): keep every follow-up's citations and URL context in a continued answer

A non-streamed answer that took several Google requests kept citationMetadata and urlContextMetadata from the last request only, and left every later request's offsets counting from that request's own text. So a URL read on the first request was missing from the answer, citations from earlier requests were dropped, and a later request's citations and search supports pointed at the wrong part of the joined answer

The joined answer now keeps every request's citations (Vertex AI's `citations` and AI Studio's `citationSources`) and URL context, and shifts each later request's citation and search support offsets by the UTF-8 bytes of the answer text sent before it, thought parts excluded, since Google counts these offsets in bytes

* fix(vertex_ai): answer when a candidate sends null citationMetadata or urlContextMetadata

A Gemini candidate whose citationMetadata or urlContextMetadata was JSON null made the call fail on every Gemini model, non-streamed with a 422 or 500 and streamed with an error event, because the per-request metadata helper handed the null back as is and the caller tried to extend a list with it. A continued answer also failed when a follow-up's citation or search support came with a null startIndex

A null field is now kept as one entry on a single request, as it was before this branch, a follow-up's null URL context adds nothing to a continued answer, and a null startIndex counts from the start of that request's text. Tests cover missing, null and empty fields on one request and on a continued answer, streamed and not

* fix(vertex_ai): pass through spans with no end index and point a continued stream's citations into the whole answer

A later request's citation or search support with no endIndex made the whole non-streamed answer fail with a 500, so a span Google sends without an end index now comes back as Google sent it, the way one request for the whole answer returns it. A span with an end index but no start index still counts from 0

A continued stream handed callers each later request's citation and search support offsets counted from that request's own text. The stream now adds the answer bytes of every earlier request, so the offsets point into the whole answer like the non-streamed path and one request for the whole answer

Adds the audit's wire tests for both, which fail 24 cells before this change

* test(vertex_ai): cover a later request whose cited span starts at its first byte with no start index

Google leaves out a zero startIndex, so the second request of a continued answer can cite its own opening with only an endIndex. These wire cells check that span lands where one request for the whole answer puts it, on Gemini and Vertex AI, chat and Responses, unary and streamed

* fix(vertex_ai): log a continued answer's raw response as one object

A non-streamed answer joined from several Google requests logged its raw response as a JSON list of every request's body. The OpenTelemetry logger reads that raw response as one Gemini body to set its `llm.gemini.*` attributes, so on a continued answer it hit a validation error and set none of them

It now logs the merged body, which carries only the last request's token, and a single request still logs Google's body unchanged. The new unit test fails with the change reverted

Also adds integration cells for a cut chat stream's token chunk, an answer joined from 41 requests, a caller that gave up on a non-streamed call, counts and caps that are not numbers or are sent twice, `n` sent as true, a partial answer cached after a refused continuation, the token in another provider's cache key, and the sync SDK path with its Vertex token refetch

* test(integration): check a continued answer logs one merged raw response

A non-streamed Gemini answer that took several Google requests now hands logging one merged response, so the OpenTelemetry span carries the llm.gemini attributes. These 12 cells fail on the merge base and on the commit before that fix, and pass on the branch

* test(vertex_ai): cover a follow-up tool call and its thought signature in long decode

* test(gemini): pin that a one-request answer logs Google's body unchanged
2026-10-15 22:54:23 +00:00
Mateo Wang
84ff9f84a9
fix(anthropic): send detail on input_image parts bridged from /v1/messages to Responses (#46720)
* fix(anthropic): send detail on input_image parts bridged from /v1/messages to Responses

* fix(ui): send detail on playground Responses image parts and code snippets

* test(messages): cover image detail on the Responses bridge wire
2026-10-15 22:33:09 +00:00
Yassin Kortam
ba3fce2bda
feat(ui): scope the MCP server selector to the team, user and access groups it edits (#46565)
GET /v1/mcp/server takes an optional user_id (admin, or the user themselves) and returns the union of the servers each of that user's teams reaches plus allow_all_keys servers. A user in no team gets the unscoped list, a deleted team still listed on the user is skipped, an unknown user is a 404 and a restricted virtual key is refused like for team_id. team_id keeps precedence when both are passed.

The dashboard MCPServerSelector passes the team and user it edits (key edit sends keyData.team_id and user_id, key create sends the picked team and owner, user edit sends the user) and narrows the server options to members of the access groups selected in the same control, keeping allow_all_keys servers and servers already granted so they can still be removed.

Resolves LIT-7784

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-15 15:16:05 -07:00
Yassin Kortam
92fb55522a
feat(mcp): add mcp_disabled_capabilities to stop advertising prompts and resources (#46563)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-15 15:15:40 -07:00
Yassin Kortam
16abb004a5
feat(ui): set a search tool's cost per query from the Admin UI (#46723)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-15 15:13:14 -07:00
rameswaran-mohan
e65e5abc4e
feat(proxy): name the successor model for deprecating deployments (#41670)
* feat(proxy): name the successor model for deprecating deployments

* chore(ui): add successor_model to the generated deprecation api types

* fix(proxy): keep the cost map successor when a deployment overrides only the date

* fix(proxy): read the cost map successor from metadata.successor

The cost map schema rejects a top-level successor_model key, and curated
entries already carry the successor under metadata.successor. Read that key,
and look it up even when the cost map entry has no deprecation date, so a
deployment that sets only its own date still names the successor.

* fix(proxy): keep the first deployment when deduping, let a blank successor hide the cost map one, and name a served successor by its model group

* fix(proxy): name only the deployment's own successor_model, since cost map successors are LiteLLM ids rather than served model names

* fix(proxy): leave out a successor_model the deployment's callers cannot call, with a warning

* fix(proxy): report successor_model as the admin set it and only warn when no model matches it

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-15 14:50:14 -07:00
devin-ai-integration[bot]
da2f5a498f
feat(agents): support Databricks Agent Platform agents in the agent gateway (#45681)
* feat(agents): support Databricks Agent Platform agents in the agent gateway

Add a databricks_agent provider that speaks the MLflow ResponsesAgent wire
(Responses-shaped input, output_text items, output_text.delta SSE) to Model
Serving endpoints (/invocations or the unified /serving-endpoints/responses)
and Databricks Apps (/responses), with a PAT or OAuth M2M client credentials

Register it through the agents config block or the Admin UI agent catalog and
call it through /a2a/<agent> (message/send, message/stream) or model:
a2a/<agent> chat completions. The chat route now runs every completion-bridge
agent through its own provider, and attaches the minted Databricks OAuth token
for both bridge and URL agents. Consumed OAuth settings never reach the
provider call

* fix(agents): harden databricks_agent input, output, and OAuth handling

Non-text content parts answer 400 unless drop_params is set, a reply without
an output list answers 502, flat OAuth fields resolve an os.environ api_base,
the bridge debug log no longer echoes the request model, routing tests stub
only the HTTP boundary, and the catalog description fits the dropdown

* fix(agents): merge agent and client extra_headers on the a2a chat route, and reject an empty databricks_oauth block

* fix(agents): bridge only databricks_agent agents on the a2a chat route, keep the card url over a client api_base, and require workspace_url for a Databricks App

* refactor(agents): read the a2a chat bridge providers from a constants set instead of branching on a provider name

* fix(databricks_agent): let the configured credential win, reject malformed client extra_headers, and keep a missing endpoint name empty

* test(agents): drive the flat OAuth a2a send through the real bridge

* fix(agents): keep an empty databricks_oauth block ignored on url agents, as before this change

* fix(agents): accept bare App hosts, env service principals, and os.environ refs on Databricks agents

* fix(databricks_agent): address review caveats on chat bridging, params, and auth

Map max_tokens and max_completion_tokens to max_output_tokens and pass
temperature, top_p, and user, so /v1/messages works without drop_params.
Send A2A message metadata only with string values, answering 400 or
dropping the rest under drop_params. Join message items with a blank line
in both stream and non-stream replies. Cache the env service principal
token until shortly before it expires. Keep databricks_agent replies out
of the response cache.

On the chat bridge, let request fields win over the agent's stored params
while pinning the agent's api_base and api_key, and apply the agent's
static_headers between client and agent extra_headers, as /a2a does.
Resolve os.environ/ only for databricks_agent api_base and api_key instead
of every bridge provider. Ignore an unusable databricks_oauth block on URL
agents on chat, as at the merge base, instead of answering 400.

* test(internal_context): list the databricks agent token cache as process-local

* fix(databricks_agent): keep the agent's endpoint, input, and target out of client control

extra_body model and input answer 400, or are dropped under drop_params.
A client api_base or base_url never replaces the agent's card URL on the
bridge. Chat to a URL agent keeps its merge-base call, and an incomplete
databricks_oauth block on a URL agent keeps its merge-base ValueError.

* test(databricks): expect the merge-base 400 for a client api_base on agent chat

* fix(databricks): keep agent-owned inputs and identity headers out of a chat caller's reach

* fix(databricks): bound the agent-over-caller merge to two levels so it needs no recursion

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-15 12:42:06 -07:00
devin-ai-integration[bot]
e344dbd8db
feat(proxy): split Azure PTU deployments across teams with per-team PTU ceilings and PTU-hour reporting (#43043)
* feat(proxy): split Azure PTU deployments across teams by share

A PTU deployment's model_info takes ptu_shares (team_id -> whole PTUs adding up to
ptu_count) in place of a single team_id. With LITELLM_ENABLE_PTU_COST_ATTRIBUTION on,
routing hides the deployment from teams holding no share, the v3 limiter enforces each
share as a per-team TPM ceiling from Azure's per-model TPM-per-PTU sizing, the daily
flat-cost rollup splits the hourly cost by share, and /team/daily/activity plus the Team
Usage page report PTU-hours next to tokens.

* fix(proxy): weight cached input in PTU ceilings and size team-scoped deployments by public name

The PTU flag is read from the process environment on every check instead of
through the secret-manager path, so a proxy with a hosted secret manager does
not pay a round trip per request. A team's ceiling now carries the model's
cached-input ratio and settlement charges cache reads at that ratio, matching
how Azure sizes a PTU. A deployment registered for one team is found by its
team_public_model_name for PTU-hours and the ceiling, since its model_name is
rewritten to a unique internal name. ptu_shares are StrictInt on ModelInfo and
parsed_ptu_shares refuses booleans, strings, and non-string team ids, a
fractional ptu_count reserves nothing, and models_processed counts distinct
deployments rather than holdings.

* fix(proxy): reserve PTU ceilings in normalized tokens, keep share routing flag-independent, and warn only shared or Azure reservations

* fix(proxy): size the uncapped output floor against the PTU share in raw output tokens and escape the exported team id

* fix(ptu): refuse ptu_shares naming a team that does not exist

* refactor(ptu): keep Azure detection under llms/azure and check share teams with one query

* fix(proxy): drop the reverted TeamIdSearchFilter import after merging main

Main reverted the team key search change that added TeamIdSearchFilter, so the PTU share team check types its where clause locally

* fix(proxy): chunk the ptu_shares team lookup under the IN-list bound

* fix(proxy): count a request naming a shared PTU deployment directly against its group's ceiling

* fix(proxy): resolve every name a shared PTU deployment is served under to its group's ceiling

* fix(ptu): refuse ptu_shares declared without ptu_count and cost_per_ptu_per_hour

* fix(ui): seed total_ptu_hours in the empty daily activity metadata

* fix(ptu): size usage rows by the name the request used

* fix(ptu): size a team's usage rows by the deployment it is served from

* fix(ptu): never size a team's usage row by another team's reservation

* fix(ptu): reserve an uncapped request against the cap the proxy writes, not its prompt length

* fix(ptu): read a share map only through the terms registration requires

A ptu_shares map stored without ptu_count, cost_per_ptu_per_hour and
ptu_effective_from, or one that does not add up to ptu_count, is refused at
registration but was enforced once loaded: every other team got the 400 and
the share holder a ceiling while no flat cost accrued. _deployment_shares now
reads the shares through ptu_terms, so such a row reserves nothing, the same
as before the map was read.

* fix(ptu): keep a declared split as the access rule and check a config split at boot with the flag off

6fb24d1d read ptu_shares through the pricing terms so a termless map reserved
nothing, but with the flag off the router skips ptu_config_error for config.yaml
entries, so such an entry loaded and served every team, against the rule that a
declared split restricts access whether or not cost attribution is on.
_deployment_shares reads the declared map again, and the router now raises
ptu_config_error for a config-sourced entry declaring ptu_shares regardless of
the flag, so a malformed split is refused at boot instead of being honoured or
ignored silently.

* fix(ptu): charge wildcard-served names, pass-through usage, and team-less settlements against the team's PTU ceiling

A name served by a wildcard route now resolves to the shared deployment behind it, so the
limiter's ceiling and the team daily activity sizing both find it. Pass-through responses
settle the PTU counter from combined_usage_object in Azure-normalized tokens, and a
reservation taken at admission is settled even when the success metadata lost the team.

* fix(ptu): resolve a wildcard-served name to the one pattern the router picks, and a deployment id before any wildcard

A team's wildcard reservation behind an open proxy-wide * route was charged and sized for traffic the * route served, and naming a shared deployment by id next to a catch-all wildcard escaped its per-team ceiling. Router.wildcard_route_deployments now owns the most-specific-proxy-wide-else-team precedence, and both the limiter and the consumption page resolve through it after listed rows and deployment ids.

* test(router): pin the wildcard route precedence that PTU sizing and the router share

* fix(proxy): accept whole-number PTU shares written as floats, as ptu_count already does

A share of 2.0 now reads as 2 on POST /model/new and in config.yaml, where it was
refused with a 422 and dropped at load. Fractional, infinite, boolean and string
shares stay refused. Adds unit tests for the malformed-rate, provider-over-prefix,
zero-token Responses usage, fractional-count and sizing-warning paths.

* fix(proxy): settle team PTU reservations in their own minute and keep billed PTU days when shares move

* fix(proxy): write each PTU deployment day's team charges in one transaction

The backfill counts a deployment day as priced once any team's row exists for it,
so a rollup that wrote team A's share and lost team C's left team C unbilled for
good. Each deployment day's team rows now go through one Prisma batch with the
retry around the whole batch, so a day is fully written or not written at all and
the next backfill prices it for every team.

* fix(proxy): read the PTU flag from a secret manager, serve share holders from their share, and refuse unfittable calls and share-holding team deletes

* test(router): list the cached PTU ceiling helpers in the router coverage check

* fix(proxy): read the PTU flag only for share holders, guard org and SCIM team deletes, keep share holders on their shared deployment through retries and the scheduler, and date PTU-hours by the terms in force

* fix(proxy): bill a rerun PTU day to the current share holders alone

* fix(proxy): replace a PTU day's rows by day and deployment, not a negated team list

* fix(proxy): read the PTU flag from the secret manager first, like get_secret

* fix(router): scope retry health lookups to the caller only for PTU share holders

* fix(router): move the share-holder retry check into ptu_shares

Adds ptu_hours and total_ptu_hours to the daily activity integration goldens and fixtures, since every route now reports them

* test(integration): cover PTU shares across routing, delete guards, cost attribution and config regex timing

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-15 18:44:30 +00:00
Thijmen Stavenuiter
a76e723b26
feat(proxy): report when each per-model budget window resets (#45797)
* feat(proxy): report when each per-model budget window resets

model_max_budget_usage on /key/info, /user/info and /team/info now carries budget_reset_at per model, the open window's start plus its period, or null when no window is open. The window start is read in the same batched lookup as the spend counter

* fix(proxy): count a monthly budget window from its own start when reporting its reset

duration_in_seconds measures Nmo from now, so a window opened on January 15 and read on February 10 reported February 12, then null while the counter still refused requests until February 15. duration_in_seconds_from measures the month from the window start, and duration_in_seconds now shares that month arithmetic

* fix(proxy): measure months from timezone-aware datetimes and assert whole usage values

datetime.fromtimestamp without a tz tripped DTZ006. The month arithmetic now runs on an aware local datetime, which keeps the same wall-clock result. The duration tests move to parametrized functions, since subTest payloads carrying datetimes cannot cross the xdist worker boundary, and the new usage tests compare the whole entry

* fix(budgets): end a monthly budget window a calendar month after it opens

Each charge recounted the month from the charge date, so a monthly window
opened in a month of another length expired days before or after the reset
/key/info reports. The window is now measured from its start, by the same
helper the usage report uses. A period ending after year 9999 reports no
reset instead of failing the read.

* fix(budgets): count monthly budget windows in UTC for every router budget

Months were counted on the host's local calendar, so replicas with different TZ
settings disagreed on when a monthly window ends. Provider, deployment, tag and
per-model budgets now all measure a window from its start in UTC, and a test
covers the router budgets. A reset past year 9999 is documented as null.

* test(budgets): cover monthly tag budgets in the router window test

* test(budgets): add integration cells for budget_reset_at and monthly budget windows

* test(budgets): find a monthly window in another span on every calendar day

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-15 10:56:45 -07:00
Oliver Jensen
84e8204d7f
fix(proxy): scope /spend/tags aggregates to the caller's own and permitted-team logs (#46194)
* fix(proxy): scope /spend/tags aggregates to the caller's own and permitted-team logs

Non-admin callers previously received the proxy-wide per-tag spend
aggregation. The endpoint now resolves the same read scope as
/spend/logs/ui and /spend/logs/session/ui, and the query honors the
start_date/end_date parameters it previously accepted but ignored.

* fix(proxy): use Annotated auth dependency on /spend/tags and sync dashboard API types

* fix(proxy): bind /spend/tags dates as UTC datetimes, not session-timezone strings

Raw YYYY-MM-DD strings cast to timestamptz are read in the database
session timezone, shifting the day window by that offset. Parse them
to UTC-aware datetimes first, like every sibling spend query in this
file, and reject malformed dates with a 400 before they reach the
database.
2026-10-15 10:09:21 +02:00
Mateo Wang
3cc9e7ef8e
perf(proxy): cache the public provider and agent field endpoints (#46617)
/public/providers/fields re-read and re-parsed the bundled 85K
provider_create_fields.json on every request, and /public/agents/fields did the
same for that file plus agent_create_fields.json. Both are unauthenticated
public endpoints, and both did the work inside an `async def`, so the read and
the parse ran on the event loop and serialized against every other in-flight
request. Measured against the bundled files, that is roughly 0.40 ms of blocking
work per request for the provider endpoint and 0.45 ms for the agent endpoint

Both now memoize with `lru_cache`, which is what `/public/endpoints` in the same
module already does through its `_cached_endpoints` global, so each file is read
once per process rather than once per request

The agent endpoint mutated the dicts it had just loaded, appending inherited
provider credential fields and popping `inherit_credentials_from_provider`. That
is only safe while the data is re-read every time, so caching it meant making the
merge pure: `_agent_with_inherited_credentials` builds a new mapping per agent,
and the cached value is the merged result rather than the raw file contents.
Responses are byte identical before and after, checked on both endpoints against
a live proxy

The bundled records are typed as Mapping[str, object] and narrowed with
isinstance at the point of use, rather than typing.Any, which ruff-strict bans

Side effects: 3 of the repo's ASYNC230 violations go away, and basedpyright
errors in the file drop from 63 to 52

Rebased onto main after /public/providers/fields started validating the bundled
JSON through a pydantic adapter on every request. That validation now runs once
and the validated records are cached; the agent endpoint keeps merging the raw
provider records, so both responses stay byte identical to main.

Co-authored-by: Alok Sharma <alok44170@gmail.com>
2026-10-15 00:13:07 -07:00
TheMentalCoding
ca04c4909e
feat(ui): filter active and paused model deployments (#43710)
* feat(ui): status filter + sort and provider-aware search on Models & Endpoints

The models table mixes active and paused deployments with no way to tell
them apart beyond the pause toggle, and the drawer only filters by public
model name and access group.

Backend (/v2/model/info):
- new optional `blocked` query param to filter by routing status
- new `blocked` sort field (asc = active first); the existing `status`
  field keeps sorting by config-vs-DB source
- `search` now also matches litellm_params.model, so typing a provider
  or upstream model id (e.g. "nvidia_nim", "openai/gpt-4") finds
  deployments whose public name doesn't mention them

Frontend (Models & Endpoints):
- visible Status column with Active/Paused badges, sortable
  (active first on asc)
- Status filter (All/Active/Paused) in the Filters drawer, wired to the
  new server param through URL state

The routing-status filter ignores non-bool sentinels the same way
exclude_auto_routers does, so direct calls bypassing FastAPI keep their
no-filter behavior.

* fix(ui): inverted routing-status filter mapping and compact Status column

Selecting "Active" in the Filters drawer sent blocked=true to the server,
showing the paused deployments instead of the active ones (and vice
versa). The mapping now lives in routingStatusToBlocked() with unit
tests, and the Status column is narrower so the row actions stay in
view.

* fix(ui): pin the Actions column to the right on the models table

With the new Status column the table overflows on narrower screens and
the pause/resume switches (last column) scroll out of view. Pinning the
Actions column keeps them visible; uses the existing DataTable
meta.pinned support (same pattern as MemberTable).

* refactor: readable db search where-condition and ruff-format clean

* fix: apply routing-status filter inside the search flow and regen API types

Greptile flagged (P1): with search + a status filter combined, the
bounded DB fetch ran before status filtering, so matches of the other
status could consume the page budget and leave short pages / stale
totals.

- extract _matches_routing_status() (sentinel-safe) and apply it to the
  router-side matches inside _apply_search_filter_to_models
- push the status condition into the DB where clause of
  _fetch_db_models_for_search, so rows of the other status never count
  against the bounded fetch
- endpoint passes blocked through to the search flow; tests cover the
  combined search+status case and the generated where clause
- regenerate ui/litellm-dashboard/src/lib/http/schema.d.ts for the new
  query param
- update useModels.test.ts expectations for the new trailing argument

* fix(proxy): keep the flat DB where shape for single-condition searches

The upstream CI test
test_apply_search_filter_honours_exact_model_name_in_db_query pins the
Prisma where shape: exact model_name stays a top-level key, and plain
substring search keeps {model_name: {contains, mode}} at the top level.
Only genuinely multi-condition queries (status filter and/or router-id
exclusion) now wrap in AND.

* fix(proxy): avoid reassigning a Final variable in the db where builder

basedpyright's reportGeneralTypeIssues budget counts the reassignment of
the Final-annotated db_where_condition; express it as a single
conditional expression instead.

* chore(deps): bump pypdf to 6.19.0 to clear osv-scan

pypdf 6.16.2 has 8 High CVEs (GHSA-5jq2-8x83-x246 and aliases) fixed in
6.17.0-6.19.0; bump past all of them

* Revert "chore(deps): bump pypdf to 6.19.0 to clear osv-scan"

This reverts commit 0e2528123db7ab23304ae20f6a4c0961d0e2d073.

* fix(proxy): keep LIT002 budget flat in the model search where-builder

Extract the dynamic Prisma where-condition fragments into module-level
helpers so every dict starts on a short line, split the litellm_params
fallback lookup into a Final local, and annotate suppressions on the
construct start lines the checker attributes

* chore(tests): repoint unbounded-IN baseline at the extracted where-builder helper

The bounded-router-id filter moved from _fetch_db_models_for_search into
_not_in_router_ids, so the old baseline key is stale and the new helper
site is the finding to record

* fix(proxy): avoid searching encrypted deployment model names

* refactor(ui): keep model routing status as a filter only

* docs(api): regenerate model info sorting description

* fix(api): remove remaining paused status sort references

---------

Co-authored-by: thementalcoding <312162298+thementalcoding@users.noreply.github.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-14 23:19:59 -07:00
Mateo Wang
3fec5075fa
feat(typesafe): support Jev 1.14 images, box questions, and long-context headers (#46596)
* feat(typesafe): support Jev 1.14 images, box questions, and long-context headers

* fix(cost): drop the typesafe/jev-1.14.0 entry main already prices

* fix(typesafe): send images only with the images header and box questions only to models that answer them

TypeSafe takes assets and input_image parts only when the request carries
TypeSafe-Danger-Zone-Images: true, so without it they follow drop_params like
any other provider that cannot take images. Box support now comes from a
supports_box_questions cost-map flag set on jev-1.14.0 and jev-preview, so
jev-1.13.0 and jev-latest refuse or drop a box question before the call. A
malformed provider_specific_header answers 400 instead of 500.

* fix(typesafe): allow null box confidence and validate asset references and base64

* fix(typesafe): gate box questions on the images header, drop unreadable assets under drop_params, forward opt-in headers on the pass-through

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-14 22:29:48 -07:00
Kerry Lu
efd8274315
feat(xai): bill grok voice realtime sessions (#46600)
* feat(xai): bill grok voice realtime sessions

* chore: revert unrelated formatting churn in test_cost_calculator.py

* refactor(xai): tidy realtime usage meter and tests

* fix(xai): read push-to-talk mode from client session.update only

* fix(xai): clamp decoded audio bytes and lock push-to-talk before content

---------

Co-authored-by: kerry <kerry@berri.ai>
2026-10-15 05:27:02 +00:00
Kerry Lu
0d741a9816
fix: index streamed thinking_blocks so the OpenAI SDK .stream() accumulator accepts them (#46611)
* fix: index streamed thinking_blocks so the OpenAI SDK .stream() accumulator accepts them

* fix: satisfy the type discipline and basedpyright gates for the thinking block index

* chore: sync the OpenAPI snapshot and schema.d.ts with the thinking block index field

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-14 22:21:45 -07:00
Mateo Wang
d6e6c6f26c
fix(responses): map Anthropic tool search to OpenAI server tool search (#46441)
* fix(responses): translate OpenAI tool_search to Anthropic tool search for Claude models

* fix(responses): define the tool_search_call output item locally for older openai SDKs

* fix(responses): gate tool search on the Anthropic request builder and validate deferred tool fields

Bedrock Converse builds its own request and has no tool search, so the bridge now drops the tool_search tool there with a warning instead of sending a tool the transport loses, while Bedrock invoke, Anthropic, and Vertex keep it. A replayed tool_search_call with no stored result is dropped from the Anthropic request so a previous_response_id follow-up no longer answers 400. Only srvtoolu_ ids whose name is one of Anthropic's search tools count as search calls, so a caller function named like one stays a function call. A non-boolean defer_loading or a non-list allowed_callers now answers 400 instead of 500

* fix(responses): keep an answered search call on replay and send tools undeferred when no search tool goes out

A replayed Claude tool search call that the caller answered with a tool message kept its
tool_result in the user turn while its tool_use was dropped from the assistant turn, so
Anthropic answered 400 on the OpenAI-SDK tool loop. The search call is now dropped only when
no tool message answers it.

The Responses bridge now strips defer_loading from every tool whenever no tool search tool is
sent, whether the caller sent none or the route cannot run it, since a deferred tool with no
search tool can never be loaded. Namespace members had started carrying the flag to every
bridge provider, which providers that reject unknown tool keys answered 422.

The audit round-1 integration cells for the bridge wire and chaos paths ride in this commit.

* fix(responses): gate only the OpenAI tool_search shape and keep native Anthropic search tools intact

A native tool_search_tool_regex_20251119 or tool_search_tool_bm25_20251119 tool the caller sends through /v1/responses passes through the bridge on every route as before, and the other tools keep defer_loading whenever any Anthropic search tool goes out. The route gate now drops the OpenAI tool_search item before translation instead of filtering the translated regex tool, so bm25 counts as a search tool. answered_tool_call_ids walks the history with islice instead of a slice copy. The tool_search_call output item drops its created_by field, which OpenAI rejects on a replayed input item and the streaming item never carried

* fix(responses): drop malformed tool flags under drop_params and keep stored search calls off non-Anthropic follow-ups

With drop_params on, a non-boolean defer_loading or a non-list allowed_callers on a Responses API tool (or a namespace member) is dropped before the bridge instead of answering 400, so only a request without drop_params gets the 400. A custom tool now carries defer_loading to the chat tool like a function tool does

A previous_response_id follow-up routed to a provider that does not build an Anthropic request no longer replays the stored Claude search call (srvtoolu_ id) as a client tool call. Mistral answered that history 400 "Unexpected tool call id". An assistant turn that held only the search call is dropped there. The Anthropic route keeps the call, since its factory pairs it with the stored result

The tool_search_call output model is renamed OutputToolSearchCall so the proxy's OpenAPI schema keeps the OpenAI SDK's ResponseToolSearchCall name. answered_tool_call_ids reads the history by index, so the lookup is linear in the history length. The streaming done event reads the response it is given, which is the same object it used to read from the iterator

* fix(responses): validate function allowed_callers, send one search tool, and keep replays clean off Anthropic

A function tool whose allowed_callers is not a list of strings now answers 400
instead of 500. The guardrail scan reads tools without malformed flags, so the
route decides between a 400 and a drop under drop_params. When the caller sends
a native Anthropic search tool beside tool_search, only the native one goes out.
Databricks Claude no longer counts as an Anthropic request route.

A previous_response_id follow-up off Anthropic also drops a search call the
client replayed as a function_call with a srvtoolu_ id, and its output. The
answered tool id scan in the Anthropic message factory runs once per assistant
run instead of once per message. A null created_by on tool_search_call and
tool_search_output output items is left out of the serialized response, so a
cache hit returns the same keys as the first call.

* fix(guardrails): keep OpenAI tool_search out of the Responses guardrail tool scan

The scan translated tool_search into Anthropic's regex search tool on every
route, so a guardrail returning only function tools removed tool_search from
native OpenAI requests and OpenAI answered 400 for the deferred tools

* fix(anthropic): drop answered tool search replays beside client calls, place system before dropped search turns, flag tool search on azure_ai

* fix(anthropic): keep answered tool search replays paired with their answers

* fix(anthropic): place mid-conversation system messages in one pass and scan tool_search_call in output guardrails

* fix(guardrails): send empty incomplete tool_search_call arguments when a rewrite is not a JSON object

* fix(responses): keep the caller's defer_loading on Claude routes when no server tool_search is sent

* fix(guardrails): deliver streamed rewrites when OpenAI's tool_search_call has no call_id

* fix(responses): answer 400 to a tool_search execution that is not server or client

OpenAI's own Responses API answers 400 invalid_value to it. With drop_params the malformed
execution is dropped like the other malformed tool flags, which leaves a server search

* test(responses): cover the 400 and drop_params paths for a malformed tool_search execution

* fix(responses): map Anthropic tool search to OpenAI server tool search

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

* fix(responses): sync generated tool search API types

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

* fix(responses): type tool search mapping predicates

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

* fix(responses): validate tool-search mappings

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

* fix(responses): use the openai SDK ToolSearchToolParam for the mapped tool search

* fix(responses): keep deferred chat tools beside a tool search on the responses bridge

Restore the hand-rolled OpenAIToolSearchToolParam, since openai 2.20.0 (the
supported floor) has no ToolSearchToolParam, and carry defer_loading from chat
function tools onto the Responses request when a tool search tool is present,
which OpenAI requires for tool_search

* fix(responses): leave the bridge's defer_loading carry to its own PR

#46439 owns keeping defer_loading on chat function tools sent through the
Responses bridge, so this PR keeps only the tool search mapping and the
openai 2.20.0 compatible tool type

* fix(openai): keep tool search mapping to the OpenAI API and drop it where the model or history cannot take it

* fix(openai): narrow the tool search drop to Anthropic's search tool, drop it when nothing is deferred, and run the wire tests against the OpenAI host

* fix(openai): pass malformed tools to OpenAI as sent and keep a caller's own tool_search under drop_params

* perf(openai): scan Responses history for Claude search calls only when Anthropic's search tool is sent

* test(responses): cover the Anthropic tool search mapping to OpenAI Responses across routes, settings, and chaos

* test(integration): define the fallback cell's deployments in the gateway config so both workers load them

* test(integration): keep the worker-kill burst held past the replacement wait

* fix(responses bridge): count an Anthropic search tool as hosted tool search on the chat bridge

main's #46439 refuses defer_loading when no tool_search tool is in the request, and the chat bridge only counted {"type": "tool_search"}, so the Anthropic bm25 or regex tool this PR maps to OpenAI's tool search was refused before the mapping ran

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

* test(integration): expect the chat bridge to map the search tool now that defer_loading survives

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-15 05:13:56 +00:00
devin-ai-integration[bot]
314f17986a
fix(responses): translate OpenAI tool_search to Anthropic tool search for Claude models (#45882)
* fix(responses): translate OpenAI tool_search to Anthropic tool search for Claude models

* fix(responses): define the tool_search_call output item locally for older openai SDKs

* fix(responses): gate tool search on the Anthropic request builder and validate deferred tool fields

Bedrock Converse builds its own request and has no tool search, so the bridge now drops the tool_search tool there with a warning instead of sending a tool the transport loses, while Bedrock invoke, Anthropic, and Vertex keep it. A replayed tool_search_call with no stored result is dropped from the Anthropic request so a previous_response_id follow-up no longer answers 400. Only srvtoolu_ ids whose name is one of Anthropic's search tools count as search calls, so a caller function named like one stays a function call. A non-boolean defer_loading or a non-list allowed_callers now answers 400 instead of 500

* fix(responses): keep an answered search call on replay and send tools undeferred when no search tool goes out

A replayed Claude tool search call that the caller answered with a tool message kept its
tool_result in the user turn while its tool_use was dropped from the assistant turn, so
Anthropic answered 400 on the OpenAI-SDK tool loop. The search call is now dropped only when
no tool message answers it.

The Responses bridge now strips defer_loading from every tool whenever no tool search tool is
sent, whether the caller sent none or the route cannot run it, since a deferred tool with no
search tool can never be loaded. Namespace members had started carrying the flag to every
bridge provider, which providers that reject unknown tool keys answered 422.

The audit round-1 integration cells for the bridge wire and chaos paths ride in this commit.

* fix(responses): gate only the OpenAI tool_search shape and keep native Anthropic search tools intact

A native tool_search_tool_regex_20251119 or tool_search_tool_bm25_20251119 tool the caller sends through /v1/responses passes through the bridge on every route as before, and the other tools keep defer_loading whenever any Anthropic search tool goes out. The route gate now drops the OpenAI tool_search item before translation instead of filtering the translated regex tool, so bm25 counts as a search tool. answered_tool_call_ids walks the history with islice instead of a slice copy. The tool_search_call output item drops its created_by field, which OpenAI rejects on a replayed input item and the streaming item never carried

* fix(responses): drop malformed tool flags under drop_params and keep stored search calls off non-Anthropic follow-ups

With drop_params on, a non-boolean defer_loading or a non-list allowed_callers on a Responses API tool (or a namespace member) is dropped before the bridge instead of answering 400, so only a request without drop_params gets the 400. A custom tool now carries defer_loading to the chat tool like a function tool does

A previous_response_id follow-up routed to a provider that does not build an Anthropic request no longer replays the stored Claude search call (srvtoolu_ id) as a client tool call. Mistral answered that history 400 "Unexpected tool call id". An assistant turn that held only the search call is dropped there. The Anthropic route keeps the call, since its factory pairs it with the stored result

The tool_search_call output model is renamed OutputToolSearchCall so the proxy's OpenAPI schema keeps the OpenAI SDK's ResponseToolSearchCall name. answered_tool_call_ids reads the history by index, so the lookup is linear in the history length. The streaming done event reads the response it is given, which is the same object it used to read from the iterator

* fix(responses): validate function allowed_callers, send one search tool, and keep replays clean off Anthropic

A function tool whose allowed_callers is not a list of strings now answers 400
instead of 500. The guardrail scan reads tools without malformed flags, so the
route decides between a 400 and a drop under drop_params. When the caller sends
a native Anthropic search tool beside tool_search, only the native one goes out.
Databricks Claude no longer counts as an Anthropic request route.

A previous_response_id follow-up off Anthropic also drops a search call the
client replayed as a function_call with a srvtoolu_ id, and its output. The
answered tool id scan in the Anthropic message factory runs once per assistant
run instead of once per message. A null created_by on tool_search_call and
tool_search_output output items is left out of the serialized response, so a
cache hit returns the same keys as the first call.

* fix(guardrails): keep OpenAI tool_search out of the Responses guardrail tool scan

The scan translated tool_search into Anthropic's regex search tool on every
route, so a guardrail returning only function tools removed tool_search from
native OpenAI requests and OpenAI answered 400 for the deferred tools

* fix(anthropic): drop answered tool search replays beside client calls, place system before dropped search turns, flag tool search on azure_ai

* fix(anthropic): keep answered tool search replays paired with their answers

* fix(anthropic): place mid-conversation system messages in one pass and scan tool_search_call in output guardrails

* fix(guardrails): send empty incomplete tool_search_call arguments when a rewrite is not a JSON object

* fix(responses): keep the caller's defer_loading on Claude routes when no server tool_search is sent

* fix(guardrails): deliver streamed rewrites when OpenAI's tool_search_call has no call_id

* fix(responses): answer 400 to a tool_search execution that is not server or client

OpenAI's own Responses API answers 400 invalid_value to it. With drop_params the malformed
execution is dropped like the other malformed tool flags, which leaves a server search

* test(responses): cover the 400 and drop_params paths for a malformed tool_search execution

* test(responses): cover a Claude search call replayed to Bedrock Converse with cache marks

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: mateo <mateo@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-14 20:42:22 -07:00
yuneng-jiang
5088b0cea6
feat(ui): replace deployment warning banners with a health icon, alert banner and panel (#46498)
* feat(ui): replace deployment warning banners with a health icon, critical row and panel

The debug, no Redis, env credential login and license expiry banners are gone.
Proxy admins and proxy admin viewers now get a shield icon in the header with
an active warning count, one row under the header for the top critical warning,
and a side panel where proxy admins snooze, ignore or restore each warning for
every admin of the proxy

* test(ui): cover deployment health wiring and warning content, drop unused license tier helper

Both dashboard shells are checked end to end for the icon, critical row and
panel, and the warning catalog is checked for its license wording, fix text
and links. getLicenseExpiryTier and its thresholds lost their last caller
with the license banner, and the proxy now decides severity

* fix(ui): wrap long setting names in deployment warning fix text on narrow screens

* style(ui): restyle deployment health to the dashboard design system

The critical row now follows the deployment alert banner: tone tint, bold lead, Review as a link and +N more on the right. The panel drops nested cards for ruled rows with severity icons, row actions move into a ⋯ menu, ignored rows show only who snoozed them and until when, and loading uses skeleton rows. The header shield matches the bell and carries a small count pill

* style(ui): rebuild deployment health on shadcn primitives with the reviewed design

The banner shows the most severe active alert by title with a Review link, the header shield carries a count badge, and the panel groups issues into collapsible Critical, Warnings, and Ignored and snoozed sections. Each issue is a tinted Alert with a docs link and a Snooze menu, snoozed issues keep the full layout with a status badge and Restore, and the empty state uses the shadcn Empty primitive

* chore(ui): exempt shadcn primitives from the PascalCase filename rule

The shadcn CLI writes kebab-case files under components/ui, so every primitive needed a suppression entry and each new one failed lint. Exempt the folder and drop the 34 entries it replaces

* fix(ui): let long setting names in deployment warnings wrap inside the alert on phones

The shadcn Alert lays out its text in a 1fr grid column, whose minimum width is the longest unbreakable run. overflow-wrap break-word does not lower that minimum, so a long setting name pushed the panel wider than a 390px screen. overflow-wrap anywhere does
2026-10-14 18:32:40 -07:00
litellm-stampbot
4d5291885f
fix(cost): price AgentCore web search at $7 per 1,000 queries (#46571)
* fix(cost): price AgentCore web search at $7 per 1,000 queries

* feat(search): honor input_cost_per_query set on a search tool (#46560)

* feat(search): honor input_cost_per_query set on a search tool

* feat(search): record the search tool price on proxy-created logging objects

* chore(search): type the search tool price lookups and shard tests/unit/search

* chore(openapi): regenerate snapshot and ui types for search tool input_cost_per_query

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Kerry Lu <klu@berri.ai>
2026-10-14 18:30:39 -07:00
yucheng-berri
1f4ef4b7d1
feat(proxy): general_settings.disable_requester_ip_address_logging keeps caller IP out of spend logs (#45804)
* feat(proxy): general_settings.disable_requester_ip_address_logging keeps caller IP out of spend logs

When the setting is on, spend log rows store requester_ip_address as null in
the column, the metadata and the stored request (which also drops client-IP
forwarding headers), and IP addresses in stored error messages are masked, so
the Logs page shows no caller IP. The setting is read the way the settings API
validates it. Capture is unchanged, so guardrails, logging callbacks and
metrics still receive the IP. Toggleable from the Admin UI General Settings.

Resolves LIT-7442

* fix(proxy): mask IPv6 addresses that end or start with :: across eight groups

The masking pattern allowed seven colons, so valid forms like 1:2:3:4:5:6:7::
and ::1:2:3:4:5:6:7 stayed in stored error text with
disable_requester_ip_address_logging on. Also lists the request metadata walker
in the recursion check, since it only walks the tree the spend log sanitizer
already returned.

* fix(proxy): mask IPs in every stored header value and after labels or underscores

With disable_requester_ip_address_logging on, client IPs carried in headers outside the forwarding list (do-connecting-ip, incap-client-ip, custom names) and addresses written as host:2001:db8::2, peer_2001:db8::2, socket_192.0.2.1 or an unbracketed IPv6 with a port were still stored in spend logs.

* fix(proxy): mask request header IPs before long values are truncated

A header value longer than the spend log string limit kept the part of an IP that fell before the truncation cut. Request headers in metadata and requester_metadata are now masked before the body is truncated, like error text already was. IPv4 followed by a dotted port (192.0.2.1.443) is masked again.

* fix(proxy): mask the IP piece a header truncation cut leaves instead of pre-masking headers

Masking header values before truncation scanned the whole client value,
including text the cut drops, and walked hook-made header cycles. Mask after
truncation instead, then mask the hex and digit run touching each side of the
truncation marker when it is short enough to be part of an IP address. Every
nested headers mapping gets the same treatment.

* docs(proxy): say the requester IP setting masks IPs in other stored header values

* fix(proxy): mask stored error text after truncation and harden the cut-piece pass

Error messages and tracebacks were regex-scanned in full before the DB cap,
which costs seconds on multi-megabyte provider errors. Mask them after the
cap like header values, with the same pass over the run beside each
truncation marker. That pass now handles every marker, skips long hex tokens
so an address beside one is still masked, keeps at most one address length,
and also masks colon-only IPv6 groups.

* fix(proxy): drop the requester IP from the originating request an auto router classifier row stores

* fix(proxy): drop the requester IP from the classifier input an auto router classifier row stores

* refactor(proxy): mask stored IPs with the content filter's ipv4 and ipv6 patterns

The requester IP setting now reuses the content filter guardrail's prebuilt
ipv4 and ipv6 patterns instead of its own address regexes, so spend logs mask
what that guardrail masks. Addresses glued to a label or underscore stay as
the content filter leaves them.

* fix(proxy): mask stored IPs with the spend-log matcher again

The content filter's ipv4 and ipv6 patterns left labelled IPv6 (host:2001:db8::2),
unbracketed IPv6 with a port and IPv4 after an underscore in stored error text and
header values, and masked parts of text like std::io::Error. Restore the
address-validating matcher and its regression cases.

This reverts commit 79537a8aa5.

---------

Co-authored-by: yucheng-berri <293926326+yucheng-berri@users.noreply.github.com>
2026-10-14 18:16:40 -07:00
airia-open-source
1fd958cc1f
feat(ui): add the Airia guardrail to the Guardrail Garden with its logo (#46562)
The Airia guardrail (#40784) shipped without a dashboard presence: no card in the Guardrail
Garden and no logo next to its name in the provider list and guardrail table, which every other
partner guardrail has. This adds both, following the Wingback and Conduct entries:

- `airia.svg`, Airia's brand mark from AiriaLLC/airia-public-assets, under the bundled logos.
- A `guardrailLogoMap` entry keyed by the config model's `ui_friendly_name` ("Airia Guardrail"),
  which is what the provider dropdown and table look the logo up by.
- A partner card and an `airia` preset. The preset's `provider` is the camel-cased registry key
  ("Airia"), the form the dashboard derives for every provider it reads from
  `/guardrails/ui/provider_specific_params`. The preset seeds both `pre_call` and `post_call`,
  as the docs example does, so a response-side redaction is not silently left out.
- The logo-file assertion in `guardrail_garden_data.test.ts` covers the new card.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-14 18:09:20 -07:00
yuneng-jiang
9271edc734
feat(proxy): let proxy admins open team metadata to team admins, keeping proxy settings with proxy admins (#46506)
* feat(proxy): let proxy admins open team member defaults to team admins

* fix(proxy): keep a team admin's never-resets duration on a team's first member budget

* test(proxy): pin the existing-row and stored never-resets cases for member defaults

* feat(proxy): let proxy admins open soft_budget to team admins

* feat(proxy): let proxy admins open team metadata to team admins, keeping proxy settings with proxy admins

* test(ui): name expected objects in team admin edit access tests

Keeps local/no-large-inline-object-arg within its eslint budget

* fix(proxy): compare team metadata without recursion and count key presence

A proxy-controlled metadata key that appears or disappears with a null value
is now a change, and the JSON comparison no longer recurses

* fix(proxy): compare team metadata numbers exactly

Numbers are compared as decimals, so integers past float precision and close
floats are no longer treated as equal
2026-10-14 23:51:44 +00:00
Yassin Kortam
10e4ecc313
feat(access-groups): grant Claude Code skills through access groups (#46294)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-14 23:18:50 +00:00
Yassin Kortam
f76f7e1368
feat(helm): support a separate hostname per component in the componentized deploy (#46360)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-14 14:56:36 -07:00
Yassin Kortam
9f6379223c
feat(access-groups): per-server MCP tool permissions on access groups (#46300)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-14 14:50:55 -07:00