Commit graph

4245 commits

Author SHA1 Message Date
Mateo Wang
aedaf4d0b0
Merge pull request #38434 from BerriAI/litellm_propagate_prompt_deletes
fix(prompts): propagate prompt deletes to every worker and pod
2026-08-26 21:03:00 -07:00
devin-ai-integration[bot]
2e2c8200ae
fix(scim): apply default_team_params (incl. models) to SCIM-created teams (#38433)
* fix(scim): apply default_team_params (incl. models) to SCIM-created teams

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

* test(scim): annotate default_team_params regression test parameters

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

---------

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-26 20:49:21 -07:00
devin-ai-integration[bot]
172e3aceaf
fix: bound row count on GET /spend/logs to stop unbounded LiteLLM_SpendLogs scans (#38420)
Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-26 20:48:10 -07:00
devin-ai-integration[bot]
ee76c9a6f4
fix(mcp): accept raw x-litellm-api-key on streamable HTTP admission (#38364)
* fix(mcp): accept raw x-litellm-api-key on streamable HTTP admission

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

* chore(mcp): drop comments restating parser behavior

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

---------

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-26 20:45:51 -07:00
yuneng-jiang
3eba0b332a
Merge pull request #38430 from BerriAI/litellm_/budget-update-e2e-skip-81b8fa
fix(budget): serialize model_max_budget before the /budget/update write
2026-08-26 18:51:17 -07:00
devin-ai-integration[bot]
4bf40c4e8d
fix(logging): stop billing and logging response reads as LLM calls (#36890)
* fix(logging): stop billing and logging response reads as LLM calls

Retrieving, deleting or cancelling a stored response, and vector store management calls, run through the same logging lifecycle as inference. A retrieved response replays the usage of the call that created it, so every read priced it again and wrote a second spend log row for the same tokens. Non-inference calls now cost 0, report no usage, log no placeholder chat message, and get a litellm.responses_management operation name instead of reading as chat.

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

* fix(responses): keep billing background response jobs after the poll

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

* fix(logging): use an empty list for read-call messages

A tuple matches no branch in the loggers that walk this value, so lunary's
parse_messages falls through to clean_message and raises AttributeError on the
success hook. An empty list reads as no messages everywhere: it satisfies the
isinstance(list) checks in newrelic, mlflow and datadog, iterates zero times in
traceloop and helicone, and is what StandardLoggingPayload.messages is typed to
hold. None would be type-legal too but is not iterable, so it trades one crash
for another in mlflow and traceloop.

* fix(otel): stop the legacy emitter reporting replayed tokens on response reads

The zeroing so far lands in the standard logging payload, which the legacy
OpenTelemetry emitter does not read for usage: it takes prompt, completion and
total tokens straight off the response object, so a retrieval span still carried
the token counts of the call that produced the response, and the token usage
histogram still recorded them. That emitter is the default, so the spend row said
zero while the trace said otherwise. The background cost poller keeps its counts,
the same exemption the pricing path already makes.

* fix(logging): keep billing a background response when its retrieval is read

A response created with background=true comes back queued and carries no usage, so
its create bills nothing. The retrieval that first sees the finished job is the only
place that job's tokens are ever visible, and pricing every read at zero therefore
loses the spend outright rather than deduplicating it. On a proxy without the
enterprise cost poller a background job ended up costing $0 end to end.

is_unbilled_non_inference_call now takes the response it is deciding about and treats
a background response the same way it already treats the poller's own read, which is
the same exemption seen from the other side. The legacy OpenTelemetry emitter's time
per output token metric picks up the read gate it was missing, so it stops dividing a
read's latency by the replayed completion token count.

* test(proxy): pass the read response to the non-inference predicate

The poller test called is_unbilled_non_inference_call with the pre-background signature, so it broke when the predicate gained the response it classifies. It now hands the predicate a foreground read, and asserts that the same read is free without the origin stamp, so the stamp is what the test proves.

* fix(otel): stop the v2 metrics recorder reporting replayed tokens on response reads

The v2 span builder sources usage from the standard logging payload, so the
earlier fix already zeroes it there. The metrics recorder reads response_obj
directly, so a responses-management read still recorded the original
generation's tokens into gen_ai.client.token.usage and divided generation time
by them for gen_ai.server.time_per_output_token.

The read still records operation and response duration, under the
litellm.responses_management operation, so it stays observable.

* fix(proxy): keep the response-cost headers on calls priced at zero

Pricing responses reads and vector-store management routes at zero dropped the whole
x-litellm-response-cost family off those replies. The header build reads a falsy zero as
a cost this response never recorded and filters it out, and a call that returns before
pricing stores no cost breakdown for the component headers to read, so a client parsing
the cost off a read got a KeyError where it had previously been handed a number.

Those calls now advertise the family at zero. Retrieving a background response, and the
cost poller's read of one, still report their real cost.

The params-taking form of the predicate moves from opentelemetry into
internal_call_metadata so the proxy header build and the OTEL recorders share one copy.

* fix(proxy): report a zero cost split only under a zero cost total

The component headers were filled from call-type membership alone, while the
total they sit beside keeps its real value when the read priced normally, so a
breakdown that had not landed by the time headers were built could advertise a
real total next to an all-zero split. The split is now reported as zero only
when the total agrees with it, and is otherwise left absent.

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Yucheng Zhu <yucheng@berri.ai>
2026-08-26 18:34:17 -07:00
mateo-berri
4fd7b9946f fix(prompts): keep prompts created mid-sync out of the deleted-row sweep 2026-08-26 18:22:32 -07:00
Mateo Wang
77765fd302
Merge pull request #36055 from BerriAI/devin_ai_fix_gemini_stream_billing_36042
fix(google_genai): price streamed generateContent with the provider that served it
2026-08-26 18:05:50 -07:00
mateo-berri
26b7bc3583 fix(prompts): propagate prompt deletes to every worker and pod 2026-08-26 18:01:08 -07:00
tin-berri
1df25e26cf
revert(proxy): remove router_model_name from auto-routed response bodies (#38429)
Reverts #37725. The field existed so SDK callers that cannot read
`x-litellm-model-id` could tell which tier an auto-router picked, and the
framework that motivated it was LangChain. `@langchain/openai` builds
`additional_kwargs` and `response_metadata` from fixed key allowlists and drops
unknown fields at both the chunk top level and inside `delta`, so no
proxy-side placement of a namespaced key can reach a LangChain caller.

The complexity router's existing `return_raw_model_name` already covers that
case: it puts the resolved model in the standard `model` field, which
LangChain does propagate (`model_name` is on its metadata allowlist), and the
proxy honors it on both the streaming and non-streaming paths.

Keeps the unrelated cleanup from #37725 that dropped the redundant
function-local `ProxyBaseLLMRequestProcessing` import shadowing the
module-level one in `async_data_generator`.

`TestModelGroupAliasReachesPreRoutingStrategies` asserted on the marker as a
proof of strategy dispatch; the surviving `response.model == "gemini-flash"`
assertion already proves it.
2026-08-26 17:58:30 -07:00
Yuneng Jiang
315144c9cc
test(budget): annotate the new locals with Final 2026-08-26 17:55:33 -07:00
Mateo Wang
5175fda0af
Merge pull request #38407 from BerriAI/litellm_fix_dotprompt_model_swap
fix(prompts): apply prompt templates before routing on /v1/responses and honor ignore_prompt_manager_model
2026-08-26 17:50:49 -07:00
Yuneng Jiang
595ada1ef7
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/budget-update-e2e-skip-81b8fa 2026-08-26 17:46:28 -07:00
Yuneng Jiang
4d6786d420
fix(budget): serialize model_max_budget before the /budget/update write
/budget/update handed prisma the raw update dict, so a model_max_budget
payload reached the Json? column as a nested python dict. prisma-client-py
renders that into the GraphQL mutation as bare object keys rather than a
JSON string, and the query engine rejects it, so every per-model budget
update returned a 500 and the cap was never stored. Model ids carrying
punctuation (glm-5.2) also produced an invalid GraphQL name.

/budget/new already ran its payload through jsonify_object for exactly this
reason. Do the same on the update path. Team member and organization member
budget updates route through this handler too, so they were failing the same
way.

The existing unit tests mocked the prisma table with an AsyncMock that
accepts any dict, which is why this never showed up outside a live proxy.
The new test asserts on what the endpoint hands prisma.
2026-08-26 17:46:17 -07:00
yucheng-berri
ecc49764af
feat(guardrails): track Azure Prompt Shield usage and cost with spend isolation (#38387)
* Track Azure Prompt Shield guardrail usage and cost with spend isolation (LIT-5917)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Resolve credential references and pydantic extras in in-place guardrail updates

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Suppress LIT001 on the dict-accepting update helper signature

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 17:42:17 -07:00
mateo-berri
8697a9ffa9 Merge branch 'litellm_internal_staging' of https://github.com/BerriAI/litellm into litellm_fix_dotprompt_model_swap 2026-08-26 17:36:03 -07:00
mateo-berri
ac2e07f6f4 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_fix_dotprompt_model_swap
# Conflicts:
#	litellm/responses/main.py
2026-08-26 17:36:01 -07:00
yuneng-jiang
f677292901
Merge pull request #38392 from BerriAI/litellm_/search-tools-sync-issue-e522a2
fix(proxy): sync search tools into the router on management writes
2026-08-26 17:34:51 -07:00
Mateo Wang
53a607e088
Merge pull request #38411 from BerriAI/litellm_fix_prompt_patch_sync
fix(prompts): propagate PATCHed prompt templates to every worker and pod
2026-08-26 17:20:27 -07:00
Mateo Wang
39dd46397e
Merge pull request #38379 from BerriAI/litellm_mcp_oauth_admin_entered_authorize_urls
fix(mcp): honor admin-entered OAuth URLs on authorize after issuer yield
2026-08-26 17:11:00 -07:00
yuneng-jiang
e80ba92cfa
Merge pull request #38313 from BerriAI/litellm_/hide-unhealthy-virtual-key-models-922c37
feat(proxy): hide unhealthy models from model listings, opt-in
2026-08-26 17:05:41 -07:00
yuneng-jiang
cbefebbd9f
Merge branch 'litellm_internal_staging' into litellm_/search-tools-sync-issue-e522a2 2026-08-26 17:04:01 -07:00
mateo-berri
5461bb3b48 fix(prompts): sync only the newest row when environments share a versioned prompt id 2026-08-26 17:00:24 -07:00
devin-ai-integration[bot]
8a9d5b15b4
feat(langfuse): support langfuse_environment as a per-key dynamic callback param (#38264)
* feat(langfuse): support langfuse_environment as a per-key dynamic callback param

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

* refactor(langfuse): type the langfuse_environment constructor param

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

* fix(langfuse): only pass environment when the SDK client supports it

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

* test(langfuse): drop the request-body metadata test for langfuse_environment

The proxy bans request-body callback params by default (derived from
_supported_callback_params in auth_utils), so the metadata channel this
test asserted is rejected with a 401 on the proxy. The supported channel
is admin-set key/team callback_vars, with LANGFUSE_TRACING_ENVIRONMENT
as the deployment-wide fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(langfuse): validate langfuse_environment, avoid redundant clients, honor it in langfuse_otel

Closes the review gaps on the langfuse_environment param:

- Validate values against Langfuse's environment pattern at save time
  (/key/generate, /key/update, /team callback all 400 on e.g. 'Production'
  instead of 200-then-silently-dropping every trace server-side) and at
  logger init; non-string values are str()-coerced instead of crashing
  the SDK's regex check per event.
- Treat empty/whitespace values and values equal to the deployment-wide
  LANGFUSE_TRACING_ENVIRONMENT as non-dynamic so an environment-only
  override that changes nothing no longer mints a duplicate SDK client
  against MAX_LANGFUSE_INITIALIZED_CLIENTS.
- langfuse_otel now reads the per-key/team langfuse_environment from
  standard_callback_dynamic_params instead of only the env var.
- Advertise the param on the discovery surfaces: callback_configs.json
  (langfuse + langfuse_otel), the dashboard callback registry, and the
  /team/{team_id}/callback docstring (schema.d.ts regenerated).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* style: ruff format langfuse files

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(lint): remove duplicate test import, LIT002 dict literal, and mock-echo otel test

- drop redundant in-function import of callback_config_error (F811)
- avoid the `or {}` mutable literal in _set_langfuse_specific_attributes (LIT002)
- rewrite the dynamic-env otel test to observe span.set_attribute output
  instead of patching litellm internals (TQ002/TQ008)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: milan <milan@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: yucheng-berri <yucheng@berri.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 16:56:55 -07:00
mateo-berri
057781a187 test(pass_through): pin stream pricing tests to injected divergent rate cards 2026-08-26 16:54:05 -07:00
mateo-berri
fed7a48b3c Merge remote-tracking branch 'origin/litellm_internal_staging' into devin_ai_fix_gemini_stream_billing_36042
# Conflicts:
#	litellm/proxy/pass_through_endpoints/llm_provider_handlers/vertex_passthrough_logging_handler.py
2026-08-26 16:44:11 -07:00
tin-berri
d8edfb69c2
fix(proxy): derive auto-router health from its underlying models (#38174)
An auto_router deployment is a marker, not something a probe can contact, so
`_run_model_health_check` returns `{}` for it and it lands healthy whatever is
behind it. This derives its verdict from the models it actually resolves.

Rules and owners:

- `strategy_router_dependencies` is the single answer to "what does this router
  call": tier, default, classifier and embedding names per router kind, aligned
  with what init and the request path actually use.
- `_health_check_eligible` is the single probe-eligibility gate, applied to the
  requested set and to the pool a router's dependencies are drawn from alike, so
  an opted-out deployment cannot re-enter through a router that depends on it.
- `_resolved_deployment_ids` resolves names through `get_model_list`, the same
  composition of alias, routing-group and wildcard channels a request uses.
- A dependency reds its router only when *every* deployment behind the name is
  known unhealthy. A replica this run never judged, hidden from the caller or
  opted out of health checks, can still serve what the dead one drops, so
  partial evidence leaves the verdict green. Absent information never reds.
- Verdicts settle over rounds, because a marker never fails a probe of its own
  and a parent whose tier is a red router must inherit that fault. Both sweeps
  are bounded loops, so a router cycle terminates green.
- Dependency probes are added only on the targeted `/health?model_id=` path the
  dashboard uses per deployment, and are dropped from the response.

Resolves LIT-6073

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 16:41:54 -07:00
Mateo Wang
b98b2d562b
Merge pull request #38416 from BerriAI/litellm_fix_lazy_openapi_stubs_for_imported_modules
fix(proxy): key lazy openapi stubs off registered features, not sys.modules
2026-08-26 16:32:02 -07:00
mateo-berri
93e7e8d980 fix(mcp): token exchange rejoins discovery for a clientless DCR bridge missing its registration endpoint 2026-08-26 16:26:44 -07:00
mateo-berri
6df307fef8 fix(prompts): validate a prompt replacement before swapping and isolate per-row sync failures 2026-08-26 15:49:31 -07:00
mateo-berri
ce8d6f7d25 fix(mcp): token refresh and M2M egress honor the admin-entered token URL 2026-08-26 15:31:45 -07:00
mateo-berri
fd751a5023 fix(proxy): key lazy openapi stubs off registered features, not sys.modules 2026-08-26 15:18:55 -07:00
mateo-berri
5130bafda8 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_fix_prompt_patch_sync 2026-08-26 15:18:11 -07:00
ryan-crabbe-berri
52b7bea6f3
Merge pull request #37708 from BerriAI/litellm_team_member_budget_no_reset
fix(team): allow no-reset default budgets for team members
2026-08-26 15:14:30 -07:00
mateo-berri
6918214266 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_fix_dotprompt_model_swap
# Conflicts:
#	tests/test_litellm/integrations/dotprompt/test_prompt_manager.py
2026-08-26 15:13:57 -07:00
mateo-berri
f824ca7433 fix(responses): run prompt hook before provider credential resolution in sync responses() 2026-08-26 15:08:23 -07:00
Mateo Wang
632a007967
Merge pull request #38406 from BerriAI/litellm_fix_db_router_settings_overwrite
fix(proxy): stop empty DB router_settings lists from clobbering yaml fallbacks
2026-08-26 15:06:26 -07:00
mateo-berri
b687fe2b50 fix(prompts): propagate PATCHed prompt templates to every worker and pod 2026-08-26 15:03:32 -07:00
Yuneng Jiang
3576c773eb
fix(proxy): serialize the search tool router refresh
reload_search_tools_from_db is a read-modify-write of the shared llm_router
global: it reads the whole table, merges the config tools in, and replaces
router.search_tools wholesale. Two of those interleaving lets the older
snapshot's assignment land last and put back a tool the newer one deleted, so a
revoked tool keeps serving on the provider key it carried until the next reload.

Take MODEL_RECONCILE_LOCK, which add_deployment already uses to serialize the
same shape of work on the same global. It has to go on this entry point rather
than in _init_search_tools_in_db, because _init_non_llm_objects_in_db calls that
while already holding the lock and asyncio.Lock is not reentrant.

A separate search-tools-only lock would not close the race: the periodic
reconcile reaches _init_search_tools_in_db under MODEL_RECONCILE_LOCK, so only
that same lock orders an endpoint refresh against a cron tick.

Ordering across workers is unchanged and still reconciles on the next tick.
2026-08-26 15:02:19 -07:00
Mateo Wang
16e9efccaf
Merge pull request #38404 from BerriAI/litellm_fix_prompt_data_double_nest
fix(prompts): reject keyed prompt_data with prompt_id and populate prompt version
2026-08-26 14:47:59 -07:00
mateo-berri
3b9f6ee2aa fix(proxy): apply empty DB router_settings lists only where yaml sets no value 2026-08-26 14:37:29 -07:00
mateo-berri
43b8ed0fd9 fix(prompts): validate only the litellm_params a PATCH sends 2026-08-26 14:20:26 -07:00
mateo-berri
dbc819dc77 fix(prompts): apply prompt templates before routing on /v1/responses and honor ignore_prompt_manager_model
On /v1/responses the prompt template ran inside litellm.aresponses, after the
router had already resolved a deployment and injected its api_key/api_base, so a
prompt whose metadata.model pointed at another provider sent the old
deployment's credentials cross-provider (401). The proxy now runs the prompt
template for aresponses in the pre-call hook, before routing, so the router
picks the deployment that matches the swapped model. As a backstop, the SDK
refuses a cross-provider swap when explicit credentials are already present
instead of forwarding them.

ignore_prompt_manager_model and ignore_prompt_manager_optional_params saved on
a prompt were only read by the generic manager, so dotprompt prompts ignored
them on every endpoint. PromptManagementBase now merges the prompt spec's flags
with the per-request ones for every manager, and the generic manager no longer
drops caller flags when no spec is present.
2026-08-26 14:12:28 -07:00
mateo-berri
465ebb1bdd fix(mcp): join discovery for a clientless DCR bridge still missing its registration endpoint 2026-08-26 14:08:09 -07:00
ryan-crabbe-berri
870328f8cc test(team): fake the budget table instead of patching new_budget and update_budget
The three member-budget tests patched litellm internals and asserted only on
the mock, which tripped the TQ002 and TQ008 test-quality ratchet. Fake the
prisma budget table on the shared client and assert on the row that reaches
the database plus the returned team payload.
2026-08-26 14:00:45 -07:00
mateo-berri
6d1b295dae fix(proxy): stop empty DB router_settings lists from clobbering yaml fallbacks 2026-08-26 13:58:00 -07:00
mateo-berri
6fa5164d86 fix(prompts): reject keyed prompt_data with prompt_id and populate prompt version
POST /prompts silently stored an empty template when litellm_params.prompt_id
was combined with prompt_data keyed by template name, because the loader
wrapped the already-keyed dict under prompt_id a second time. The loader now
wraps only a flat template (a dict carrying a content key), and create,
update, and patch reject the ambiguous keyed+prompt_id combination with a 400
that names both valid shapes. The API also returned version null on every
create and lost version, environment, and created_by on registry reload; both
now carry through. Versioned ids like my-prompt.v1, which the create API
itself returns, now resolve to their base template on the SDK prompt hooks,
and a flat DB prompt with no litellm_params.prompt_id registers under its base
API id instead of garbage.
2026-08-26 13:57:10 -07:00
milan
99d4741586 fix(team): allow no-reset default budgets for team members
The Default Budget Duration field in Team Member Settings only offered daily, weekly and monthly, so a team member budget could never be set to never reset. It now uses the shared BudgetDurationDropdown, and /team/update writes an explicitly null duration through to the member budget row along with its reset time.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-26 13:48:08 -07:00
yuneng-jiang
a5d8963616
Merge pull request #38308 from BerriAI/litellm_key-update-redis-evict
fix(proxy): stop cache eviction errors from failing /key/update
2026-08-26 13:44:20 -07:00
mateo-berri
19e6f03a3c fix(mcp): let root oauth routes defer discovery to the endpoint-gated flow join 2026-08-26 13:39:35 -07:00