Gemini returns each thoughtSignature on exactly one part. LiteLLM
stores a function-call signature both message-level (thought_signatures)
and on the tool call itself, then re-attached it to BOTH the text part
and the function-call part when serializing history. gemini-3 and newer
models bill every replayed copy as the previous turn's full reasoning
token count, so long agentic sessions doubled their context growth and
hit the 1,048,576-token limit
Only attach a message-level signature to the text part when the same
signature is not already carried by a tool-call part:
- compare signature values instead of boolean presence so a distinct
text-part signature is never dropped
- ignore the gemini-3 dummy-signature fallback during detection so
replaying gemini-2.5 history to a newer model keeps the real text
signature
- count signatures carried by server-side tool invocations so they are
not re-attached to the text part
gemini-2.5 responses (signature on the text part, function call
unsigned) are unaffected: the text signature is preserved as before
An auto-router deployment's litellm_params.model (auto_router/...) is the
discriminator the router loads it by, but the model management endpoints
accepted any client-supplied value verbatim; a doubled or stripped prefix
made router init fail on the next load and ignore_invalid_deployments
silently dropped the deployment. Validate writes that supply
litellm_params.model at all three endpoints against the merged params and
reject incoherent values with an actionable 400. Classification is
extracted to router_utils/auto_router_model_naming.py so the Router
predicates and the validation share one source
Every model-write endpoint returned 200 off the DB write alone; a model the
reload dropped (ignore_invalid_deployments, or a wholesale reload failure)
stayed invisible on every channel at once, which is how the registry-leak
defect went undiagnosed for three weeks. ProxyConfig.add_deployment and
clear_cache now return whether the reload pass completed, and each write
endpoint verifies the rows it wrote are live in this pod's router afterwards,
distinguishing a deliberately environment-inactive model via the same
predicate the Router's own gate uses. The access-group writers return the
mutated id set instead of discarding it
* fix(proxy): warm rotate Prisma client for IAM refresh
* fix(proxy): drain Prisma operations during IAM rotation
* fix(proxy): bound the drain wait when retiring a replaced prisma engine
A replaced engine waited indefinitely for its drain tracker to empty.
Hung queries self-release via prisma's 30s default HTTP timeout, but a
transaction whose owner is hard-cancelled before commit/rollback leaks
its drain count forever, keeping the retired engine and its DB
connection pool alive indefinitely; at one rotation per 12 minutes such
engines accumulate. Cap the wait at 90 seconds, which exceeds every
legitimate operation bound (30s HTTP timeout, 60s max interactive
transaction timeout in this codebase), then kill the engine anyway.
Work killed at the deadline degrades to the pre-drain behavior and is
retried by the existing reconnect/backoff layers.
---------
Co-authored-by: ryan-crabbe-berri <ryan@berri.ai>
Scrub aliases on delete only when the deleted deployment's model_name no
longer resolves in the router. A legacy load-balanced team model can have
several deployment rows sharing one internal name; deleting one replica
must not remove aliases that still route to the survivors, in any team
A team's model_aliases can map a public name like gpt-4 to the internal
routing key (model_name_{team_id}_{uuid}) of a team deployment that has
since been deleted, e.g. after replacing per-team duplicates with one
gateway-level model. The pre-call rewrite then sent every request to a
name the router cannot serve, failing with "no healthy deployments for
model_name_..." even though the requested name still resolves at the
gateway level. The rewrite is now skipped when the alias target has no
live deployment in the router
delete_model also skipped the team alias scan for internal-shaped names
on the assumption they can never be alias values, which is exactly the
shape legacy team model aliases have, so deleting a legacy team model
left the stale alias behind. The scan now always runs, and a public
name that still resolves to a live router deployment (e.g. a shared
gateway-level model group) stays in team.models so the delete does not
revoke the team's access to it
The aggregate gateway DCR flow ends in a 303 to the client's loopback
redirect_uri. When the MCP client runs on a browserless machine (EC2,
SSH box, container) the user authorizes from a browser on another
machine, so the 303 dereferences the wrong loopback and the code never
reaches the client.
The connect banner now offers manual delivery for loopback clients: the
finish form posts delivery=manual and /authorize/complete renders the
callback URL on a no-store page instead of redirecting. The user pastes
it into the client (Claude Code v2.1.191+ accepts a pasted callback URL)
or fetches it from the client machine's terminal. Manual codes keep the
same sealing, PKCE binding, and single-use guard, with a 5 minute
expiry instead of 2 to survive the copy-paste hop; the used-code marker
TTL derives from the code's own remaining lifetime so the single-use
property holds for the full 5 minutes. The default redirect path is
unchanged.
Resolves LIT-4863
Anthropic-shaped usage was mapped through the Responses API usage converter, which ignores top-level cache_read_input_tokens/cache_creation_input_tokens, so cache hits on /v1/messages were billed entirely at the uncached input rate
delete_deployment resolved the outgoing deployment through get_deployment before
popping it, and ran the strategy release before repairing the index maps. Both
halves of that ordering could leave the router inconsistent. A resolution failure
meant the entry left the model_list with its registry slots still held, so the
alias stayed routable and the name could not be reused; a failure inside the
release meant the outer handler returned None with the entry already popped and
model_id_to_deployment_index_map never repaired, breaking every later lookup and
delete until a restart.
upsert_deployment already had this right: it pops, repairs the caches and indices,
and only then releases the slot. delete_deployment now follows the same sequence
and resolves the deployment from the item it just popped rather than through a
lookup that can fail. Releasing the slot is secondary to structural integrity, so
it runs last and a failure there is logged instead of abandoning a removal that has
already happened.
* fix(ui): validate default team values in Default User Settings
The Default User Settings form accepted any free-text team id, and the
proxy persisted it without checking the team exists. New users were then
silently never added to the default team because the consume-time 404
from team_member_add was swallowed at debug level.
Backend: PATCH /update/internal_user_settings now rejects unknown and
duplicate team ids with a 400 naming them, before any persistence or
team budget side effects. Team-add failures in _add_user_to_team now log
at ERROR with user and team ids.
UI: DefaultUserSettings rewritten as a shadcn + react-hook-form + zod
form following the org-settings pattern. The team id free-text input is
replaced with a searchable server-backed team picker, so only existing
teams can be selected; zod blocks empty and duplicate rows. The shared
deriveErrorMessage helper now unwraps the HTTPException detail.error
shape so backend validation errors surface readably in toasts.
* fix(ui): restore read-only view with Edit Settings toggle on default user settings
Parity with the pre-migration form: the tab renders a read-only summary
of the saved defaults, Edit Settings opens the RHF form, Cancel discards
pending edits and returns to the summary, and a successful save returns
to the summary showing the new values. Model sentinel labels in the
summary are derived from ModelSelect's now-exported special values
instead of duplicating the strings.
* refactor(ui): rename MODEL_SELECT_SPECIAL_VALUES_ARRAY to MODEL_SENTINEL_OPTIONS
* fix(ui): move Edit Settings into the card header action slot
The finalize re-run in upsert_deployment keyed off the auto_router/adaptive_router
prefix only, so editing a complexity router with adaptive enabled released its
adaptive_routers entry (and post-call hook) without rebuilding it: complexity
routing kept serving while bandit recording, DB persistence and
/adaptive_router/state went silently dark until the next full reload. Gate the
re-run on a participation predicate that mirrors both arms of the finalize pass,
drop the import that pass no longer uses, and pin the registry helpers with
direct contract tests
Auto-router-family deployments live in two structures: the model_list, and a
pre-routing strategy registry keyed by (model_name, tags). Removing a deployment
dropped it from the model_list without releasing its registry slot, so the re-add
that follows hit the "already exists" guard in _register_pre_routing_strategy and
ignore_invalid_deployments swallowed it. The deployment came out and never went
back, while the DB row and the endpoint response both looked fine. Only a restart
healed it, and under multiple replicas each pod diverged into holding a different
subset of routers.
Removal now releases the (model_name, tags) slot from every strategy registry, in
both upsert_deployment and delete_deployment, guarded on the auto_router/ prefix so
removing a regular deployment cannot evict a router that merely shares its
model_name. Releasing from every registry rather than the first match is what makes
this correct for hybrids: registration is one-to-many, since a complexity router
configured with adaptive is also registered in adaptive_routers under the same key
by the deferred finalize pass. Releasing only the first match left that adaptive
strategy live, so a deleted or replaced alias stayed routable through it.
Adaptive post-call hooks are rebuilt whenever the adaptive registry changes, not
only at the end of set_model_list. The hook set is defined as exactly one hook per
registered adaptive router, so a released router stops recording turns instead of
holding a hook bound to a strategy nothing points at any more.
The swallowed upsert failure is logged at warning instead of debug, which is below
the default log level and left this failure with no observable signal anywhere.
delete_deployment resolves the outgoing deployment before popping it, and a
resolution failure no longer aborts the removal; previously an entry that failed
validation would have been left in the model_list permanently.
delete_model drops its blanket pop across all four registries. That predates this
change and over-evicts: it removes every tag variant registered under the name
while only one is being deleted, and nothing reloads on that path to restore the
survivors. delete_deployment now handles it correctly and tag-scoped, so the
endpoint-level eviction and its helper are removed rather than left to mask it.
Exercises the streaming field-fill heuristics, model_construct fallbacks,
and the get/cancel/delete/list request and response transforms that had no
tests.
litellm_provider_cache_creation_input_tokens_metric only read the
Anthropic-style top-level usage.cache_creation_input_tokens and had no
prompt_tokens_details fallback, unlike its cache-read twin. OpenAI models
that bill prompt cache writes report them only in
prompt_tokens_details.cache_write_tokens, so the counter never fired for
them. Resolve provider cache read/write tokens through a shared helper
that falls back to prompt_tokens_details.cache_write_tokens (canonical)
then cache_creation_tokens when the explicit top-level field is absent,
and give litellm_input_cache_creation_tokens_metric the same fallback for
raw usage dicts that only carry cache_write_tokens