Commit graph

8121 commits

Author SHA1 Message Date
yucheng-berri
e99151bb95
feat(guardrails): make the Generic Guardrail resilient to built-in tools and errors (adopted from #31286) (#31461)
Some checks failed
GitHub Actions Security Analysis / zizmor (push) Waiting to run
LiteLLM Rust / rustfmt, clippy, test (push) Has been cancelled
* fix(guardrails): stop Generic Guardrail API 500 on built-in tools

Requests carrying built-in tools (code_interpreter, file_search, ...) crashed
the Generic Guardrail with a 500. GenericGuardrailAPIRequest.tools validated
each tool against ChatCompletionToolParam, whose base TypedDict requires a
function block, so a tool like {"type": "code_interpreter"} raised a Pydantic
ValidationError before the request was ever sent.

Type the field with a permissive GuardrailToolParam model (type required,
extra=allow) so built-in tools validate and their config is forwarded to the
guardrail intact instead of being stripped.

* feat(guardrails): add complete fail-open (fail_on_error) to Generic Guardrail

The Generic Guardrail already honored unreachable_fallback, which fails open
only on network-unreachable errors. This wires up the existing generic
fail_on_error config (so far implemented only by Model Armor) so that
fail_on_error=false degrades any guardrail error to a critical-log warning and
lets the request proceed as if the guardrail were absent.

Only a valid guardrail response can act: a parsed BLOCKED decision still raises,
while endpoint errors, malformed responses, and internal serialization or
validation errors all fall through when fail_on_error=false. To cover that last
class, the request construction now runs inside the protected block, so the kind
of validation error that previously surfaced as a 500 is caught here too.

Defaults to true (fail closed), matching today's behavior; turning it off is an
explicit availability-over-security choice and is logged at critical level on
every bypass.

* test(guardrails): cover fail_on_error on the response path

The existing fail_on_error tests all drive the request path. Add response-path
(input_type=response) coverage: an endpoint error proceeds unchanged under
fail_on_error=false, and a valid BLOCKED decision still raises. Guards against a
future regression that special-cases input_type in the error handling.

* style(guardrails): black-format the fail-open guard expression

CI runs black (line-length 88) over litellm/; the unreachable_fail_open
assignment exceeded it. Wrap it to satisfy the formatter.

* fix(guardrails): validate tools into GuardrailToolParam at the call site

Changing the request field to List[GuardrailToolParam] left the construction
passing List[ChatCompletionToolParam] (list is invariant), which tripped the
basedpyright reportArgumentType budget gate. Validate each tool explicitly,
which is what Pydantic did implicitly, so the types line up with no Any or
suppression and the serialized payload is unchanged.

* fix(guardrails): make fail-open log message accurate for non-network errors

The fail-open path is now shared by fail_on_error, so it fires for any guardrail
error, not just unreachability. The log said 'unreachable' even for an HTTP 400
or a malformed response; reword to 'error' (the status code and exception are
already logged). Addresses the Greptile review's only finding.

* fix(guardrails): align GenericGuardrailAPIResponse.tools with GuardrailToolParam

Greptile flagged that the request side moved to GuardrailToolParam but the
response side still annotated tools as List[ChatCompletionToolParam], which
mandates a function block and contradicts the new built-in-tools support.
Update the response annotation (and the now-unused import) so the two sides
agree. Runtime is unchanged; from_dict stores the raw dicts and the only
consumer assigns through to GenericGuardrailAPIInputs without inspecting
the elements.

---------

Co-authored-by: Itay Ovadia <itay@sun.security>
2026-06-26 11:25:56 -07:00
Mateo Wang
5a1c7839be
feat(mistral): add mistral/mistral-ocr-2512 (OCR 3) to cost map (#31463)
Adds the OCR 3 model (mistral-ocr-2512) released 2025-12-18 to both the
root and bundled backup cost maps at $2 / 1000 pages and $3 / 1000
annotated pages, mirroring the existing Mistral OCR entries. Regresses
the pricing in both maps and verifies completion_cost scales per page.
2026-06-26 10:29:07 -07:00
Yassin Kortam
aa49568059
perf(caching): memoize _get_all_llm_api_params, rebuilt per request (#31430)
ModelParamHelper._get_all_llm_api_params() introspects six sets of supported
kwargs from static OpenAI type annotations and fixed sets and unions them. The
result is constant for the process lifetime, but it was recomputed on every
request through both Cache.get_cache_key (caching path) and
_get_relevant_args_to_use_for_logging -> get_standard_logging_model_parameters
(spend-logging / callback path). Memoize it with lru_cache(maxsize=1); the
function takes no arguments, its result is process-static, and both callers
treat it as read-only. ~4.7 us/call to ~0.02 us/call.
2026-06-26 16:46:18 +00:00
Yassin Kortam
0e1a3babf0
perf(cost-calc): precompute service-tier cost-key suffixes (#31431)
_get_token_base_cost rebuilt f"_{st.value}" for every ServiceTier while
scanning every model_info key on each request, and _get_cost_per_unit rebuilt
the same f-strings in its fallback loop. The suffixes are constant, so compute
them once at module level (matching the existing _IMAGE_RESPONSE_CALL_TYPES /
_VALID_DATA_RESIDENCIES pattern) and use str.endswith(tuple) for the threshold
check. Behavior is identical; ~3.5 us/call to ~2.2 us/call on the threshold scan.
2026-06-26 09:38:58 -07:00
Yassin Kortam
fc644cff3d
perf(spend-logs): only strip NUL bytes in safe_dumps when present (#31424)
safe_dumps ran strip_null_bytes (a str.replace) on every string value and
every dict key during recursive serialization. NUL bytes are vanishingly
rare, so for the common case this was pure overhead that scaled with payload
size; with store_prompts_in_spend_logs the full prompt and response are
serialized on every request, so it landed directly in the per-request hot
path. Guard the strip behind a cheap "\x00" in obj membership check so
NUL-free strings are returned untouched. Behavior is unchanged: NUL bytes are
still stripped from values, keys, nested structures, and the str() fallback.
2026-06-26 09:37:19 -07:00
Sameer Kankute
4476923ac4
test: add realtime proxy e2e suite across providers (#30960)
* tests: add e2e tests for spend, budgets and llms

* style: make chained comparison of status_code clearer

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* remove e2e_tests folder

* test: add spend tracking tests

* test: multi-window budgets coverage

* fix: p0 issues, added types and shared functions for each test suite

* chore: add config.yml

* test: passthrough endpoints stream/non-stream e2e

* style: carry clearer status_code comparison into renamed e2e dir

* fix: rename cost breakdown function

* fix: pydantic validation for budget info, dont allow explicit type cast

* refactor: migrate to gateway client

* test: add custom pricing tests

* chore: change master key

* test(e2e): address greptile review feedback

Remove the duplicate cache/cache_params block in the gateway config so the two
can't silently diverge under future edits. Reorder the soft-budget test to assert
the call isn't a budget block before require_successful_call, since that helper
hard-fails any non-2xx and left the budget-block check unreachable; the misleading
"skip" comment is corrected. Add a deferred delete in test_budget_delete_removes_it
so a failed delete doesn't leak a budget on the shared proxy. Scope the
spend_tracking sys.path insertion in pytest_sessionfinish to just the cleanup
import so a broader "pytest tests/" run isn't left with a mutated path.

* test(e2e): drop misleading skip comment on require_successful_call

require_successful_call fails hard, it does not skip; the trailing
comment was factually wrong. The function name already states intent,
so the comment is removed in both per-model and tag budget helpers.

* test(e2e): assert budget-isolation invariant before success check

On the should-still-succeed path of the per-model and tag isolation
tests, check is_budget_block before require_successful_call. If the
isolation bug fires the unaffected model/tag is blocked, so asserting
the specific 'blocked by X' invariant first yields the diagnostic
message instead of a generic upstream-failure. Matches the ordering in
test_soft_budget_e2e.py.

* fix(e2e): guard spend-log truncate on skip and stop returning unrelated priced rows

* fix(e2e): run case init() inside try so partial-init failures tear down

run_case called case.init() outside the try/finally that runs teardown(), so a
case that registers cleanups progressively (create team, then user, then key)
and then fails partway through init() would leak the already-created entities on
the long-lived shared proxy. Move init() inside the try so teardown always runs.

Add a regression test that registers a cleanup then raises mid-init and asserts
the resource is still released.

* test(e2e): mark known pricing-leak isolation test xfail(strict)

test_custom_pricing_is_isolated_from_sibling_deployment documents a real proxy
gap (a deployment's custom per-token pricing leaks into the shared cost map for
sibling deployments of the same underlying model) and was left unconditionally
failing, which pollutes the suite's pass/fail signal. Mark it xfail(strict=True)
so the suite stays green while the leak persists and turns into a failure the
moment isolation is fixed, prompting the marker's removal.

* refactor(e2e): make suite pass its shipped strict basedpyright config

The suite ships tests/pyrightconfig.json (strict, no Any), but basedpyright
--project tests reported four errors in it: three reportAny on the parametrize
ids=lambda c: c.__name__, and one reportUnusedFunction on the underscore-prefixed
autouse fixture _require_live_proxy. Replace the untyped lambda with a typed
_case_id(case_cls: Type[_BudgetCase]) -> str so the ids are no longer Any, and
rename the fixture to require_live_proxy so basedpyright no longer treats it as an
unused private function (it is referenced only by pytest's autouse machinery).
basedpyright --project tests now reports zero errors.

* fix(tests/e2e): gate spend-log truncate on e2e marker, not test directory

* test(e2e): run harness unit tests without a live proxy

The autouse session fixture skipped the whole tests/e2e session when no proxy
answered, which also skipped test_lifecycle.py, a pure unit test of run_case that
never touches the proxy. A regression test that silently skips gives no signal,
so the skip now lives in pytest_runtest_setup gated on the same e2e marker the
spend-log truncate guard already uses: live tests skip when no proxy is up while
harness unit coverage always runs. The liveness probe is cached with lru_cache so
it still runs once per session

* test(e2e): clean up gateway config comment debris

Fix the typo on the header comment and drop the orphaned namespace/ttl
comment remnants left indented under cache_params; the active values are
already set above. Flagged by greptile review.

* fix: add new tests, split gateway

* test(e2e): type the redis spend-counter probe for strict basedpyright

The new cold-counter reseed test drove its redis client untyped, so the strict
tests/pyrightconfig.json (reportUnknown*, reportAny) flagged ten errors once the
file landed: scan_iter/get came back unknown and the pool.map lambda had an
untyped parameter. Annotate the client as redis.Redis[str] via a TYPE_CHECKING
import (the runtime import stays lazy so the suite still skips, not errors, when
redis is absent), which resolves scan_iter to Iterator[str] and get to str | None,
and replace the lambda with a typed inner function mirroring _burst. basedpyright
--project tests is back to zero errors.

* test(e2e): xfail the known team multi-window failure and isolate member teardown

Greptile flagged two issues in the mirrored split-gateway commit. The team
multi-window budget test documents a real /team/new write bug (budget_limits go
straight to the Json? column and Prisma 500s, unlike the json.dumps'd key and
/team/update paths) and was left as an unconditional hard failure, which would
turn any live-proxy CI run red; mark it xfail(strict=True) like the custom-pricing
isolation test so the suite stays green while the bug persists and flips to a
failure the moment the write is fixed and the marker should go.

The class-scoped member fixture in test_team_member_budget_e2e.py tore down its
key, user, and team sequentially with no exception isolation, so a failed
delete_key would strand the user and team on the long-lived shared proxy. Route
cleanup through a ResourceManager: register each delete progressively and run them
LIFO best-effort in a finally, so a partial-setup failure still releases what came
before and one failed delete never blocks the rest.

* test: add realtime proxy e2e suite across providers

Add tests/realtime_e2e covering the proxy realtime websocket endpoint
end to end against live providers (openai, azure, gemini, vertex_ai,
bedrock, xai). Two layers: a raw-websocket suite asserting the
normalized OpenAI GA event sequence, delta/transcript consistency,
usage, and a full tool-call round-trip; and a pipecat smoke driving the
proxy through the GA OpenAIRealtimeLLMService. Tests carry a new
realtime_e2e marker and skip cleanly when the proxy or provider creds
are absent, so they stay out of the default unit run.

* test: move realtime e2e suite into tests/e2e harness

Replace the standalone tests/realtime_e2e with a tests/e2e/realtime suite
that follows the existing e2e conventions: a session-scoped client fixture,
a frozen-dataclass RealtimeClient wrapping the shared Gateway, pydantic
models for every sent and received event, and the e2e marker with the
parent harness's liveness skip. The suite opens the proxy realtime
websocket (websockets.sync to stay synchronous like the rest of the
harness) and asserts the normalized OpenAI GA event sequence for a text
conversation plus a full tool-call round-trip, parametrized across
providers. A provider whose realtime alias is not configured on the proxy
skips via /model/info. Adds a gemini realtime model to the gateway config
and fixes the openai realtime model id.

* test: add pipecat realism layer to realtime e2e suite

Add test_realtime_pipecat_e2e driving the same providers through pipecat's
GA OpenAIRealtimeLLMService with base_url pointed at the proxy, as a coarse
realism check on top of the raw-websocket suite. Each test stays synchronous
and runs the async pipecat pipeline via asyncio.run, and the module skips
unless pipecat-ai is installed. Lift the shared provider matrix, ws-url
helper, and skip helper into realtime_client so both suites use them.

* fix(e2e): parse GA realtime transcript events in e2e client

The realtime e2e client speaks the GA protocol, but transcript() only
aggregated beta delta event names. Handle GA deltas, fall back to
response.done output, and accept nested usage details on response.done.

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

* fix(e2e): address realtime code-review findings

- Use the real openai/gpt-4o-realtime-preview model ID in the gateway
  config (gpt-realtime-2 does not exist and would fail every live test)
- Pass a bare base_url to pipecat's OpenAIRealtimeLLMService so pipecat
  can append ?model= itself; the previous realtime_ws_url already
  contained ?model= causing a malformed duplicated query parameter
- Wrap connection.recv() in a try/except TimeoutError in collect_until
  so a deadline expiry inside recv preserves the collected-events
  diagnostic instead of raising a bare, message-free exception

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

* fix(e2e): filter configured_models to mode:realtime entries only

ModelInfoEntry.model_info used CustomPricing (extra="ignore") so the
mode field from /model/info was silently dropped, making it impossible
to distinguish realtime from non-realtime deployments. Add an optional
mode field to CustomPricing and filter configured_models() to entries
whose model_info.mode == "realtime" so skip_if_unconfigured never
accidentally skips a realtime test due to a naming-pattern collision
with a non-realtime deployment.

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

* Update litellm-config.yml

* fix(e2e): use TypeVar instead of PEP 695 generic in realtime parse_last

PEP 695 type-parameter syntax (def f[T: Bound](...)) is only parseable on
Python 3.12+, but the project declares requires-python >=3.10. Importing the
realtime e2e client on 3.10/3.11 raised a SyntaxError before any test could
run. Switch parse_last to the backport-safe TypeVar idiom so the suite imports
across the full supported range.

* fix(e2e/realtime): use GA openai/gpt-realtime model id

The realtime gateway config used openai/gpt-realtime-2, which is not a real
OpenAI model id and would 404 once live OpenAI realtime credentials are wired
in. The GA speech-to-speech model is openai/gpt-realtime (snapshot
gpt-realtime-2025-08-28); switch the openai-realtime alias to it.

* fix(realtime): harden Gemini/Vertex Live for audio-native e2e

Coerce TEXT responseModalities to AUDIO on native-audio and flash-live
models, suppress the orphan turnComplete response.done that arrives
immediately after tool results, omit function_response.id on Vertex,
stop appending client query params to Gemini/Vertex WSS URLs, and add
regression tests for these paths.

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

* Add xai full compatibility

* Add working vertex ai realtime tests

* Add audio + server vad e2e tests

* Add config for e2e testing models

* Add fix xai server vad

* fix: use correct OpenAI realtime model ID in e2e gateway config

openai/gpt-realtime is not a valid model; replace with the correct
openai/gpt-4o-realtime-preview model ID to prevent model-not-found
errors when running the openai-realtime e2e tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* revert: restore openai/gpt-realtime model ID

gpt-realtime is a valid model; reverting the unnecessary change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: resolve UP006 violations, mock test failures, and stale spec field

- Guard gemini setup-without-tools deferral with litellm.gemini_live_defer_setup
  flag so the default (False) path sends setup immediately, fixing two failing
  mock tests: test_client_ack_caches_setup_to_prevent_duplicate_session_update_setup
  and test_deferred_setup_sends_session_update_before_buffered_audio
- Replace deprecated typing generics (Dict, List, Tuple, Optional) with builtin
  equivalents in xai/realtime/transformation.py, gemini/realtime/transformation.py,
  and realtime_streaming.py to satisfy the UP006 ruff-strict ceiling
- Remove 'role' from OpenAPI compliance test expected fields; Google removed it
  from the Interaction schema in their live spec

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: use Optional[dict] in xai normalizer to preserve Black line-split

dict[str, Any] | None is shorter than Optional[Dict[str, Any]] by enough
that Black collapses the _normalize_usage signature to a single line
(86 chars), conflicting with the existing multiline format. Using
Optional[dict[str, Any]] keeps the line at 90 chars (> 88 limit) so
Black preserves the multiline shape, while still satisfying UP006 by
replacing Dict with dict.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: remove proxy-level setup-tools deferral, delegate to transformer

The _gemini_setup_deferred / _gemini_pre_setup_buffer block in
_send_to_backend was double-deferring: GeminiRealtimeConfig already
handles the session.update-to-setup mapping internally and always
returns a ready-to-send setup on the first session.update call
(session_configuration_request=None). The proxy layer was incorrectly
holding back that setup waiting for tools that the transformer had
already incorporated.

Removing the block fixes two failing tests:
  test_client_ack_caches_setup_to_prevent_duplicate_session_update_setup
  test_deferred_setup_sends_session_update_before_buffered_audio

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: abstract Gemini protocol keys out of core and use cost map for live model detection

Move Gemini-specific message key knowledge (setup, realtimeInput, clientContent,
toolResponse) out of the core RealTimeStreaming module into provider-level methods.
BaseRealtimeConfig gains is_setup_message and is_content_message (both default False);
GeminiRealtimeConfig overrides them with the actual Gemini key checks.

Add gemini_native_audio and gemini_audio_only_live capability flags to the 10
affected model entries in the cost map. _is_audio_only_live_model and
_is_native_audio_model now read from the cost map first and fall back to the
existing string markers for models not in the map.

* fix: apply black formatting and register gemini capability fields in schema

* refactor: drop string-marker fallback; resolve audio-only live models via cost map only

* fix: use registered cost-map model name in vertex realtime tests

* fix: patch cost map in tests so they don't depend on remote main branch state

* fix: align gateway config vertex-realtime model ID with cost-map registered name

* fix: patch gemini-2.5-flash-native-audio in cost map fixture for CI

* fix(e2e): use correct OpenAI realtime model id in gateway config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(e2e): add budget rescheduler short intervals to gateway config

Without proxy_budget_rescheduler_min/max_time set, the rescheduler
defaults to ~600s, causing all budget-reset e2e tests to timeout
before the reset fires. Set to 5–10s so tests complete within 90s.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(e2e): strip non-realtime files from PR scope

Restore budget, spend-tracking, and custom-pricing test files to their
litellm_internal_staging state. Keep the mode field addition to
CustomPricing in models.py (needed by realtime configured_models filter).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(tests): restore async_realtime regression test and add missing fixture

- Restore the end-to-end async_realtime regression test for Vertex
  query-param forwarding; the previous unit-only version did not exercise
  the code path where the original bug lived
- Add patch_gemini_audio_cost_map_entries fixture to
  test_gemini_audio_only_live_models_drop_text_from_text_audio_combo
  so it does not depend on the cost map having gemini_audio_only_live
  set in CI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(lint): resolve ANN401 violations in realtime streaming code

Define RealtimeEventNormalizer Protocol and replace bare Any annotations
with typed alternatives (object for event/value params, the Protocol for
the normalizer) to stay within the strict-rule budget.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style: black format realtime_streaming.py

* fix(tests): add gemini_native_audio and gemini_audio_only_live to model prices schema

* fix(lint): fix I001 import sort order in realtime_streaming.py

* fix(lint): restore import litellm to correct position before from-litellm imports

* undo budget removal

* test(e2e): pin explicit credentials for gemini and vertex realtime models

* test(e2e): share keepalive-safe LiteLLMRealtimeLLMService across pipecat suites

The pipecat smoke test drove the proxy through the stock OpenAIRealtimeLLMService,
which sends websocket keepalive pings at its default interval. The proxy does not
answer them, so the connection is closed with a 1011 before the run completes.
Move the proxy-aware LiteLLMRealtimeLLMService (keepalive disabled) into a shared
pipecat_service module and use it from both the smoke and audio suites.

* test(e2e): document that LiteLLMRealtimeLLMService._connect keeps the ?model= param

The proxy routes realtime websockets on the ?model= query param, and pipecat's
OpenAIRealtimeLLMService.__init__ bakes it into self.base_url before _connect
runs. Passing self.base_url through preserves it; spell that out so the override
is not misread as dropping the param.

* fix(realtime): set _content_sent_after_setup only after the backend send succeeds

A failed content send used to flip _content_sent_after_setup to True before the
send was confirmed, mirroring the correct-on-failure ordering the adjacent
session-config cache already follows. If the send raised, the flag stayed True
and a later session.update that produced a setup frame was silently dropped even
though the backend never received any content. Set the flag after the send
succeeds and add a regression test that fails if the ordering is reverted.

* fix: normalize realtime passthrough events

* refactor(realtime): declare patch_outgoing_session on normalizer Protocol; fix wav chunk return type

The RealtimeEventNormalizer Protocol only declared should_drop and normalize,
so the outgoing session.update patch went through a getattr(..., None) lookup
even though should_drop/normalize are called directly. The sole implementer
(XAIRealtimeNormalizer) already provides patch_outgoing_session, so declare it
on the Protocol and call it directly for consistent, fully-typed dispatch.

Also correct _load_wav_chunks' return annotation from list[bytes] to
tuple[list[bytes], int]; it returns (chunks, sample_rate) and the caller
unpacks both.

---------

Co-authored-by: mubashir1osmani <mubashir.osmani777@gmail.com>
Co-authored-by: Mateo Wang <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 09:36:49 -07:00
Mateo Wang
bc0cb24606
fix(cost): restore per-query Gemini 3.x web search billing (#31363)
* fix(cost): restore per-query Gemini 3.x web search billing

* fix(cost): adopt resolved provider in web search prefix fallback

The provider-prefix fallback in _handle_web_search_cost re-resolved
model_info from the model's prefix but kept the original
custom_llm_provider for routing. A non-Gemini "/"-containing model whose
initial lookup failed (e.g. openrouter/google/gemini-3.1-flash-lite, which
carries no web search pricing) was therefore re-resolved and then fed into
the vertex_ai Gemini calculator, which charged its $0.035 per_prompt
default. Adopt the provider from the re-resolved model_info so the cost is
always routed and priced with the model that was actually resolved.

Tests now derive the expected per-query and per-prompt web search costs
from the loaded cost map instead of pinning literals, and add a regression
asserting a non-Gemini prefixed model with no web search pricing is not
mis-charged via this fallback.

* refactor(types): narrow web_search_billing_unit to a Literal

Only "per_query" and "per_prompt" are meaningful for this field, so a
Literal narrows the type at call sites (an unknown billing unit becomes a
type error) and matches the existing Literal-typed mode field on the same
TypedDict, instead of leaving it as a coarse str.

* test(cost): isolate local cost map mutation behind a monkeypatch fixture

The Gemini web search billing tests set LITELLM_LOCAL_MODEL_COST_MAP and
reassigned litellm.model_cost without teardown, leaking that global state
into later tests. Move both into a local_model_cost_map fixture using
monkeypatch.setenv / monkeypatch.setattr so they auto-restore.
2026-06-26 09:25:35 -07:00
Sameer Kankute
133da06aa3
chore: litellm oss staging (#31185)
* fix(ui): widen Y-axis gutter on Usage charts so large token/request labels aren't clipped

The Total Tokens Over Time and Total Requests Over Time AreaCharts on the
Usage page used Tremor's default yAxisWidth (~56 px), which is too narrow
once totals pass the hundred-million mark — leading digits of labels like
"100.00M" / "4500.00M" got clipped against the chart edge. The requests
chart was worse: it formatted with toLocaleString(), so billion-scale
request counts produced "1,000,000,000" (13 chars) and overflowed
immediately.

Fix in two places so neither alone has to carry the whole margin:
- activity_metrics.tsx: add yAxisWidth={80} to both AreaCharts, and
  switch the requests chart to the shared valueFormatter so it uses the
  same compact k/M/B suffixes as the tokens chart.
- value_formatters.tsx: add a >= 1e9 branch to valueFormatter /
  valueFormatterSpend that emits a "B" suffix (4.50B, $4.50B), keeping
  every formatted label at most 7 chars.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>

* Update ui/litellm-dashboard/src/components/UsagePage/utils/value_formatters.tsx

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* docs(readme): add Deploy on AWS/GCP with Terraform section

Adds a quickstart for the two published Terraform modules on the public
registry (BerriAI/litellm/aws and BerriAI/litellm/google). Copy-paste
main.tf for each cloud, the one-time GCP Artifact Registry remote-repo
command, and pointers to the registry pages for the full input surface.

Sits inside the Get Started section, between the gateway/SDK table and
Run in Developer Mode -- where someone scanning the README for "how do I
deploy this" will land.

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

* docs(readme): add 1-click deploy buttons for AWS + GCP

GCP gets the real 1-click: Open in Cloud Shell badge that clones the repo
and walks through `terraform apply` via the existing DeployStack
tutorial (already shipped at terraform/litellm/gcp/examples/default/
TUTORIAL.md). User just picks a project.

AWS gets a soft 1-click: a Launch in AWS CloudShell badge that opens an
in-browser, already-authenticated shell. User runs four commands
(clone + cd + cp tfvars + terraform apply) once inside. There's no
native AWS deeplink that pre-clones a repo + runs a tutorial -- CFN
"Launch Stack" + CodeBuild would be needed for that, and that's a
separate piece of work.

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

* docs(readme): move AWS + GCP deploy buttons next to Render button

* docs(readme): unify deploy button sizes and badge styles

* docs(readme): bump deploy button height to 48 to match Render/Railway

* docs(readme): bump AWS/GCP badge height to compensate for SVG padding

* docs(readme): bump AWS/GCP badge height to 72

* docs(readme): bump AWS/GCP badge height to 84

* fix(readme): make deploy buttons same height (48px)

https://claude.ai/code/session_01MxQRMHSDXbqJh74rF86UBc

* docs(readme): flag GCP project ID substitution in image_registry

* docs(readme): equalize deploy button heights and fix Cloud Shell button font

GitHub rewrites an image's height attribute to "height: auto; max-height: Npx", which only caps and never stretches, so each image renders at its intrinsic height. The AWS/GCP shields badges are intrinsically 28px while the Render/Railway buttons are 40px, leaving the row uneven regardless of the height="48" we set. Replace the two shields badges with committed 40px PNGs so all four header buttons render at the same 40px.

Also swap the Cloud Shell button from open-btn.svg to open-btn.png. The SVG renders its label as live text with font-family "Roboto, Sans" and no generic fallback; since neither font exists in GitHub's render environment, the text fell back to a serif (Times New Roman). The PNG bakes in the correct typeface.

* docs(readme): collapse Railway deploy anchor to a single line

The Railway button wrapped its img across indented lines, so the anchor contained leading and trailing whitespace. GitHub underlines link content, rendering that whitespace as a small blue underline beside the button. Put the anchor on one line like the other three buttons so there is no inner whitespace to underline.

* Add Claude Fable 5 cost map entries as a data-only hotfix

Backports only the model map changes from #30064 so deployments on
released litellm versions pick up Fable 5 pricing, context window, and
the adaptive thinking flag through the hosted cost map fetch without
upgrading. Includes the supports_sampling_params flag on the 28
Fable 5 / Opus 4.7 / Opus 4.8 entries (ignored by released code, read
by the gating that ships with the next release) and the matching
one-line schema declaration so the map validation test passes.

https://claude.ai/code/session_01MZarYYT3aS7DxaNjoax6Gm

* fix: correct context window tokens for GPT-5 Pro and GPT-5.4 Mini/Nano

Three bugs in model_prices_and_context_window.json:

1. gpt-5-pro and gpt-5-pro-2025-10-06: max_input_tokens and max_tokens
   were SWAPPED. GPT-5 Pro has a 400K context window (input) with 128K
   max output, but the values were set as max_input=128000,
   max_tokens=272000. This caused token limit errors when sending
   prompts over 128K tokens to GPT-5 Pro.

2. gpt-5.4-mini and gpt-5.4-mini-2026-03-17: max_input_tokens was
   272000, but GPT-5.4 Mini shares the same 1,050,000 token context
   window as GPT-5.4. This was inconsistent with the azure/ variants
   which already correctly had 1,050,000.

3. gpt-5.4-nano and gpt-5.4-nano-2026-03-17: same issue as Mini,
   max_input_tokens was 272000 instead of 1,050,000.

Source: OpenAI model documentation and contextwindows.dev which
aggregates official context window sizes.

Fixes #30928 (partially — the issue incorrectly claims gpt-5/gpt-5-mini
should be 400K; their 272K values are correct per OpenAI docs)

* fix: also correct max_output_tokens for gpt-5-pro (272000→128000)

Per reviewer feedback, max_output_tokens was left at 272000 while
max_tokens was corrected to 128000, causing an internal inconsistency.
Both should be 128000 per OpenAI docs.

* fix(cost): price gpt-image generated output tokens as image tokens (#31147)

The OpenAI Images endpoints (/v1/images/generations, /v1/images/edits) return
usage with no output token breakdown — litellm's `ImageUsage` has no
`output_tokens_details` field — so generated-image OUTPUT tokens were priced at
the text rate (`output_cost_per_token`) instead of the image rate
(`output_cost_per_image_token`). For gpt-image-2 that is $10/1M vs $30/1M, a ~3x
undercount on the dominant cost component (image output is ~74% of spend). This
also affects azure gpt-image, which shares this calculator.

The OpenAI gpt-image cost calculator re-implemented usage handling instead of
reusing `calculate_image_response_cost_from_usage`, the shared helper that
azure_ai/gemini/vertex_ai already use. That helper classifies generated output
tokens as image tokens when the provider does not itemize output, and splits
text/image when it does.

Fix: route the ImageUsage path through `calculate_image_response_cost_from_usage`
(pre-transformed chat Usage objects are still costed directly). Adds a regression
test for the no-breakdown ImageUsage case (gpt-image-2).

* fix(bedrock): route application-inference-profile ARNs to converse (#18258) (#31098)

A bare application-inference-profile ARN passed as bedrock/arn:... fell
through to the invoke route, which cannot derive a provider from the
opaque profile id and raised 'Unknown provider=None'. The converse route
needs no provider, so detect these ARNs in get_bedrock_route and route
them to converse, matching the behavior of the already-documented
bedrock/converse/arn:... workaround.

Explicit invoke/ prefixes still win, and they remain a dead end for these
ARNs by design (no provider derivable). System-defined inference-profile
ARNs that embed a known model, and other opaque ARN types
(provisioned-model, imported-model, custom-model-deployment) that are
frequently invoke-only, are deliberately left on their current routes;
tests guard both boundaries.

* fix(moonshot): stop mutating caller messages on tool_choice='required' (#31060)

_add_tool_choice_required_message appended the "select a tool" prompt to
the caller's messages list in place, so transform_request corrupted the
caller's conversation history and appended a duplicate prompt on every
retry. Build and return a new list instead so the call stays idempotent.

Adds a regression test asserting the input messages list is unchanged
across repeated transform_request calls.

Co-authored-by: Wassbdr <wassim.badraoui07@gmail.com>

* fix(transcription): accept fractional usage.seconds in diarized_json responses (#30996)

gpt-4o-transcribe and compatible ASR backends return a diarized_json
response with usage={"type": "duration", "seconds": <float>}, e.g. 295.8.
TranscriptionUsageDurationObject typed seconds as int, so parsing the
response raised a pydantic ValidationError (int_from_float). That error
surfaces as an APIConnectionError which the router treats as retryable, so
it keeps re-calling the upstream (200 every time) until the upstream
rate-limits and returns 429 to the caller.

OpenAI specs this field as a float (see openai SDK UsageDuration.seconds),
so widen seconds to float. With the parse succeeding there is no exception
left to retry, which removes the loop.

Co-authored-by: Neimar Avila <19142978+neimaravila@users.noreply.github.com>

* fix(deepseek): drop non-function tools before chat completions call (#30910)

* fix(deepseek): drop non-function tools before chat completions call

DeepSeek's /chat/completions only accepts tools of type "function".
Requests bridged from /v1/responses can carry responses-API-native tool
types, for example a Codex CLI tool typed "namespace", which DeepSeek
rejects with "unknown variant 'namespace', expected 'function'" so the
whole request fails (issue #30722).

Filter unsupported tool types in the DeepSeek request transform so the
function tools still go through; when nothing callable remains, also drop
the now-dangling tool_choice and parallel_tool_calls

Fixes #30722

* test(deepseek): cover async tool filtering and document tool_choice assumption

Add an async_transform_request regression test so the sync and async tool
filtering paths cannot silently diverge, and document in _drop_unsupported_tools
that only non-function tools are dropped, so a function-named tool_choice always
references a surviving tool

* feat(catalog): add zai/glm-5.1, zai/glm-4.7-flash, openrouter/z-ai/glm-5.1 (#29840)

* feat(ui): surface team budget on key overview when key has no own budget (#30801)

* feat(ui): surface team budget on key overview when key has no own budget

* fix(ui): replace IIFE with derived variable and use find() for team budget display

* fix(anthropic): emit replayable streaming thinking blocks (#31022)

* feat(proxy): read cold-storage prompts back in the logs detail view (#30364)

* feat(proxy): read cold-storage prompts back in the logs detail view

When a deployment offloads prompts and responses to cold storage instead of
Postgres, the spend-log row holds only "{}" placeholders plus a
metadata.cold_storage_object_key pointer, so the UI logs detail drawer showed
nothing. The detail endpoint only read the placeholder columns and never
fetched the object back.

Resolve the payload per row based on actual content, not a config flag: if
Postgres has content, return it; otherwise read the exact stored object key and
fetch from the configured cold storage backend through ColdStorageHandler.
Reading the persisted key is a single GET. The key embeds a microsecond
timestamp that cannot be reconstructed from the millisecond-precision startTime
column, and listing the day's prefix to match on request_id would be too
expensive for this per-open path.

Also teach the detail drawer's pretty-view parser to accept a bare messages
array. The cold storage payload carries the prompt as a top-level messages list
with no proxy_server_request, so without this the output rendered while the
input stayed blank.

ColdStorageHandler gains an optional injected logger so the resolver can be unit
tested without monkeypatching. Postgres-stored prompts are unaffected: the fast
path returns the existing columns and the request-body object still renders the
same way.

* Update litellm/proxy/spend_tracking/spend_management_endpoints.py

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* test(proxy): cover ColdStorageHandler resolution paths and cold-storage fetch failure

Add unit tests for ColdStorageHandler (injected logger, graceful None when no
logger is configured, and resolution of a configured logger from the callback
registry) and a regression test asserting a cold storage backend exception
degrades to the Postgres values instead of surfacing a 500.

---------

Co-authored-by: Bytechoreographer <Bytechoreographer@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix(mavvrik): advance metricsMarker after upload; fix scheduler startup (#31068)

* fix(mavvrik): advance metricsMarker after upload + fix scheduler startup

Two bugs fixed:

1. deliver() never called PATCH /metrics/agent/ai/{connectionId} after a
   successful GCS upload, so metricsMarker stayed at 0 and every daily run
   re-exported the same dates in an infinite catch-up loop.
   Fix: add _update_metrics_marker(date_epoch) called at the end of deliver()
   after _upload_to_gcs() succeeds. A 4xx warns but does not raise (the GCS
   file is already committed). A 410 raises consistent with the rest of the
   destination.

2. init_mavvrik_focus_background_job runs at proxy startup before any LLM call
   has triggered lazy instantiation of MavvrikFocusLogger, so it found no
   logger instance and silently skipped registering the daily export job.
   Fix: if no instance is found but "mavvrik" is in litellm.callbacks, call
   _init_custom_logger_compatible_class to force instantiation before
   the APScheduler job is registered.

* fix(mavvrik): catch up from earliest window when metricsMarker=0

When the connector is freshly registered, metricsMarker=0 parses to None.
The catch-up block was guarded by `if last_ingested and ...` which skipped
it entirely for None, so only yesterday was exported instead of the full
_MAX_CATCHUP_DAYS window.

Fix: treat None as being _MAX_CATCHUP_DAYS behind (start from earliest_catchup).
The existing > 7 day warning only fires for non-None markers that are old.

* fix(mavvrik): use now as end_time for yesterday's export window

LiteLLM_DailyUserSpend rows for a given date get their updated_at
bumped by the spend flush job throughout the next morning. The core
database query filters on updated_at, so capping end_time at midnight
(yesterday + 1 day) missed any spend rows flushed after midnight.

Fix: pass now (cron fire time) as end_time for the daily "yesterday"
window so all fully-settled rows are captured regardless of when the
flush job ran.

Verified: claude-3-5-sonnet BilledCost went from 0.0 to ~$2.40 per
row in the exported FOCUS CSV.

* fix(mavvrik): also use now as end_time for catch-up windows

* fix(mavvrik_focus): pass required args to _init_custom_logger_compatible_class

Calling it with only logging_integration raised TypeError at proxy startup
because internal_usage_cache and llm_router have no defaults. Also fix test
name to reflect the actual status code (5xx not 4xx) used in the mock.

* ci: retrigger CI run

* feat: pass through optional `instruction` field in the rerank API (vLLM/Qwen3-Reranker) (#30757)

* Add optional `instruction` passthrough to the rerank API

vLLM's /v1/rerank and /v1/score accept an optional top-level `instruction`
field (folded into the model's chat_template_kwargs and consumed by the
chat template — e.g. Qwen3-Reranker). LiteLLM's managed rerank route silently
dropped it: RerankRequest / OptionalRerankParams had no such field, so the
outgoing body was rebuilt without it.

Thread an opt-in `instruction: Optional[str]` through rerank()/arerank(),
get_optional_rerank_params, and the hosted_vllm transformation into the
request body, only when non-None. When callers omit it, model_dump(exclude_none)
drops the field and the outgoing request is byte-for-byte unchanged — fully
backward-compatible. (DeepInfra already forwards `instruction` via
non_default_params; this formalizes the field in the shared types.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Address review: thread `instruction` as a typed param + cover rerank_utils

Per PR review (greptile P2 + codecov):

- Make `instruction` a typed, named argument on the rerank provider interface
  instead of recovering it from the opaque `non_default_params` blob. Adds
  `instruction: Optional[str] = None` to `BaseRerankConfig.map_cohere_rerank_params`
  and every provider override, and forwards it explicitly from
  `get_optional_rerank_params`. hosted_vllm now reads the named param directly.
  It is still also surfaced in `non_default_params` so providers that read it
  there (e.g. DeepInfra) keep working now that `rerank()` consumes `instruction`
  as a named param rather than leaving it in **kwargs.
- Add get_optional_rerank_params unit tests (present + absent) to cover the
  previously-uncovered threading line flagged by codecov.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: scan rerank `instruction` through request guardrails

The rerank guardrail translation (CohereRerankHandler.process_input_messages)
only scanned `query`, so the newly added `instruction` field reached the
backend model unscanned. Since instruction-aware rerankers (hosted vLLM /
Qwen3-Reranker) fold `instruction` into the prompt, an authenticated caller
could place content there to bypass configured rerank request guardrails.

Generalize the handler to scan every user-controlled text field (`query` and
`instruction`) in one apply_guardrail call and write each sanitized value back
by index. Query-only requests are unchanged (single-element list at index 0);
non-string fields are left untouched. Adds tests covering instruction
scanning, PII masking write-back, and the non-string case.

Addresses the Veria AI security review on PR #30757.

* test: narrow Optional results before len() to satisfy basedpyright budget

The lint gate (basedpyright delta-vs-base budget) flagged one new
reportArgumentType: len(result.results) where results is
List[RerankResponseResult] | None. Assert results is not None first to
narrow the type before len()/indexing.

* fix: read rerank `instruction` from kwargs to satisfy basedpyright budget

The basedpyright delta-vs-base gate flagged one new reportArgumentType: the
Router forwards rerank calls via an untyped `**kwargs` unpack
(`litellm.arerank(**{**data, **kwargs})`), and declaring `instruction` as a
typed named param on the public `rerank`/`arerank` entrypoints made pyright
check that key against `str | None`, adding an error at router.py with no real
safety gain. Read `instruction` from kwargs in `rerank` instead.

It remains fully typed where it matters - threaded as a typed argument through
`get_optional_rerank_params` and each provider's `map_cohere_rerank_params`
(the original Greptile P2 ask). Whole-repo reportArgumentType is back to the
base count (net 0); rerank hosted_vllm + cohere guardrail suites pass; ruff clean.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(github_copilot): synthesize empty choices at the provider seam (#30929)

Newer Copilot Claude models (opus-4.7, opus-4.8) return responses with
choices=[], either carrying Anthropic-native content blocks or, for the
max_tokens=1 probe Claude Code sends, no content at all. github_copilot
is dispatched through the OpenAI SDK handler, which calls
convert_to_model_response_object directly and never invokes
GithubCopilotConfig.transform_response, so the empty-choices guard there
surfaced as a 500

Instead of synthesizing choices inside the shared
convert_to_model_response_object (which would silently turn empty choices
into a fabricated success for every provider), add a no-op
transform_parsed_response_dict hook on BaseConfig. GithubCopilotConfig
overrides it to synthesize choices from Anthropic-native content, reusing
its existing parsing, and the OpenAI SDK handler routes its parsed
response through the hook before generic conversion. The core utility
keeps treating empty choices as an error for all other providers

Fixes: https://github.com/BerriAI/litellm/issues/30927

Signed-off-by: David J. M. Karlsen <david@davidkarlsen.com>

* fix(router): stop fallback lookups from mutating the router fallbacks config (#30624)

* fix: correct amazon.titan-embed-text-v2 input price to $0.02/1M tokens (#29693)

* fix: correct amazon.titan-embed-text-v2 input price to $0.02/1M tokens

* test: scope local cost map env var with monkeypatch to avoid test pollution

* fix(sensitive_data_masker): fully mask secrets at or below the reveal threshold (#30764)

* fix(sensitive_data_masker): fully mask secrets at or below the reveal threshold

_mask_value did partial reveal by showing the first visible_prefix and last
visible_suffix characters, but for a value whose length was at or below
visible_prefix + visible_suffix (8 by default) it returned the value verbatim.
A value of exactly 8 chars fell through the length guard and computed
masked_length == 0, reconstructing the original string with no mask characters;
anything shorter hit the early return. Either way short credentials were emitted
in plaintext.

mask_dict routes real secrets through this path, so an 8-char-or-shorter redis
password, api key, or token could be written to logs and the UI unmasked. The
sibling helper mask_sensitive_keys already guards this case; _mask_value now does
the same by fully masking any value at or below the threshold.

* fix(sensitive_data_masker): add mask_short_values opt-out for truncation callers

Fully masking short values is the right default for secret masking, but
CooldownCache reuses the masker purely to truncate exception messages to the
first 50 characters, and it relies on short messages being returned readable.
Masking those blanked out short exception text and broke its tests.

Add a mask_short_values flag (default True, secure) and have CooldownCache pass
False so it keeps the truncation behavior, while every secret-masking caller
still gets short values fully masked.

* fix(mcp_debug): opt out of short-value masking to keep diagnostic token preview

MCPDebug uses the masker to preview auth tokens in debug headers and documents
that values of 10 chars or fewer are shown unchanged so token types stay
distinguishable. Pass mask_short_values=False so that diagnostic behavior is
preserved while secret maskers keep masking short values.

* fix(mcp_debug): mask short auth values in debug headers instead of echoing them

Earlier this masker opted out of short-value masking to keep a token preview, but
that echoes short authorization and token values verbatim in debug response
headers, which is the same leak this change is meant to close. Auth material
should never be emitted in full, so mask short values here too; the first/last
character preview still applies to longer tokens. Only CooldownCache keeps the
opt-out, since it truncates exception text rather than masking secrets.

* test(mcp_debug): assert masked short value preserves length

* refactor(fireworks_ai): remove deprecated audio transcriptions endpoint (#30917)

Fireworks AI deprecated audio inference on 2026-06-10
(https://docs.fireworks.ai/updates/changelog#audio-inference-and-image-generation-deprecation).
Live API testing confirms the endpoint is already non-functional: a valid
Fireworks API key receives HTTP 401 "Unauthorized" from
api.fireworks.ai/inference/v1/audio/transcriptions for every request,
regardless of payload. The audio-prod.api.fireworks.ai host referenced in
the test suite returns 401 for every path; the entire host is decommissioned.

Remove the dead FireworksAIAudioTranscriptionConfig class and every
reference to it across the codebase:

- Delete litellm/llms/fireworks_ai/audio_transcription/ directory (17-line
  config class that inherited from OpenAIWhisperAudioTranscriptionConfig)
- Remove the Fireworks branch from
  ProviderConfigManager.get_provider_audio_transcription_config() in
  litellm/utils.py; update the stale comment in
  get_optional_params_transcription that referenced fireworks ai
- Remove the FireworksAIAudioTranscriptionConfig entries from
  LLM_CONFIG_NAMES and _LLM_CONFIGS_IMPORT_MAP in
  litellm/_lazy_imports_registry.py
- Remove the TYPE_CHECKING re-export in litellm/__init__.py
- Remove the transcription branch in the fireworks_ai case of
  get_supported_openai_params() in
  litellm/litellm_core_utils/get_supported_openai_params.py
- Remove the whisper-v3 and whisper-v3-turbo entries from
  model_prices_and_context_window.json and
  litellm/model_prices_and_context_window_backup.json (both had
  mode: audio_transcription and zero-cost pricing)
- Remove the TestFireworksAIAudioTranscription test class and its
  imports from tests/llm_translation/test_fireworks_ai_translation.py

No other provider is affected. The openai_compatible_providers list,
FireworksAIMixin, and the OpenAI Whisper transcription handler all stay
because they are shared with other Fireworks endpoints and other
providers. The provider_endpoints_support.json registry already had
audio_transcriptions set to false for fireworks_ai.

* feat: add darkbloom provider (#30876)

* feat: add darkbloom provider

* fix: document darkbloom provider endpoints

* fix: address darkbloom review feedback

* fix: update darkbloom tool metadata

* fix: fail fast for non-Postgres database URLs (#30883)

* fix(proxy): fail fast on non-PostgreSQL DATABASE_URL instead of hanging on startup

LiteLLM's Prisma datasource is pinned to provider = 'postgresql', so a sqlite:// or mysql:// DATABASE_URL can never connect.

Today that surfaces as an opaque startup stall where the port never binds, and a separate 'DB not connected' 500 on /key/generate when no DATABASE_URL is set at all leaves operators guessing what to configure.

Validate the DATABASE_URL / DIRECT_URL scheme in run_server before any Prisma call and exit with an actionable message naming the unsupported scheme.

Also reword CommonProxyErrors.db_not_connected_error to tell the operator to set DATABASE_URL to a postgresql:// connection string.

Add regression tests covering postgres acceptance and sqlite/mysql/mssql rejection.

* fix: resolve CI failures and proxy DB URL typing issue

* fix(proxy): fail fast on non-PostgreSQL DATABASE_URLs with clear startup errors instead of hanging

* Validate DIRECT_URL alongside DATABASE_URL startup guards

* fix(bedrock): surface modeled HTTP status for mid-stream error events so 5xx is retryable (#24608) (#30946)

* fix(bedrock): surface modeled HTTP status for mid-stream error events (#24608)

* test(bedrock): mid-stream server errors trigger streaming fallback (#24608)

* style(bedrock): black-format stream-error helper (#24608)

* fix(mcp): re-land native tool preservation with typed annotations (#30645)

* fix(mcp): preserve native tools in semantic filter hook with typed annotations

* fix(mcp): tighten _is_mcp_tool Chat Completions shape check

* fix(sambanova): return embeddings supported params instead of dropping them (#30937)

* fix(router): send fallback metadata when streaming (#30914)

When a streaming request triggers a fallback, there was previously no way to
know it happened. This commit addresses this in a few ways:

1. The response now correctly populates the fallback headers
    (`x-litellm-attempted-fallbacks`) so callers know a fallback happened.
2. The correct model ID is passed in the streaming chunks.
3. A streaming chunk with the fallback error can be optionally sent back
    to the client (opt-in) by passing `include_fallback_errors: true` in
    the request.

The format of the fallback errors while streaming is intentionally OpenAI
compatible to not break existing libraries that parse these events. It was
tested with Vercel's AI SDK (ai-sdk.dev). It is also opt-in, so it is not
delieved unexpectedly to callers by default.

* fix(mistral): drop output-only reasoning fields from input messages (#30884)

LiteLLM attaches reasoning_content and thinking_blocks to assistant
responses. Replaying those assistant turns verbatim forwarded the fields
back to Mistral, whose input schema forbids unknown keys, so the whole
request failed with a 422 extra_forbidden and reasoning models became
unusable across multiple turns.

Strip both fields from assistant messages before the request is built, in
a spot that runs ahead of the image/file branch so it applies on every
path. Fixes #30835

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

* fix(perplexity): bill search queries at the per-request price, not 1/1000 of it (#30652)

* fix(perplexity): bill search queries at the per-request price, not 1/1000

The fallback cost calculator divided search_context_cost_per_query by
1000, but that field stores the per-request price in USD: sonar is
{low: 0.005, medium: 0.008, high: 0.012}, matching Perplexity's published
$5/$8/$12 per 1,000 requests expressed per request. The gemini cost
calculator reads the same field per request with no division (its
docstring calls it "the per-request cost").

The division understated search cost by 1000x on every Perplexity call
that falls back to manual calculation (i.e. when the API does not return
a pre-computed usage.cost). Use the value directly.

Update the tests that had encoded the /1000 factor in their expectations,
and drop an unused import flagged by ruff in the touched test file.

* test(perplexity): update integration test search-cost expectations to per-request

The integration tests still encoded the old /1000 search-cost factor, so
they failed once the fallback calculator was corrected to bill
search_context_cost_per_query per request. Update the four expected-cost
computations (and the high-volume dollar-value comments) to match.

* test(perplexity): drop unused mock imports flagged by ruff

* fix: include model_access_groups when expanding all-team-models in get_team_models (#30622)

* fix(fireworks_ai): return None for transcription in get_supported_openai_params

Fireworks AI deprecated audio inference on 2026-06-10; the endpoint is
decommissioned. Without an explicit transcription branch, requests with
request_type='transcription' fell through to the else and returned
FireworksAIConfig chat-completion params. Return None instead to signal
the provider does not support transcription.

* fix(proxy): gate include_fallback_errors behind expose_fallback_errors_to_caller setting

Without an operator gate, any authenticated caller could set include_fallback_errors=True,
trigger a fallback, and read raw upstream exception messages from the
x-litellm-fallback-errors header and the litellm-fallback-metadata SSE event.

Strip include_fallback_errors from request data in common_processing_pre_call_logic
when expose_fallback_errors_to_caller is not set, so the router never builds the
error list. Also gate _should_include_fallback_errors on the same setting as a
secondary check for the streaming SSE injection path.

* test(proxy): opt in to expose_fallback_errors_to_caller in streaming SSE test

The operator gate added in e7ff3e1 means include_fallback_errors is only
honoured when general_settings.expose_fallback_errors_to_caller is True.
Set that flag via monkeypatch in the test that exercises the emit path.

* test(prompt_templates): make test_convert_url hermetic instead of hitting picsum.photos

test_convert_url called convert_url_to_base64 against a live picsum.photos
URL and asserted nothing, so it added no real signal and broke CI whenever
the host was unreachable (it was returning 522 and blocking this branch).
Replace the live call with a mocked HTTP client and assert the produced
base64 data URL, so the conversion path is exercised deterministically with
no network dependency. This suite runs under VCR, which is why a transport
level mock (respx) does not reliably intercept; mocking the client object
itself is robust regardless.

* fix(interactions): drop role from Interaction response to match Google spec

Google removed the output-only role field from the Interaction schema (it
now lives only on Turn), so the live OpenAPI compliance canary started
failing with 'role' not in spec. Reconcile our generated types by removing
role from Interaction, CreateModelInteractionParams, CreateAgentInteractionParams
and from the LiteLLM InteractionsAPIResponse/InteractionsAPIStreamingResponse,
stop stamping role=model in the responses-to-interactions transformation, and
update the compliance and integration tests accordingly. Turn.role is kept
since the spec still defines it.

* fix: align all-team-models sentinel access

* fix(router): forward include_fallback_errors through multi-hop fallbacks

run_async_fallback received include_fallback_errors as an explicit named
parameter, so it was bound out of **kwargs and never reached the nested
async_function_with_fallbacks call. Multi-hop fallback chains (a fallback
group that itself fails over) therefore stopped collecting fallback errors
beyond the first hop when a caller opted in. Re-inject the flag into kwargs
before the nested call so inner hops keep accumulating errors, which
add_fallback_headers_to_response already merges across levels.

* fix(router): stop fallback lookups from mutating the router fallbacks config

get_fallback_model_group resolved a bare-string fallback by popping it out
of the fallbacks list it was handed. That list is frequently the live
router.fallbacks config, so a single lookup permanently removed the entry and
the configured fallback stopped applying to later requests until restart. The
pop also ran inside enumerate(), shifting indices and skipping an adjacent
string fallback. Read the item instead of popping it, and add a regression
test that fails on the old mutating behavior

---------

Co-authored-by: Srivatsa Kamballa <skamb10@uic.edu>
Co-authored-by: Ahmad Shahzad <107808273+shzdehmd@users.noreply.github.com>
Co-authored-by: Jeremy Chapeau <113923302+jychp@users.noreply.github.com>
Co-authored-by: KRISH SONI <67964054+krishvsoni@users.noreply.github.com>
Co-authored-by: Kent <72616338+kingdoooo@users.noreply.github.com>
Co-authored-by: Ayush Shekhar <106994833+ayushh0110@users.noreply.github.com>
Co-authored-by: dav nguyxn <hoangson091104@gmail.com>
Co-authored-by: Tal Marian <tal.marian@island.io>
Co-authored-by: Hemant K <51333870+hemant1026@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Yash Raj Pandey <55940078+devYRPauli@users.noreply.github.com>
Co-authored-by: Zang Peiyu <166481866+factnn@users.noreply.github.com>
Co-authored-by: Sameer Kankute <sameer@berri.ai>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>

* fix(sambanova): update pricing, deprecate retired models, and add missing models (#30016)

* feat(bedrock): add amazon.titan-embed-g1-text-02 embedding model support

- Add model to provider routing allowlist in embedding.py
- Add request transformation using AmazonTitanG1Config
- Add response transformation using AmazonTitanG1Config
- Add pricing metadata to model_prices_and_context_window.json
- Add unit tests for embedding and model info

Fixes missing cost tracking reported in #29786
Related to VANDRANKI/litellm PR #29790

* style: fix syntax error, trailing whitespace and missing newline

* style: apply black formatting to embedding.py

* style: apply black formatting to test_bedrock_embedding.py

* fix(sambanova): update pricing, fix context windows, add deprecation dates, and add missing models

* fix(sambanova): sync model_prices_and_context_window_backup.json with primary

* fix(sambanova): fix indentation on Meta-Llama-3.2-1B-Instruct deprecation_date

* fix(bedrock): add amazon.titan-embed-g1-text-02 to unmapped model error message

* style: apply black formatting to embedding.py

* fix(sambanova): correct indentation on DeepSeek-V3.2 entry

* fix(sambanova): replace gemma-3-12b-it with gemma-4-31B-it (verified pricing)

* fix(utils): preserve arbitrary above-threshold tiered pricing keys in get_model_info (#30880)

* fix(utils): preserve arbitrary above-threshold tiered pricing keys in get_model_info

get_model_info rebuilt ModelInfo by copying a fixed allow-list of
input/output_cost_per_token_above_<N>_tokens keys (128k/200k/272k/512k), so any other
threshold a user registered was dropped before reaching _get_token_base_cost, which already
reads an arbitrary threshold out of the key name. Custom tiers such as above_500k_tokens were
silently ignored and billing fell back to the base per-token rate. Carry over any
_above_<N>_tokens cost key present on the source cost-map entry that the fixed fields miss

Fixes #30344

* test(cost): keep suite hermetic by popping the temp tiered-pricing model

Wrap the regression body in try/finally so litellm.model_cost no longer
leaks the litellm-test-non-standard-tier entry into later tests that
iterate or reset the global cost map. Addresses Greptile review thread.

* fix: resolve UP045 lint violations (Optional[X] -> X | None)

Convert Optional[X] type annotations to X | None syntax across rerank
transformations, spend tracking, and other modules to satisfy ruff strict gate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: run black formatting on UP045-fixed files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: remove unused Optional imports after UP045 migration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: black format cold_storage_handler.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): correct OSS staging branch name in guard-main-branch errors

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

* fix: strip trailing zeros from M/B spend formatter

* fix: address focus and streaming edge cases

* feat: add LAR-1 semantic routing strategy

Optional router strategy that picks a deployment tier from
request_kwargs.metadata.lar1 (confidence, evidence, time). Deployments
are tagged with model_info.type (cloud-smart, cloud-fast, local, deep).
Thresholds are configurable via routing_strategy_args. Includes 30 unit
tests and an Ollama example config.

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

* fix(mavvrik): advance metricsMarker on empty-content deliver

When deliver() receives empty content (no spend data for a date), it now
registers with Mavvrik and PATCHes the metricsMarker before returning
instead of short-circuiting. Dates with zero spend no longer stall marker
advancement, preventing unnecessary catch-up API calls on subsequent runs.

* style: black format mavvrik_destination

* fix: handle empty mavvrik exports and lar1 reset

* test: add regression test for _reset_custom_routing_strategy

* fix(test): mock async destination.deliver in mavvrik export window test

* style: ruff format spend_management_endpoints after merge

* fix(router): apply LAR-1 strategy atomically so invalid thresholds don't leave partial state

apply_lar1_routing_strategy set router.routing_strategy to "lar1" before
constructing LAR1RoutingStrategy, whose __init__ validates thresholds via
_normalize_thresholds and raises on a misconfigured (out-of-order or
out-of-range) set. On a live update_settings call with bad thresholds the
router was left advertising routing_strategy="lar1" with no custom selector
bound, while the previous strategy's selectors stayed registered.

Build (and validate) the strategy before mutating any router state, so a
threshold error leaves the router exactly as it was. Add a regression test
that asserts a failed switch keeps the prior strategy intact.

---------

Signed-off-by: David J. M. Karlsen <david@davidkarlsen.com>
Co-authored-by: Bytechoreographer <Bytechoreographer@users.noreply.github.com>
Co-authored-by: Claude Opus 4 (1M context) <noreply@anthropic.com>
Co-authored-by: Rick <26716961+Bytechoreographer@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: shin-berri <shin-laptop@berri.ai>
Co-authored-by: yuneng-jiang <yuneng@berri.ai>
Co-authored-by: Yassin Kortam <yassin@berri.ai>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Krrish Dholakia <krrish+github@berri.ai>
Co-authored-by: xbrxr03 <abrarhabib03@gmail.com>
Co-authored-by: hayden <sktpghks138@gmail.com>
Co-authored-by: Kent <72616338+kingdoooo@users.noreply.github.com>
Co-authored-by: Wassim Badraoui <98709649+Wassbdr@users.noreply.github.com>
Co-authored-by: Wassbdr <wassim.badraoui07@gmail.com>
Co-authored-by: Neimar Avila <neimar.avila@gmail.com>
Co-authored-by: Neimar Avila <19142978+neimaravila@users.noreply.github.com>
Co-authored-by: Jerry-Scintilla <jerrycaocao@126.com>
Co-authored-by: AlexBGoode <me.at.forum@gmail.com>
Co-authored-by: Carsten Boloz <cdboloz1@gmail.com>
Co-authored-by: jesco <team@srswti.com>
Co-authored-by: Praveen Ghuge <pghuge@digitalex.io>
Co-authored-by: Jim Smith <j.h.smith@ieee.org>
Co-authored-by: David J. M. Karlsen <david@davidkarlsen.com>
Co-authored-by: Vedant Agarwal <43557509+Vedant-Agarwal@users.noreply.github.com>
Co-authored-by: Srivatsa Kamballa <skamb10@uic.edu>
Co-authored-by: Ahmad Shahzad <107808273+shzdehmd@users.noreply.github.com>
Co-authored-by: Jeremy Chapeau <113923302+jychp@users.noreply.github.com>
Co-authored-by: KRISH SONI <67964054+krishvsoni@users.noreply.github.com>
Co-authored-by: Ayush Shekhar <106994833+ayushh0110@users.noreply.github.com>
Co-authored-by: dav nguyxn <hoangson091104@gmail.com>
Co-authored-by: Tal Marian <tal.marian@island.io>
Co-authored-by: Hemant K <51333870+hemant1026@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Yash Raj Pandey <55940078+devYRPauli@users.noreply.github.com>
Co-authored-by: Zang Peiyu <166481866+factnn@users.noreply.github.com>
Co-authored-by: bhumikadangayach <139267865+bhumikadangayach@users.noreply.github.com>
Co-authored-by: Ewertonslv <ewertoncom297@gmail.com>
Co-authored-by: carlsonchik <carlsonchik@users.noreply.github.com>
2026-06-26 09:17:44 -07:00
Sameer Kankute
687a62e561
fix(cli): mint per-session agent credential on lite login (#31072)
* fix(cli): mint per-session agent credential on lite login

The `lite login` command was producing a shared UI session token that broke agent use in three ways: a $0.25 budget cap (from max_ui_session_budget) that killed agent sessions in minutes, a fixed identity "cli-jwt-token" shared across every user preventing per-session spend attribution, and auth gated behind EXPERIMENTAL_UI_LOGIN so the token was rejected on default deployments.

This fixes all three. Each login now generates a unique cli-session-{uuid} token with no per-key budget cap (enforced via shared team/user counters instead), and the decrypt path activates for any non-sk- token without requiring EXPERIMENTAL_UI_LOGIN.

* fix(cli): address review feedback on EXPERIMENTAL_UI_LOGIN gate and e2e test

Restore EXPERIMENTAL_UI_LOGIN=false as an explicit opt-out: operators who set it to false keep the old boundary; unset (new default) and true both attempt NaCl decryption, which fails closed for non-blob tokens.

In the e2e test: replace the silent Redis fallback with pytest.skip so a missing Redis instance is explicit rather than silently degrading to a directly-minted token. Write the seeded flow back as JSON (proxy reads it via json.loads on cache fetch) instead of Python repr, and build the updated flow immutably.

* fix(key-management): cap CLI session token delegation budget to team ceiling

A CLI session token intentionally carries max_budget=None to avoid a per-session LLM spend cap. The key-generation delegation check (GHSA-q775-qw9r-2r4g) previously skipped non-admin callers with max_budget=None, treating them as having unlimited delegation authority. This allowed any internal user with a lite login session to mint virtual keys with arbitrary budgets.

Adds is_session_token=True to UserAPIKeyAuth for CLI session tokens and uses the caller's team budget as the delegation ceiling in that case, so the effective limit is min(requested_budget, team.max_budget) rather than unbounded.

* chore: regenerate dashboard OpenAPI types

The is_session_token field added to UserAPIKeyAuth cascades to the
dashboard schema. Regenerate types from the updated OpenAPI spec.

* fix(key-management): block personal key budget delegation from CLI session tokens

When team_table is None (personal key, no team_id in request), the personal key
has no team-budget enforcement at request time. A session token therefore cannot
delegate any explicit max_budget for a personal key -- that would open a budget
bypass path. Block the request with a clear 400 directing the caller to use a
team_id instead.

* test(auth): add unit coverage for non-admin CLI session token production path

* fix(type-check): use model_validate in _return_user_api_key_auth_obj to fix reportArgumentType gate

UserAPIKeyAuth(**user_api_key_kwargs) spread triggers a basedpyright
reportArgumentType error for each named field in UserAPIKeyAuth because
the dict's inferred value type (str | Span | LitellmUserRoles | Unknown)
is not assignable to each field's specific type. Adding is_session_token:
bool introduced +2 more such errors, breaching the gate cap.

model_validate accepts an untyped dict without per-field argument checking,
which eliminates the +2 new errors and also ratchets down the pre-existing
333 errors at those call sites. basedpyright-code-budget.json is updated
to reflect the new lower baseline (1814, down from 1934).

* fix(type-check): ratchet down reportArgumentType baseline only

The previous lint-budget-update captured all baselines from the local
environment, raising many ceilings vs the merge-base and failing the
non-gating budget_ratchet_check. Restore staging's values for every
rule and only lower reportArgumentType (1934 -> 1814) to reflect the
reduction from switching to model_validate in _return_user_api_key_auth_obj.

* fix(auth): set max_budget on CLI session token to enforce max_ui_session_budget

CLI session tokens were missing max_budget, so _virtual_key_max_budget_check
had no per-session ceiling to enforce. Operators relying on max_ui_session_budget
could be bypassed for the full token lifetime. Mirrors the existing UI token path.

* revert(auth): remove max_ui_session_budget from CLI session token

max_ui_session_budget defaults to $0.25 and is sized for the UI chat
pane (10-min sessions). CLI sessions are 24-hour tokens for real work;
capping them at that ceiling would throttle users under their actual
user/team budget. Budget enforcement for CLI sessions is via the shared
user and team counters as originally intended.

* fix(auth): cap CLI session at max_ui_session_budget only when user and team have no budget

When neither the user nor their team has a budget configured, CLI sessions
were fully uncapped. The poll endpoint now looks up the real user and team
objects from DB; if both have no max_budget, it passes litellm.max_ui_session_budget
as the token's per-key ceiling. Users or teams that already have a budget
configured are unaffected and continue to rely on the shared counters.

* fix(auth): fix black formatting and update test mock for cli_poll_key budget lookup

The get_user_object and get_team_object async calls in cli_poll_key were
not mocked in the existing test, causing MagicMock await errors. Patch
both functions at the auth_checks module level. Also apply black formatting
to ui_sso.py which CI rejected.

* fix(auth): skip fallback budget cap when team lookup fails for cli session token

* test(auth): pin cli session budget cap to user/team budget presence

The session_max_budget fallback in cli_poll_key only applied
max_ui_session_budget when neither the user nor the resolved team had a
budget. The existing coverage exercised only the team-lookup-failure
branch. Add two regression tests: a user with a configured budget must
not receive the fallback cap, and a session with no user and no team
budget must fall back to max_ui_session_budget. Mutating either guard
out of the branch now fails these tests.

* fix: remove CLI poll session budget cap

* revert(auth): restore CLI session fallback budget cap

Bugbot autofix (60b81fb8) removed the user/team budget lookup in
cli_poll_key and stopped passing max_budget to the session token,
making CLI sessions fully uncapped whenever neither the user nor the
team has an explicit budget.

That reintroduces the unbounded-spend bypass veria flagged as High
("CLI session budget bypass"): on deployments that rely on
max_ui_session_budget rather than per-user/team budgets, a completed
lite login could run LLM calls with no ceiling for the whole token
lifetime. The fallback only applies when no other budget bounds the
session, so users and teams with a configured budget are unaffected and
keep relying on their shared counters.

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-06-26 09:05:15 -07:00
Sameer Kankute
2b496bc7f7
fix(proxy): restore wildcard expansion in /v1/model/info (#31444) 2026-06-26 08:50:58 -07:00
Yassin Kortam
248389c276
fix(router): surface clean RateLimitError on mid-stream 429 with no fallbacks (#31298)
When a streaming request hits a mid-stream 429 the streaming handler wraps it
in the internal MidStreamFallbackError so the router can attempt fallbacks. With
no fallbacks configured, async_function_with_fallbacks_common_utils falls through
to re-raising that wrapper, which the streaming iterators caught and re-raised
verbatim, so the client received MidStreamFallbackError (an internal type) rather
than a clean RateLimitError (429).

When the fallback path produces a MidStreamFallbackError that carries an
original_exception (i.e. no fallback handled it), the iterators now raise that
underlying provider exception instead of the wrapper, chained with from. Users
with fallbacks are unaffected since their path never reaches this branch. Applied
consistently to the chat async, chat sync, and responses streaming iterators.

Resolves LIT-3503
Fixes #26015
2026-06-25 23:37:29 -07:00
Yassin Kortam
29c254d3d3
fix(vertex): stop O(n^2) re-parse of accumulated Gemini stream JSON (#31297)
handle_accumulated_json_chunk re-ran json.loads on the entire accumulated
buffer after every fragment. For a streaming response fragmented across many
chunks that is O(n^2) total work in a single GIL-holding C call, so a large
enough Gemini response freezes the asyncio event loop for seconds, liveness
probes time out, and the proxy pod gets killed and restarted.

A complete Gemini stream value is a JSON object or array, so the buffer can
only become parseable once its last non-whitespace byte can close one. Gate
the json.loads attempt on that, which makes the common fragmented-response
case parse roughly once instead of once per fragment. An 8MB payload drops
from a 6.9s event-loop freeze to ~0.3s with identical parsed output.

Resolves LIT-3503
Fixes #26181
2026-06-26 09:04:29 +03:00
Sameer Kankute
e5da5a3b6d
fix(proxy): skip model override when response has no model field (#31183)
* fix(proxy): skip OpenAI model override for search responses

Search responses omit a model field by spec but still set model on the
request for routing, which caused noisy errors and dict injection.

* fix(proxy): drop redundant search-specific model override skip

The silent return for responses without a model field already covers
SearchResponse objects; remove the extra search type check.

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

* fix(proxy): skip model override for dict responses without model key

Dict-shaped responses (e.g. search) must not get a spurious model field
injected when they never had one; only override when model is present.

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

* test(proxy): cover swallowed setattr failure in model override

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-06-25 21:02:52 -07:00
Sameer Kankute
062d8ceeed
fix(vertex_ai): prevent stale Vertex bearer token causing /v1/messages 401 after token expiry (#31276)
* fix(vertex_ai): prevent stale Vertex bearer token causing /v1/messages 401 after token expiry

Router shallow-copies litellm_params so extra_headers is a shared reference.
The chat/completions path was calling headers.update() on that shared dict,
persisting the Vertex OAuth bearer. After ~1 h the token expired and /v1/messages
kept reusing it (skipping refresh due to Authorization-already-present guard).

- Build a new headers dict in the Claude partner-models completion path instead
  of mutating the shared extra_headers object.
- Always call _ensure_access_token() in validate_anthropic_messages_environment
  regardless of an existing Authorization header; the token cache makes this cheap.

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

* fix(vertex_ai): copy headers in validate_anthropic_messages_environment to prevent shared-dict mutation

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 21:01:02 -07:00
Sameer Kankute
7eacdd5258
chore: litellm oss staging 250626 (#31305)
* fix(anthropic): support Bearer auth for custom api_base endpoints (Fixes #30926)

* style: format common_utils.py with black

* fix(anthropic): extract api_base from litellm_params in batches/files validate_environment

* fix(anthropic): scope Bearer key check to custom api_base endpoints

* fix(streaming): reset Anthropic message_start cursor (output_tokens=1) when no message_delta arrives

The Anthropic streaming protocol emits `message_start.usage.output_tokens=1`
as a placeholder cursor; the real cumulative output count only arrives in
the final `message_delta` event. When a stream is cancelled before
`message_delta` lands (common for thinking models on long-tail prompts),
ChunkProcessor._calculate_usage_per_chunk's last-wins accumulator left
completion_tokens stuck at 1. Because 1 is truthy, the
`completion_tokens or token_counter(text=...)` fallback in
calculate_usage() never fired, and requests were billed for 1 output
token even when several thousand tokens of text had actually streamed.

Fix: track whether any chunk's completion_tokens exceeded 1
(saw_non_cursor_completion). If the only update we saw was the cursor,
reset completion_tokens to 0 so the text-based fallback estimates from
the real completion content.

Legitimate 1-token completions (model returns "Yes." etc.) are unaffected
in practice — token_counter on a 1-token completion_output also yields
~1, so billing stays approximately correct.

Tests:
- TestAnthropicCursorBug (6 cases) — pins the post-fix behavior
- TestNonAnthropicStreamingIntact (2 cases) — guards against regression on
  providers without the cursor pattern

All 8 new tests pass; 9 existing streaming_chunk_builder_utils tests
still pass.

* fix(streaming): scope cursor reset to anthropic provider + recognize message_delta arrival

Addresses both Greptile P2 threads on PR #30420:

CLASS A — Anthropic-specific heuristic was applied globally
============================================================
The `completion_tokens == 1 and not saw_non_cursor_completion` reset
lived in provider-neutral `streaming_chunk_builder_utils.py`. Any
non-Anthropic provider that legitimately reports completion_tokens=1
in a single usage chunk (perfectly normal for short OpenAI / Bedrock /
Vertex single-token replies with stream_options.include_usage=true)
would have its value silently rewritten to 0 and re-billed via
token_counter — producing a different number than what the provider
actually charged.

Fix: gate the reset on `custom_llm_provider == "anthropic"`, resolved
from the first chunk's `_hidden_params` (the same field set by
streaming_handler.py:722 on the live path). Unknown / missing provider
is treated as non-Anthropic and skips the reset, so newer providers and
custom plugins are also safe by default.

CLASS B — `saw_non_cursor_completion` missed legitimate single-token replies
============================================================
Previous condition was `usage_chunk_dict["completion_tokens"] > 1`,
which never fires for an Anthropic stream where the model legitimately
emits exactly one output token (e.g., "Yes."). Anthropic still sends
message_start (output_tokens=1, the cursor) AND message_delta
(output_tokens=1, the real value) — same value, but two distinct usage
events. The old check couldn't tell that apart from a cancelled stream
where only message_start landed.

Fix: track `completion_usage_updates` and flip `saw_non_cursor_completion`
when EITHER (1) the value exceeds 1 (definitely not a placeholder), OR
(2) we've seen >=2 completion-bearing usage events (positive evidence
that message_delta arrived). Cancelled cursor-only streams still have
exactly one event and still hit the reset; cache chunks with
completion_tokens=0 don't count toward the threshold.

Tests
============================================================
- _make_chunk now sets `_hidden_params["custom_llm_provider"]` (default
  "anthropic") so the gate is exercised by every existing test —
  none of them needed assertion changes besides the legitimate-single-
  token case, which now expects exactly 1 (was a fuzzy 0..3 range).
- New: test_anthropic_cache_only_chunks_after_message_start_still_resets
- New: test_non_anthropic_provider_completion_tokens_one_not_reset
- New: test_unknown_provider_completion_tokens_one_not_reset

11/11 tests pass.

* chore: add Co-authored-by trailer for attribution

Co-authored-by: songkuan-zheng <songkuan-zheng@users.noreply.github.com>

* fix(anthropic): preserve messages cache usage

* style(anthropic): format messages cache usage helper

* fix(anthropic): accept integral float cache token counts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix(anthropic): accept integral float cache token counts

* test(anthropic): cover cache usage edge cases

* fix(gemini): preserve thoughtSignature for server-side tool responses

When Gemini API returns toolCall and toolResponse parts, they might have
different thoughtSignatures. Previously, LiteLLM merged them into a single
dict, overwriting the response's thoughtSignature with the call's.
This fix extracts them separately and re-injects them correctly.

TAG=agy
CONV=755b21d0-3200-40bc-bd1a-bb58a378a9a6

* fix(gemini): address PR comments on thoughtSignature handling

- Fix orphan-response thoughtSignature regression by copying thought_signature to response_thought_signature
- Add missing assertions in existing tests
- Add new unit tests for orphan-response signature handling

TAG=agy
CONV=755b21d0-3200-40bc-bd1a-bb58a378a9a6

* feat(mcp): include server alias and server_id in mcp_info response

- Add alias and server_id fields to mcp_info object in /mcp-rest/tools/list endpoint
- Update rest_endpoints.py to surface alias from server config
- Add test coverage in test_mcp_server.py and test_rest_endpoints.py

Fixes #31015

* fix(proxy): reject non-finite spend via validate_finite_spend

A NaN/-inf spend would bypass spend >= max_budget enforcement. Add a
shared finite-value guard, defined above the litellm.proxy.* imports to
avoid the module-level cyclic-import warning.

* fix(proxy): require admin for any /key/update spend, reject non-finite

Gate the admin check on the presence of `spend` (not a value diff): the
DB spend lags the live cross-pod counter, so an "unchanged" spend on the
non-admin path let a key owner / team member overwrite the live counter
below real usage. Also reject NaN/+-inf spend before the DB write.

* fix(proxy): invalidate spend counter on /user/update spend change

A direct spend change on /user/update wrote the DB row but left the warm
cross-pod counter at the stale value, so enforcement kept reading the old
spend. Invalidate spend:user:{user_id} after the write (reseed-from-DB),
and reject non-finite spend before the write.

* fix(cache): route Bedrock semantic-cache sync embedding through the Router (#28244)

The semantic cache's embedding model is a proxy Router alias whose AWS
credentials (aws_role_name, aws_session_name) live only in the Router
deployment's litellm_params. The sync embedding paths called litellm.embedding()
directly, bypassing the Router, so they could neither resolve the alias nor
assume the configured role; cross-account Bedrock semantic caching failed with
"bedrock:InvokeModel is not authorized". On Redis this surfaced at proxy startup
because redisvl's CustomTextVectorizer eagerly fires a dimension-probe embedding
during cache construction, while llm_router is still None.

Fix A: make the sync paths mirror the already-correct async paths. A shared,
dependency-injected helper (litellm/caching/_embedding_router.py) decides whether
to route through llm_router.embedding(...) when the model is a Router deployment,
else fall back to direct litellm.embedding(...). Redis and qdrant sync
set_cache/get_cache now precompute the embedding and pass vector= to the backend,
exactly as the async astore/acheck already do. Both async _get_async_embedding
methods are unified onto the same helper and now forward the caller's full
metadata instead of a hand-picked subset.

Fix B (Redis only): defer redisvl index construction from __init__ into a lazy,
memoized llmcache property, so the dimension-probe embedding fires on first cache
use, after llm_router is wired. A failed build is not memoized, so a transient
outage recovers on the next request.

Known limitation: resolve_embedding_router gates on an exact model-name match
(same as the shipped async path); wildcard/alias/team-public routes still fall
back to direct embedding. Tracked as a follow-up.

* fix(cache): harden embedding-router and shrink Any surface (review)

Address review feedback on the semantic-cache aws-role fix (#28244):

- resolve_embedding_router now skips deployment entries missing model_name
  instead of raising KeyError on a malformed model_list (Greptile P2);
  add a regression test that fails on the old direct-key access.
- Replace the `**kwargs: Any` passthrough on the four cache _get_embedding /
  _get_async_embedding helpers with an explicit, typed
  `metadata: Optional[Dict[str, Any]] = None` parameter. The helpers only
  ever consumed kwargs["metadata"], so this is behavior-preserving, makes the
  forwarded field obvious at the call site, and removes three bare-Any
  annotations (keeps the strict-rule ANN401 budget within ceiling).
- Note in _build_llmcache that redisvl's dimension-probe embedding adds one
  extra billable embedding on the first cache request (Greptile P2).

* fix(bedrock_mantle): correct responses routing for openai.gpt-5.x models

Dashboard Test Connection for bedrock_mantle/openai.gpt-5.4 and openai.gpt-5.5 was failing with maximum recursion depth errors and "model does not exist"

Route detection in the bedrock provider matched route tokens by plain substring, so the bedrock_mantle/ prefix was mistaken for the mantle/ invoke route and the body model was rewritten to bedrock_openai.gpt-5.5; route tokens now only match at a path-segment boundary so the bare model name is preserved

A responses-mode model whose provider has no responses config bounced forever between the responses API and chat completions; the responses to completion fallback now tags its call so completion() does not bridge back, breaking the loop

The Test Connection endpoint hardcoded the test mode to chat, which disabled mode auto-detection for responses-only models; the default is now None so the mode is detected from model capabilities

acompletion() now drops a duplicate acompletion kwarg before building the partial and treats model_info=None as an empty dict to avoid a NoneType crash

* test(bedrock_mantle): cover route guard and bridge flag; fix reportArgumentType regression

Adds the regression coverage codecov flagged on the two responses to completion
bridge guard lines and the bedrock route-prefix helper. The handler tests drive
both the sync and async fallback paths with litellm.completion and
litellm.acompletion mocked, and assert the forwarded kwargs carry
_skip_responses_api_bridge=True, so dropping either flag line fails the suite.
The common_utils tests assert that bedrock_mantle/openai.gpt-5.x no longer
resolves to the mantle route while the genuine mantle/ and bedrock/mantle/ ids
still do, exercising both branches of _model_has_route_prefix.

Also aligns update_messages_with_model_file_ids model_id to Optional[str],
matching its Responses API sibling, so the defensive model_info fallback no
longer introduces a new reportArgumentType in completion(); the file-id lookup
narrows model_id before the dict get

* chore(ui): sync generated OpenAPI types for optional test_connection mode

The test_model_connection mode body param default changed from chat to None so
the mode is auto-detected from model capabilities, which makes the field
optional in the proxy OpenAPI spec. Regenerate the committed schema so the
dashboard types match: mode becomes optional and the description and default
JSDoc follow the spec, keeping the Check UI API Types Sync gate green

* refactor(bedrock): match all explicit route prefixes at path-segment boundary

Migrates the remaining substring route checks to the existing
_model_has_route_prefix helper so every explicit route token matches only as a
leading path segment, consistent with get_bedrock_route and the mantle route.
Covers _explicit_converse_route, _explicit_claude_platform_route,
_explicit_invoke_route, _explicit_agent_route, _explicit_agentcore_route,
_explicit_converse_like_route, _explicit_async_invoke_route and
_explicit_openai_route. This also stops invoke/ from substring-matching
async_invoke/. Route precedence and order are unchanged, and a note on the
segment invariant is added to the helper docstring

* test(bedrock): cover explicit route prefix segment matching

Exercises all eight migrated _explicit_*_route helpers (converse, converse_like,
invoke, async_invoke, agent, agentcore, claude_platform, openai) directly: each
matches its token as a leading path segment and rejects the token glued to a
preceding segment, so reverting any method to the old substring check fails the
suite. Also asserts invoke/ no longer matches async_invoke/ models, the concrete
improvement of the segment-boundary migration

* test(proxy): assert negative spend is allowed (one-time grant use-case)

Negative spend is intentionally permitted so admins can grant extra
allowance for the current budget period only, without raising the
recurring budget ceiling. Cover it explicitly in validate_finite_spend
and via the /user/update invalidation test.

* fix(google_genai): forward native generateContent top-level fields

Google's native generateContent REST body carries safetySettings, toolConfig,
cachedContent and labels at the top level as siblings of generationConfig. The
proxy's :generateContent endpoint spread them into agenerate_content as loose
kwargs and then dropped them, so callers had to wrap them in extra_body for them
to take effect; safetySettings, for instance, was silently ignored

The provider config now exposes the native top-level field names and
setup_generate_content_call collects whichever are present, merging them into the
outgoing request body through the existing extra_body merge so they reach Google
verbatim. An explicit extra_body still wins on conflict. The sync
generate_content_stream path now also forwards systemInstruction, matching the
other three entry points

Fixes #12671

Claude-Session: https://claude.ai/code/session_016MFtMXokCjT8u6mvyASudK

* fix(proxy): resolve env refs for DB-stored models

* fix(proxy): restrict DB env ref resolution

* fix(proxy): block team DB env ref resolution

* fix(lint): resolve ANN401/UP045/C901 strict-gate violations

- Replace Optional[X] with X | None (UP045) in 8 files
- Replace Any return/param types with concrete types or object (ANN401)
- Extract _make_api_key_auth_header helper to reduce get_anthropic_headers complexity below C901 threshold (17 → 14)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(anthropic): preserve x-api-key for custom endpoints; opt-in Bearer via prefix

Users who pass a key already prefixed with "Bearer " get Authorization: Bearer.
All other keys continue to use x-api-key, preserving backward compatibility with
custom api_base endpoints that expect x-api-key rather than Authorization.

Also consolidates get_auth_header to reuse _make_api_key_auth_header helper,
eliminating the duplicated custom-endpoint routing logic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* revert(anthropic): restore Bearer routing for non-sk-ant- keys on custom api_base

The backwards-compat change broke existing tests that verify the intentional
Bearer-for-custom-base behavior (Fixes #30926). Restore original logic while
keeping the _make_api_key_auth_header helper for code deduplication.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(anthropic): gate Bearer-for-custom-base behind use_bearer_for_custom_base flag

Previously the auth-header switch from x-api-key to Authorization: Bearer
applied unconditionally for non-sk-ant- keys on a custom api_base, silently
breaking existing deployments that proxied to gateways expecting x-api-key.

Introduce use_bearer_for_custom_base: bool = False on _make_api_key_auth_header,
get_anthropic_headers, and get_auth_header. validate_environment reads it from
litellm_params so callers can opt in per-model without any API surface change.

Tests updated to pass use_bearer_for_custom_base=True where Bearer behavior is asserted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(redis): apply namespace prefix in delete_cache and async_delete_cache (#29981)

DEL was the only Redis cache operation that skipped check_and_fix_namespace,
so it targeted the raw SHA256 hash (e.g. 3997c4...) rather than the
namespaced key (litellm:3997c4...). This caused two problems: a Redis NOPERM
error on deployments with an ACL restricting DEL to the litellm:* pattern,
and a silent no-op on all other deployments since the un-prefixed key was
never stored.

* style(anthropic): reformat common_utils.py with Black (--target-version py312)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: preserve cache metadata and spend counters

* style: apply ruff format to streaming_iterator.py

* refactor: reduce complexity of usage/spend helpers to satisfy strict ruff gate

Extract Anthropic message_start cursor reset into
_reset_anthropic_cursor_completion_tokens and the cross-pod spend-counter
invalidation into _invalidate_user_spend_counter_if_changed, keeping both
_calculate_usage_per_chunk and _update_single_user_helper under the
max-complexity ceiling. Use builtin generics in the new signatures so no
new UP006 violations are introduced. Behavior unchanged.

---------

Co-authored-by: rupak-eng <rupakji99@gmail.com>
Co-authored-by: songkuan-zheng <252822057+songkuan-zheng@users.noreply.github.com>
Co-authored-by: songkuan-zheng <songkuan-zheng@users.noreply.github.com>
Co-authored-by: Kannan Priyadharshan <kpd2204@gmail.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Marco Georgaklis <mgeorgaklis@google.com>
Co-authored-by: Anjaiah Methuku <anjaiahspr@gmail.com>
Co-authored-by: Andrii Butko <booandrew23@gmail.com>
Co-authored-by: Kent <kingdooo@gmail.com>
Co-authored-by: kunal2002 <k.nayyar2002@gmail.com>
Co-authored-by: Ali Khan <alirazakhan.offi@gmail.com>
Co-authored-by: jesco-absolut <team@srswti.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Matt Hill <mhill@dataminr.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-06-25 21:00:28 -07:00
yucheng-berri
7680cedf42
test(logging): regression coverage for streaming /v1/messages OpenAI Responses spend logs (#31388)
* test(logging): cover streaming /v1/messages OpenAI Responses spend logs

The #28595 fix added unit tests that call _handle_anthropic_messages_response_logging
directly, but nothing exercises the streaming wiring that actually regressed:
a streaming /v1/messages call cross-routed to the OpenAI Responses backend whose
success handler took the no-op async_log_stream_event path and dropped the SpendLogs
row. Add an end-to-end test that drives litellm.anthropic_messages(stream=True) with a
mocked upstream Responses SSE and asserts async_log_success_event fires with non-zero
cost and call_type anthropic_messages, plus a key-gated live counterpart.

* test(logging): exercise stream deltas and assert single success log

Address review on the streaming bridge regression test: emit output_item.added
plus text deltas before response.completed so it covers mid-stream delta handling
rather than only end-of-stream success logging, assert at least one
content_block_delta surfaces, restore litellm.callbacks via monkeypatch instead of
leaking global state, and assert async_log_success_event fires exactly once.

* test(logging): drop live network test from mock-only suite

Greptile flagged that tests/test_litellm only permits mock tests; network calls
belong in tests/e2e. Remove the key-gated live counterpart and keep the
deterministic mocked test as the regression guard. The live verification stays
in the PR description as the proof of fix.
2026-06-25 20:30:04 -07:00
Mateo Wang
6e3540856c
fix(vertex): preserve Gemini Embedding 2 usageMetadata for cost tracking (#31354)
* fix(vertex): preserve Gemini Embedding 2 usageMetadata for cost tracking

* style(vertex): apply ruff format to batch_embed_content_transformation

* fix(vertex): bill files/ image refs in Gemini embedContent at per-image rate

Resolved files/... references whose mime type is an image were not detected
by _is_image_element, so image_count stayed 0 and generic_cost_per_token fell
back to the text token rate instead of input_cost_per_image. Thread the
resolved_files mapping into the usage builder so resolved image references are
counted and billed per image. Also modernize the _flatten_input return
annotation to satisfy the ruff UP006 strict gate.

* fix(vertex): bill Gemini embedding audio per-second and stop video+audio double-billing

Audio-only embedContent responses set audio_tokens, but generic_cost_per_token only
charges audio via input_cost_per_audio_token. gemini-embedding-2 prices audio via
input_cost_per_audio_per_second, so spend stayed at $0. Plumb a new
audio_length_seconds field through PromptTokensDetailsWrapper, parse it in
_parse_prompt_tokens_details, and bill it from _calculate_input_cost. The vertex
embedding transformation derives audio_length_seconds from audio_tokens using
the documented 32 tokens/sec Gemini rate.

The 1-token text floor that protects video billing only fired when no other
modality was billable, but audio presence flipped that flag, leaving text_tokens
at zero for video+audio responses. generic_cost_per_token then rewrote
text_tokens to prompt_tokens minus audio_tokens (the video token count),
charging video tokens as text on top of the per-second video cost. The rewrite
trigger is text_tokens == 0 and image_count == 0; align the floor with that
trigger and ignore audio_tokens.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-06-25 19:53:16 -07:00
Shivam Rawat
eb15fe667d refactor(passthrough): address multipart review feedback
Build form_data_dict in one pass with groupby instead of rescanning form_items per field name, and assert on the files list directly in the boundary regression test so repeated field names are not collapsed by dict().

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 19:28:59 -07:00
Shivam Rawat
ef0785881a fix(passthrough): forward all multipart files with repeated field names
Passthrough multipart uploads used form.items() and a files dict, so only the last file under a repeated field name reached the upstream. Read multi_items() and send httpx a list of file tuples instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 19:14:13 -07:00
ryan-crabbe-berri
f16af8853b
feat(mcp): opt-in least-privilege default for team key MCP access (#31380)
* feat(mcp): add require_key_mcp_access_defined to stop keys inheriting team MCP servers

By default a virtual key that grants no MCP servers of its own inherits its
team's full MCP server list. The new general_settings flag
require_key_mcp_access_defined (default false) flips this so the team list
acts purely as a ceiling: a key reaches only the servers it grants explicitly
(or via an access group), and inherits none. This mirrors the existing
require_end_user_mcp_access_defined setting.

The default is unchanged, so existing deployments keep today's behavior until
they opt in. The no-mcp-servers sentinel and key access-group grants are
unaffected.

* docs(mcp): note require_key_mcp_access_defined effect in resolver docstring
2026-06-25 18:49:15 -07:00
ishaan-berri
bdafc9a008
feat(ocr): thin Rust OCR Python bridge (#31368)
* feat(ocr): thin Rust OCR Python bridge

* refactor(rust): group provider routing helpers
2026-06-25 18:42:59 -07:00
Mateo Wang
6cc9ea2538
fix(cost-map): retarget mistral-medium-latest to Medium 3.5 and add date-pinned aliases (#31373)
* fix(cost-map): retarget mistral-medium-latest to Medium 3.5 and add date-pinned aliases

Mistral repointed the rolling mistral-medium-latest alias from Medium 3.1
to Medium 3.5, but the static cost map still carried Medium 3.1 specs,
showing wrong pricing/context in the model hub and undercharging spend by
about 3.75x (LIT-3883).

Update mistral/mistral-medium-latest to Medium 3.5 ($1.50/$7.50 per 1M,
256K context, reasoning + vision), add the bare date-pinned aliases
mistral/mistral-medium-2604 (Medium 3.5) and mistral/mistral-medium-2508
(Medium 3.1) that match Mistral's real API model ids, and add
supports_reasoning to mistral/mistral-medium-3-5.

Apply every change to both model_prices_and_context_window.json and the
bundled litellm/model_prices_and_context_window_backup.json so the two
stay in sync, and extend the regression tests to lock the resolved
get_model_info values and the main/backup parity for all touched models.

* test(cost-map): force local cost map in mistral-medium-latest resolution test

get_model_info reads litellm.model_cost, which is fetched from the remote
main branch at import time when LITELLM_LOCAL_MODEL_COST_MAP is unset. Until
this PR lands on main, that remote map still carries the pre-merge Medium 3.1
pricing, so the assertion was only passing when the remote fetch happened to
fail and fell back to the bundled backup. Force the local cost map (the same
fixture pattern the other get_model_info tests use) so the alias resolution is
verified deterministically against the in-repo file.
2026-06-25 18:27:18 -07:00
yucheng-berri
9203488578
feat(spend): store litellm_call_id on spend logs for DB-to-trace correlation (#31344)
* feat(spend): store litellm_call_id on spend logs for DB-to-trace correlation

Successful spend logs keyed request_id to the provider response id while
tracing uses x-litellm-call-id, so a DB row could not be correlated with its
trace; this only worked for failures, where request_id already fell back to
the call id. Add a nullable litellm_call_id column to LiteLLM_SpendLogs,
populate it in get_logging_payload, and surface it in the spend logs read
endpoints so correlation works both directions for successful calls

Fixes LIT-3868

* chore: sync schema.prisma copies from root

* test(spend): cover cache-hit and missing-response-id paths for litellm_call_id

Lock the intended behavior surfaced in review: on a cache hit request_id gets
the uniqueness suffix while litellm_call_id stays the raw call id, and when the
provider returns no id request_id falls back to the call id so both columns
match. Both assertions fail when the populate line is reverted

* test(spend): ignore litellm_call_id in spend logs payload comparisons

get_logging_payload now always writes litellm_call_id, so the full-payload
comparisons in test_spend_management_endpoints.py saw an unexpected key and
failed. litellm_call_id is a per-request runtime uuid like request_id, which
is already ignored, so add it to ignored_keys

* test(logging): ignore litellm_call_id in gcs pubsub spend logs comparison

The gcs pubsub spend logs payload comparison flags any key present in the
actual payload but absent from the golden snapshot. get_logging_payload now
always emits litellm_call_id, a per-request runtime uuid like request_id which
is already ignored, so add it to ignored_keys

* refactor(spend): store litellm_call_id in spend log metadata, drop column

Switch DB-to-trace correlation off a dedicated column and onto the existing
metadata JSON, avoiding a schema migration entirely. litellm_call_id is now
written into spend log metadata (already selected and re-hydrated on the read
paths) instead of a new LiteLLM_SpendLogs column, so the three schema.prisma
copies and the migration are reverted and the read SELECTs go back to their
original form. Correlation is queryable via metadata->>'litellm_call_id'

Trade-off: an unindexed JSON lookup rather than an indexed column; acceptable
for this use case and removes all migration risk

* refactor(spend): thread litellm_call_id into _get_spend_logs_metadata

Set litellm_call_id beside the other computed metadata values inside
_get_spend_logs_metadata rather than mutating clean_metadata back in the
caller, matching how applied_guardrails, cost_breakdown and the rest are
threaded. No behavior change; the value still comes from kwargs with a
litellm_params fallback

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-25 17:45:37 -07:00
yucheng-berri
71ee1a852a
fix(proxy/client): redact api key from key/info client error messages (#31342)
* fix(proxy/client): redact api key from key/info client error messages

The keys management client builds GET /key/info?key=<key> and lets the
requests HTTPError propagate. str(HTTPError) renders the failing request URL
verbatim ("... for url: .../key/info?key=sk-..."), so any caller that logs the
exception leaks the full key; the 401 branch leaked the same way through
UnauthorizedError(str(orig_exception))

Redact both branches with the existing redact_secrets helper so the
secret-bearing query param is scrubbed to ?REDACTED while the status code,
reason, and response object are preserved. Server-side responses already mask
the key, so this closes the remaining client-side surface

* fix: preserve key info unauthorized response

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-06-25 17:35:15 -07:00
Mateo Wang
e0e920d80e
feat(mistral): support Mistral OCR 4 (mistral-ocr-4-0) (#31353)
* feat(mistral): support Mistral OCR 4 (mistral-ocr-4-0)

Add the mistral/mistral-ocr-4-0 model to the cost map and reprice
mistral/mistral-ocr-latest, which now resolves to OCR 4 server-side,
at $4 / 1000 pages. Add the include_blocks param so callers can request
OCR 4's paragraph-level bounding boxes and typed content blocks.

OCR 4's new per-page response fields (blocks, confidence_scores, tables,
hyperlinks, header, footer) already pass through transform_ocr_response
via the extra="allow" config on OCRPage; add a regression test pinning
that behavior alongside cost and param coverage.

* fix(mistral): revert unverified OCR 4 annotation_cost_per_page bump

Mistral's published OCR 4 pricing lists $4/1000 pages for the API and no
separate annotation rate; the $5/1000 figure is the distinct Document AI
(Studio) tier. The earlier 0.003 -> 0.005 bump on annotation_cost_per_page
had no cited source, and ocr_cost() never reads that field (it bills off
ocr_cost_per_page), so the value is documentation-only.

Revert annotation_cost_per_page to the existing 0.003 convention for both
mistral-ocr-latest and mistral-ocr-4-0, keeping only the verified, tested
ocr_cost_per_page: 0.004 change.

* fix(mistral): set OCR 4 annotation_cost_per_page to verified $5/1000 rate

Verified against Mistral's authoritative sources: the pricing page, the
OCR 4 announcement, and the ocr-4-0 model card all list OCR 4 at $4/1000
pages for basic OCR and $5/1000 for annotated pages (Document AI). The
$5/1000 figure is the annotated-pages rate, which is exactly what
annotation_cost_per_page encodes, mirroring the original OCR entry's
0.001 basic / 0.003 annotated split.

Restore annotation_cost_per_page to 0.005 for mistral-ocr-latest and
mistral-ocr-4-0; the earlier revert to 0.003 was based on an incomplete
reading that treated Document AI as a separate product. ocr_cost_per_page
stays 0.004, which is the value billed by ocr_cost().

* fix(mistral-rust): include_blocks in Rust OCR supported params

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-06-25 16:42:37 -07:00
Mateo Wang
b7f28bd89f
feat(aiml): add openai/gpt-image-2 image model (#31323)
* feat(aiml): add openai/gpt-image-2 image model

Adds aiml/openai/gpt-image-2 to the cost map and teaches AimlImageGenerationConfig
to route OpenAI-style image models through the upstream OpenAI request schema
instead of the AI/ML flux schema. Without this, size, n, and response_format would
be remapped to image_size/num_images/output_format, which the gpt-image-2 endpoint
on api.aimlapi.com does not accept.

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>

* chore(aiml): note gpt-image-2 flat-rate pricing basis; apply ruff format

Documents in the cost-map notes that output_cost_per_image is AI/ML's
published medium-quality rate, billed as a flat per-image price like the
other aiml image entries. Reformats the touched files under the repo's
ruff formatter (migrated from black in #31317).

* fix(aiml): drop /v1/images/edits from gpt-image-2 supported_endpoints

LiteLLM only implements an image generation transformer for AIML, so
listing /v1/images/edits overclaimed support. Align with every other
aiml image entry, which lists only /v1/images/generations.

* style(aiml): format transformation.py at line-length 88

The repo formats litellm/ with ruff at line-length 88 (Makefile/CI call
sites), while ruff.toml's global 120 only governs E501/import sorting.
Reformat the transformer to 88 so make format-check / CI lint pass, and
restore the test files to their original layout since tests/ is not part
of the auto-formatted tree.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>
2026-06-25 16:41:43 -07:00
Yassin Kortam
01035499da
fix(cache): apply Redis namespace to all key operations (#31288)
The namespace configured under cache_params was only applied to get/set/
increment paths. Operations that take keys through other code paths (the Lua
scripts registered via async_register_script, delete, scan_iter, rpush, lpop,
get_ttl, and the sync increment_cache) hit raw keys. With a namespace set, the
rate limiter ({key}:tokens/requests/window), pod-lock release, and budget
limiters wrote keys outside the configured prefix, breaking multi-tenant key
isolation and leaving those operations reading keys the namespaced writes never
created.

check_and_fix_namespace is now applied uniformly across every key-taking
RedisCache operation. It is a no-op when no namespace is configured, so
deployments without a namespace are unaffected. The prefix is prepended ahead of
any {hash-tag}, so Redis Cluster slotting is preserved.

Resolves LIT-3374
2026-06-25 15:39:07 -07:00
ishaan-berri
62f93a3343
feat: add Rust OCR providers (#31272)
* feat: port OCR providers to Rust gateway

* chore(deps): update langgraph checkpoint lock

* ci: scope ruff format check to changed files

* ci: fix OCR lint and patch coverage

* fix(ocr): block mapped IPv6 fetch targets

* test(ocr): include rust bridge coverage in OCR shard

* ci: rerun responses shard
2026-06-25 15:12:30 -07:00
Mateo Wang
92d0788da2
chore(lint): widen ANN slack to 10% of baseline and drop PLR0913 from the strict gate (#31335)
* chore(lint): widen ruff budget slack to 10% of baseline for high-volume ANN rules and PLR0913

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>

* chore(lint): drop PLR0913 from strict gate to roll out rules gradually

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>

* fix(lint): ratchet-guard rising baselines even when slack is cut to mask them

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>
2026-06-25 14:43:45 -07:00
ishaan-berri
d8ef1da49d
feat: package Rust OCR bridge in LiteLLM wheel (#31267)
* feat: package rust ocr bridge in litellm wheel

* Install Rust in Windows CircleCI job

* Address Rust wheel review feedback

* Pin Windows rustup installer hash
2026-06-25 12:32:55 -07:00
yucheng-berri
a545c493d7
fix(otel): hashable scope for _emit_once when guardrail_mode is list (#31262)
Some checks are pending
LiteLLM Rust / rustfmt, clippy, test (push) Waiting to run
GitHub Actions Security Analysis / zizmor (push) Waiting to run
* fix(otel): hashable scope for _emit_once when guardrail_mode is list

`_emit_once` keys `spans_logged` by `(class, id, *scope)`. When a
guardrail entry's `guardrail_mode` arrives as a `List[GuardrailEventHooks]`
(the shape Presidio expands to with `output_parse_pii: true`, and the
shape `event_hook` carries for any `mode: [...]` in config), the tuple
contains a list and `spans_logged.get(dedupe_key)` raises
`TypeError: unhashable type: 'list'`. On the post-call path this fires
inside the logging callback and is swallowed; the request returns 200 but
the OTEL `guardrail` span is silently dropped. On the blocking path the
same error surfaces as HTTP 500.

Adds `_freeze_for_dedupe`, a small recursive normalizer that turns lists
and tuples into tuples, sets into frozensets, dicts into frozensets of
`(key, value)` pairs, and falls back to `repr` for arbitrary
unhashables. Applied inside `_emit_once` before the dict lookup, so all
three callsites are protected without touching the guardrail-specific
callsite. Helper assumes acyclic input; `guardrail_mode` values are
built fresh from config (str enums, lists of str enums, TypedDict of
str/list-of-str), so no cycle can arise in practice.

Regression tests in `TestOpenTelemetrySpanDedupe` cover the list crash,
distinct-list-scope collision, dict and set scope parts, and an
end-to-end `_create_guardrail_span` exercise that confirms exactly one
`guardrail` span is emitted across repeated lifecycle entrypoints. Each
new test fails on a reverted helper (4/4 mutation kill)

* fix(otel): cap _freeze_for_dedupe recursion depth and ignore in recursive detector

CI's recursive_detector blocks new recursive functions in litellm/ unless they
are in the allowlist with a documented bound. Cap the helper at 16 levels and
return repr(value) past the cap; this is well past the realistic depth of
guardrail_mode (1-3 levels) and means a future caller passing a cyclic
container can no longer push the proxy logging path into a RecursionError.
Add a regression test that exercises the cycle path.

* refactor(otel): annotate _freeze_for_dedupe return as a HashableScope union

Per review feedback from @mateo-berri: replace the loose `-> object` annotation
with a recursive `HashableScope` union (str | int | float | bool | bytes | None
| Tuple[HashableScope, ...] | FrozenSet[HashableScope]) so the helper's contract
is visible at the signature. Replace the `try/except hash(value); return value`
passthrough with an explicit isinstance check over the hashable-scalar types so
the type checker can narrow without requiring `cast(Hashable, value)` on the
return. Symmetric: dict keys also flow through the freezer (a TypedDict key is
already a string in practice, so behaviorally identical). All 16 regression
tests still pass; mutation kill behavior preserved

* fix: avoid explicit casting

---------

Co-authored-by: Mateo Wang <277851410+mateo-berri@users.noreply.github.com>
2026-06-25 11:59:35 -07:00
Mateo Wang
6db55e0aa5
feat(mcp): add mcp_xff_num_trusted_hops to harden X-Forwarded-For client IP resolution (#31257)
* feat(mcp): add mcp_xff_num_trusted_hops to harden XFF client IP resolution

MCP per-server IP access control reads the client IP from X-Forwarded-For
and trusts the leftmost entry. Behind an append-style proxy or load
balancer (AWS ALB, nginx with $proxy_add_x_forwarded_for, HAProxy, Envoy,
Cloudflare), a client can prepend an arbitrary value to the header, so the
leftmost entry is attacker-controllable even when the direct peer is a
trusted proxy. An attacker can therefore spoof an internal IP and reach
servers marked available_on_public_internet=false.

This adds an optional mcp_xff_num_trusted_hops general setting modelled on
Envoy's xff_num_trusted_hops. When set to N, the client IP is read N entries
from the right of the chain (where N is the number of trusted appending
proxies in front of the gateway) instead of the leftmost value, so any
entries a client prepends are ignored. It composes with mcp_trusted_proxy_ranges,
which still validates the direct peer, and only takes effect once that check
passes; without a validated direct peer the gateway keeps failing closed, so
hop counting cannot be abused by a direct-to-pod attacker. The chain must
contain at least N valid entries or resolution fails closed.

Default is unset, preserving existing behaviour.

* chore(ui): regenerate dashboard schema for mcp_xff_num_trusted_hops

* fix(mcp): warn when mcp_xff_num_trusted_hops is below the minimum

A 0 or negative value is silently treated as disabled, which could leave
an operator believing they enabled append-style X-Forwarded-For hardening
while client IP resolution stays on the spoofable leftmost value. Emit a
warning, consistent with how the module already surfaces invalid CIDR
config, so the misconfiguration is visible in logs.

* fix(mcp): reject mcp_xff_num_trusted_hops < 1 at config-parse time

Add a ge=1 bound to the ConfigGeneralSettings field so the
update_config_general_settings path rejects 0 and negative values with a
clear validation error instead of accepting them, and self-documents the
valid range. The runtime warning stays as defense-in-depth for raw-dict
config that bypasses model validation.

* style(mcp): black-format ip_address_utils.py

* fix(mcp): fail closed when mcp_xff_num_trusted_hops is set but invalid

A present-but-invalid mcp_xff_num_trusted_hops (non-integer, or below 1)
previously made _resolve_num_trusted_hops return None, which the caller
treated identically to "unset" and silently fell back to the legacy
leftmost X-Forwarded-For value. An operator who set the value to harden
client IP resolution but typo'd it would get weaker security than before,
with no fail-closed signal.

Model the setting as a tagged union (_HopCountUnset, _HopCountInvalid,
_HopCount) so the three states are distinct: unset keeps the legacy path,
a valid count drives hop-counting, and an invalid value fails closed
(returns "") instead of reverting to the spoofable leftmost address. The
caller matches on the union exhaustively.

Add a parametrized regression test asserting get_mcp_client_ip returns ""
for 0, -1, "abc", and 1.5 even with a spoofed internal leftmost entry,
and update the resolver unit tests for the new return type.
2026-06-25 07:31:29 -07:00
michelligabriele
0a8a87afe0
fix(streaming): word-sliced cache replay for stream=true cache hits (#30216)
* fix(streaming): word-sliced cache replay for stream=true cache hits

* fix(streaming): align mypy and replay happy-path test with word-sliced cache replay

* fix(streaming): short-circuit whitespace-only content in cache replay splitter

* fix(streaming): emit tool_calls/function_call only on first replay slice

* refactor(streaming): drop dead delattr guard in cache replay

A non-None usage on the replay base object always lives in
__pydantic_extra__ (it is attached via setattr earlier in the same
function), so delattr can never raise here; the try/except AttributeError
that silently swallowed a failure was dead defensive code that could only
ever hide a real regression, so it is removed in both the async and sync
generators.

Also switches the new replay annotations from typing.List to the builtin
list to satisfy the strict ruff UP006 gate and drops the unused
PLR0915 noqa directives (the rule is not enabled in this repo's ruff
config, so RUF100 flagged them).

* fix(streaming): drop carried-over metadata from later cache replay slices

The word-sliced cache replay deep-copies the full ModelResponseStream per
slice, so reasoning_content, thinking_blocks, logprobs, enhancements,
annotations and the rest of the per-message metadata rode on every slice, not
just the first. Downstream handlers that accumulate streamed deltas would
collect each one once per slice, e.g. duplicating a cached reasoning trace N
times on a stream=true cache hit.

Later slices are now rebuilt as a content-only delta with choice-level logprobs
and enhancements stripped, so the whole metadata class stays on the first slice.
Adds async (logprobs) and sync (reasoning_content/thinking_blocks/logprobs/
enhancements, plus annotations) regression tests

---------

Co-authored-by: Mateo <277851410+mateo-berri@users.noreply.github.com>
2026-06-25 07:13:05 -07:00
tin-berri
0f5603895c
fix(mcp): challenge delegate-auth OAuth servers with upstream resource_metadata (#31255)
An oauth2 MCP server with delegate_auth_to_upstream=true never prompted the
user to sign in. On an unauthenticated initialize the gateway answered locally
(200, no tools) and emitted no WWW-Authenticate, so clients like Claude Desktop
either connected empty or hit "OAuth probe timeout after 10000ms".

#30124 added a bare `continue` in _raise_preemptive_401_for_unauthenticated_servers
to stop sending LiteLLM's gateway authorization_uri challenge for delegate-auth
servers, expecting the upstream to emit its own challenge. On initialize the
gateway never probes upstream, so no challenge ever reached the client.

Replace the `continue` with a preemptive 401 carrying the proxied
resource_metadata (RFC 9728) challenge, the same form passthrough servers and
MCPUpstreamAuthError already use. This keeps #29770 fixed (still no
authorization_uri) while restoring the upstream PKCE sign-in prompt.
2026-06-24 20:50:21 -07:00
tin-berri
f426912ba1
fix(mcp): resolve toolset tools by the server's known prefix (#31254)
* fix(mcp): resolve toolset tools by the server's known prefix

Toolsets store {server_id, bare tool_name} and reconcile that against the
live prefixed tool name at list time. The reconciliation chopped the live
name at the first MCP_TOOL_PREFIX_SEPARATOR with no server context, so a
server whose prefix contains the separator (a hyphenated alias, or the
UUID server_id used as the prefix when a server has no alias) had its
tools silently dropped from /toolset/<name>/mcp while listing fine
everywhere else. Strip the exact known prefix for the tool's server_id
instead of guessing the boundary, on both the resolve and filter sides

Also render toolset tools as {server-prefix}-{tool} in the dashboard
picker result and chips; this is display only, the persisted record
stays {server_id, bare tool_name}

Resolves LIT-3419

* test(mcp): add focused unit tests for strip_known_server_prefix

Cover the LIT-3419 cases directly on the helper with real MCPServer
objects: clean prefix round-trip, hyphenated alias, UUID server_id
fallback, unprefixed passthrough, and the server=None legacy fallback
2026-06-24 20:50:16 -07:00
Mateo Wang
9c41077786
fix(mcp): warn loudly when X-Forwarded-For is present but use_x_forwarded_for is off (#31266)
* fix(mcp): warn loudly when X-Forwarded-For is present but use_x_forwarded_for is off

When a request carries an X-Forwarded-For header but use_x_forwarded_for is
unset, get_mcp_client_ip silently falls back to the direct peer's IP (the load
balancer / reverse proxy). That peer almost always sits inside
mcp_internal_ip_ranges, so the 'Internal network only'
(available_on_public_internet: false) restriction trusts every external caller
as internal and effectively exposes those servers.

Emit a one-shot loud error pointing the operator at use_x_forwarded_for instead
of hard-failing: on a deployment with no load balancer, a crafted
X-Forwarded-For header must not be able to take the service down, and a one-shot
log keeps a flood of crafted headers from spamming the logs.

* fix(mcp): re-arm XFF-disabled warning on config change and harden test assertion

Address PR review: tie the one-shot warning flag to the observed
use_x_forwarded_for value so it re-arms whenever the setting is seen enabled,
restoring the diagnostic on a later rollback to disabled. Also assert against
str(call_args) so the test survives a positional-to-keyword logger refactor.
2026-06-24 20:49:32 -07:00
Mateo Wang
257d67167f
fix(mcp): correct misleading no-trusted-proxy warning for XFF access control (#31264)
* fix(mcp): correct misleading no-trusted-proxy warning for XFF access control

* test(mcp): assert the no-trusted-ranges warning was logged instead of relying on StopIteration
2026-06-24 20:49:29 -07:00
mubashir1osmani
0e1d0f4742
fix(proxy): stop double-decrypting email/slack alerting env vars in get_config (#31117)
* fix(proxy): stop double-decrypting email/slack alerting env vars in get_config

proxy_config.get_config() already returns environment_variables decrypted
(the DB overlay decrypts them in _update_config_fields, and YAML values are
plaintext), so the /get/config/callbacks slack and email blocks were running
decrypt_value_helper() a second time on plaintext. That second decrypt always
failed and the helper swallowed the error and returned None, so every SMTP_*
field came back blank when the Admin UI reloaded the email settings, and the
proxy logged a misleading "Did your master_key/salt key change recently?"
error even when nothing changed.

Consume the already-decrypted values directly, matching process_callback's
handling of the same dict for langfuse/datadog/etc. Sensitive-value masking
is preserved.

Fixes #19221

* fix(proxy): preserve a cleared slack webhook instead of falling back to OS env

Use an explicit is-not-None guard rather than truthiness when deciding whether
to fall back to os.getenv for SLACK_WEBHOOK_URL. With `or`, a webhook the admin
cleared (stored as "") is falsy and would surface a stale SLACK_WEBHOOK_URL from
the OS environment; only a truly absent key should trigger the OS lookup. No
decryption is reintroduced.
2026-06-24 19:19:08 -07:00
Ishaan Jaff
b4b032116f
fix: align rust OCR request preparation 2026-06-24 17:10:17 -07:00
Ishaan Jaff
65ce6a1522
fix: reduce OCR basedpyright argument errors 2026-06-24 16:59:30 -07:00
Ishaan Jaff
7180f79887
fix: satisfy OCR lint budget 2026-06-24 16:49:35 -07:00
Ishaan Jaff
82ec9aeb70
fix: address OCR bridge review comments 2026-06-24 16:04:31 -07:00
Ishaan Jaff
725deeed19
feat: make rust OCR async-first 2026-06-24 15:36:21 -07:00
ishaan-berri
4efce809d0
feat(proxy): add POST /v1/callbacks/logs to replay logging payloads through callbacks (#31134)
* feat(proxy): add logging_endpoints package init

* feat(proxy): add POST /v1/callbacks/logs to replay logging payloads through the success/failure callback fan-out

* feat(proxy): register callback_logs_router

* test(proxy): add logging_endpoints test package init

* test(proxy): cover /v1/callbacks/logs replay, admin guard, and partial-failure handling

* refactor(proxy): move callback-logs request/response models to litellm/types/proxy

* refactor(proxy): wrap callback-logs replay in CallbackLogsReplayer class with payload logging

* test(proxy): update callback-logs tests for class-based replayer and separated types

* fix(proxy): cover /v1/callbacks/ in backend component allowlist

The new /v1/callbacks/logs route was dropped by both component
allowlists, failing test_gateway_plus_backend_covers_full_app. It's an
admin-only spend-logging route, so it belongs on the backend (control
plane) alongside the existing /callbacks family.

* refactor(proxy): use builtin dict/list generics in callback-logs endpoint

Switch Dict/List from typing to builtin dict/list to satisfy the ruff
strict-rule budget (UP006).

* refactor(proxy): use builtin dict/list generics in callback-logs types

UP006: builtin generics over typing.Dict/List.

* chore(ui): regenerate schema.d.ts for /v1/callbacks/logs

Run npm run gen:api to add the CallbackLogRecord/CallbackLogsRequest/
CallbackLogsResponse types and the /v1/callbacks/logs path, keeping the
dashboard types in sync with the proxy OpenAPI spec.

* fix(proxy): force stream=False when replaying callback logs

A replayed StandardLoggingPayload is a terminal, fully-aggregated event —
the producer (e.g. the rust realtime gateway) already collected the whole
session before POSTing. Marking the rebuilt Logging object as streaming made
async_success_handler wait for a complete_streaming_response that never
arrives, so the spend log was never written. Realtime sessions now land in
LiteLLM_SpendLogs.

* feat(litellm-rust): CustomLogger callback layer posting to /v1/callbacks/logs

integrations/ mirrors litellm/integrations/: a sync, typed CustomLogger trait
(base contract), a typed StandardLoggingPayload, and LiteLLMPythonProxyAPILogger
— the first concrete logger, owning a bounded channel + background worker that
batches and POSTs to the Python proxy's /v1/callbacks/logs.

* feat(litellm-rust): RealTimeStreaming per-session log collector

1:1 with Python's RealTimeStreaming: observe() accumulates O(1) usage/model/id
per event (never buffers frames); log_messages() builds one StandardLoggingPayload
on session close and fans out to the CustomLogger callbacks. request_id == the
OpenAI realtime session id (sess_…), with the gateway id as fallback.

* feat(litellm-rust): wire realtime logging into the splice (lock-free observe)

The collector is owned on the splice task and observed via a synchronous &mut
callback threaded through providers::realtime::realtime() — no Arc/Mutex/atomic
on the per-frame hot path. On session close the bridge flushes one payload.
AppState carries the registered loggers; main spawns the proxy logger.

* docs(litellm-rust): ai-gateway realtime logging architecture

* docs(litellm-rust): document request-log egress to the LiteLLM control plane

Add a 'Request logging' guide to the ai-gateway README: how to point the gateway
at a LiteLLM proxy via LITELLM_PROXY_BASE_URL (+ LITELLM_MASTER_KEY for the
admin-only /v1/callbacks/logs POST), and the non-blocking / one-payload-per-session
behavior.

* feat(litellm-rust): make log-egress tunables env-overridable

Channel capacity, batch size, and flush interval now read from
LITELLM_LOG_CHANNEL_CAPACITY / LITELLM_LOG_BATCH_SIZE / LITELLM_LOG_FLUSH_INTERVAL_MS,
falling back to the DEFAULT_* consts on missing/invalid/non-positive values.
Grouped behind an EgressTunables::from_env() read once at logger construction.

* docs(litellm-rust): document log-egress tuning env vars

* docs(litellm-rust): require constants in a crate-level constants.rs

Mirror of Python's litellm/constants.py rule — magic numbers and fixed strings
go in src/constants.rs, not inline in feature modules; env-overridable tunables
keep their DEFAULT_* value there.

* refactor(litellm-rust): move ai-gateway constants into constants.rs

Per the new rule: the log-egress defaults (proxy base, ingest path, channel
capacity, batch size, flush interval) and the realtime provider default move to
crates/ai-gateway/src/constants.rs; modules import from it.

* ci: run logging_endpoints tests in the proxy-infra coverage shard

tests/test_litellm/proxy/logging_endpoints wasn't in any coverage-uploading
job, so callback_logs_endpoints.py showed only import-level coverage (~35%) on
codecov/patch despite being ~98% covered locally. Add it to proxy-infra's
test-path so the test is exercised under --cov.

* fix(litellm-rust): hash the master key before logging — never send the raw credential

Greptile/Veria P1: user_api_key_hash was the plaintext LITELLM_MASTER_KEY, which
fans out to spend logs and every callback (Langfuse/Datadog) and could be
recovered from logs. SHA-256 it (auth::hash_token, matching the proxy's
hash_token); the field is named *_hash and the proxy stores it verbatim when it
isn't sk-prefixed, so the DB value is identical with zero plaintext exposure.

* fix(litellm-rust): observe realtime logging on upstream events only

Greptile P1: observe ran on the client->upstream arm too, so an authenticated
client could send a fabricated response.done and inflate its own spend log.
session.created/response.done are server->client events; observe the upstream
arm only.

* feat(proxy): bound callback-logs batch + return per-record failures

Greptile P2: cap /v1/callbacks/logs at MAX_CALLBACK_LOG_RECORDS (default 1000,
env-overridable) so one POST can't trigger an unbounded callback/DB fan-out; and
return per-record {index, error} failures so a caller (the rust gateway) can
distinguish a transient callback error from a structurally bad payload.

* chore(ui): regenerate schema.d.ts for CallbackLogFailure / failures field

* fix(constants): make MAX_CALLBACK_LOG_RECORDS a plain constant

It doesn't need to be env-configurable (only the rust egress tunables are). As an
os.getenv var it tripped tests/documentation_tests/test_env_keys.py, which requires
every env key to be documented in the (separate-repo) config_settings.md. Plain
constant → not scanned → code-quality + documentation checks pass.

* docs(litellm-rust): trim ai-gateway ARCHITECTURE.md to one diagram + notes

* docs(litellm-rust): tighten the README request-logging section

* docs(litellm-rust): ARCHITECTURE.md is just the diagram (gateway = inference, spend = callback)

* docs(litellm-rust): drop em-dashes from the request-logging section

---------

Co-authored-by: Ishaan Jaffer <ishaanjaffer0324@gmail.com>
2026-06-24 15:25:10 -07:00
tin-berri
bbef1b84ab
feat(mcp): graft v2 resolver onto _create_mcp_client (none + api_key static family) (#31058)
* feat(mcp): add v1 bridge + none/api_key resolver arms (unwired)

PR4a of the MCP v2 outbound-credential migration, stacked on the resolver skeleton.
Builds the bridge for the first live modes without wiring it onto the request path:

- resolver.py: the none arm (NoOpAuth) and the api_key shared-key arm (StaticHeaderAuth
  from the config); the BYOK source and the other five arms stay not_implemented.
- adapter.py: the v1 <-> v2 edge (to_subject, to_server_spec, raise_public, should_defer).
  to_server_spec maps only none + the static-header family and returns None to defer every
  other mode to v1. Imports v1, kept out of the package __init__ so the resolver core stays
  v1-free.
- MCPClient gains an optional resolved_auth that feeds the factory's auth= slot, taking
  precedence over the SigV4 aws_auth; default None keeps current behavior.

Nothing calls these from _create_mcp_client yet, so production behavior is unchanged; the
graft lands in PR4b. Unit tests cover the two arms, the full mapping table, and the auth
plumbing.

* feat(mcp): graft v2 resolver onto _create_mcp_client for migrated modes

Wire the none + api_key static-family resolver arms from PR4a onto v1's
live request path. In _create_mcp_client's HTTP/SSE branch, to_server_spec
decides per mode: a migrated mode resolves through the injected
UpstreamCredentialProvider and feeds the resulting httpx.Auth into the new
resolved_auth slot; every other mode returns None and falls through to the
unchanged v1 construction. resolve_mcp_auth now runs only when the mode
defers, so a migrated server skips the v1 token-exchange / M2M I/O.

stdio is untouched: auth_type/auth_value never reach the upstream on the
stdio path (_get_auth_headers is HTTP/SSE only), so there is nothing to
graft there. No v1 code is deleted yet; resolve_mcp_auth's static return
still backs stdio and the not-yet-migrated modes until later PRs retire it.

* test(mcp): cover the v2-resolver graft in _create_mcp_client

Regression tests for the PR4 graft. Migrated HTTP modes resolve through the
provider into resolved_auth: none -> NoOpAuth, and the static api_key family
emits the right header per scheme (X-API-Key, Bearer, token, raw authorization,
base64 basic). Deferred modes (oauth2) and a missing static token fall back to
v1's auth_value. A stdio server with a migrated auth_type still defers to v1,
since httpx.Auth never reaches the subprocess. A resolver Error is mapped to the
public HTTP contract (401) via an injected provider, exercising the DI seam.

* fix(mcp): defer to v1 when an inbound credential would be overridden

The graft attaches the resolved static credential as an httpx.Auth, whose auth
flow writes its header after extra_headers. That silently overrode an inbound
Authorization: a per-request mcp_auth_header override, or a header supplied via a
guardrail hook / static_headers / forwarded caller header. v1 lets those win, so
the graft had inverted the credential precedence for the migrated static modes.

Mirror the v2 egress credential-isolation invariant: defer the request to v1 when
mcp_auth_header is set, or when the header the resolved credential would write is
already present in extra_headers. none writes no header, so it never defers.

* test(mcp): cover the credential-isolation defer guard

Regression tests for the precedence fix. A per-request mcp_auth_header override and an
Authorization already present in extra_headers (guardrail hook like the JWT signer,
static_headers, or a forwarded caller header) both defer a migrated static server to v1
so the inbound credential wins; none stays on v2 and does not clobber an inbound
Authorization since NoOpAuth writes nothing. The deferred cases assert resolved_auth is
None, which fails if the guard is removed.

* refactor(mcp): resolve inbound-header conflict on v2 instead of deferring

For an Authorization already supplied via extra_headers (a guardrail hook such as the
JWT signer, static_headers, or a forwarded caller header), keep the request on the v2
path and skip resolved_auth rather than deferring to v1. The inbound header still wins
since nothing overwrites it, but hooks no longer pin a v1 fallback, which is what lets
resolve_mcp_auth be retired once the remaining modes migrate.

The mcp_auth_header per-request override still defers to v1, since that value becomes
the upstream credential rather than sitting in extra_headers; that defer falls away
once the per-user modes stop writing mcp_auth_header.

* fix(mcp): clear UP037 lint gate and fix allowed-servers test under the graft

adapter.py uses `from __future__ import annotations`, so the quoted "UserAPIKeyAuth" /
"MCPServer" annotations in to_subject/to_server_spec/_shared_key_spec were unnecessary
and pushed UP037 over the strict-rule budget; drop the quotes.

test_list_tools_only_returns_allowed_servers passed a MagicMock as user_api_key_auth.
The graft now builds a Subject from the principal, and the MagicMock's non-string
org_id/user_id fail Subject validation, so the listing came back empty. Use a real
UserAPIKeyAuth instead (MagicMock for an injected dependency was the anti-pattern here).

* test(mcp): assert config token via resolved_auth, not the headers dict

test_mcp_server_config_auth_value_header_used inspected _get_auth_headers(), but the
graft now carries the static credential on the client's httpx.Auth (resolved_auth) and
writes the header at send time, so that dict is empty. Assert the header the
StaticHeaderAuth emits onto the request instead. Both config keys (authentication_token,
auth_value) stay covered.

* chore(typecheck): set reportMatchNotExhaustive slack to 0

The previous slack of 3 put the ceiling at baseline + slack = 4, so a newly
non-exhaustive match (for instance dropping an Error arm off a Result match)
could land without tripping the gate. Setting slack to 0 pins the ceiling at
the current baseline of 1, so any added non-exhaustive match now fails CI while
the one pre-existing violation in router.py stays within budget
2026-06-24 14:53:33 -07:00
tin-berri
6003187165
fix(mcp): let proxy admins assign MCP servers to teamless keys (#31126)
Creating or updating a key with a specific (non-allow_all_keys) MCP
server or access group failed with a 403 when the key had no team:

    Key is not in a team. Only globally available (allow_all_keys) MCP
    servers can be assigned

validate_key_mcp_servers_against_team computed the allowed set as
team servers + allow_all_keys servers. For a teamless key the team
set is empty, so the allowed set collapsed to just allow_all_keys
servers and any explicitly-picked server or access group was rejected.

This was asymmetric with runtime: get_allowed_mcp_servers honors a
teamless key's own object_permission.mcp_servers verbatim, with no
team gate and no allow_all_keys filter. So the create/update path
refused to persist a grant the run path would have served.

Thread is_proxy_admin into the validator from both call sites
(/key/generate and /key/update). When a key has no team and the
caller is a proxy admin, the requested servers and access groups are
folded into the allowed set so the existing subset checks pass. A
proxy admin can already reach every MCP server, so there is nothing
to escalate. Non-admins and every team-scoped key are unchanged.

Resolves LIT-3815
2026-06-24 13:20:11 -07:00
mubashir1osmani
56825926af
fix(vertex/files): stream OpenAI->Vertex batch JSONL uploads (#31036)
* fix(vertex/files): stream OpenAI->Vertex batch JSONL uploads to fix OOM on large files

Large (1GB+) batch JSONL uploads to Vertex AI / GCS caused OOM or killed the worker
because the request body was buffered and multiplied 2-3x in size. The create-file
path is now streaming end-to-end: transform_create_file_request returns a
ResumableChunkedUploadConfig carrying a lazy _OpenAIToVertexBatchUploadStream, and the
HTTP handler opens a GCS resumable session and PUTs the body in bounded 8 MiB chunks
(Content-Range, 308 between chunks) so the transformed payload is never held in full.
The proxy /v1/files endpoint streams from Starlette's spooled upload handle instead of
reading the whole body, and batch rate limiting counts tokens and models in a single
streaming pass.

Only gcs_bucket_name is supported for the GCS target; the legacy bucket_name key is
intentionally not read.

Also removes the unreachable VertexAIFilesHandler create path and everything only it
kept alive (VertexAIJsonlFilesTransformation, _stream_openai_jsonl_to_vertex, the legacy
transform helpers), plus the orphaned batch_utils helpers the streaming rewrite replaced.

* fix(batches): return original JSONL on unparseable row to avoid silent batch truncation

The streaming rewrite of replace_model_in_jsonl accumulated physical lines and
skipped a row on JSONDecodeError to support multi-line objects, but a genuinely
malformed or truncated row never completes: it poisons the buffer, swallows every
following row, and the function still returned the partial rewrite (the rows before
the bad one, already model-rewritten) as if the batch were complete. That turned the
pre-rewrite behavior of returning the original file unchanged (so the provider rejects
the bad batch loudly) into a silent partial submission.

Restore the original-content fallback: when an unparseable remainder is left after the
loop, return the original file_content (rewinding a consumed seekable source) instead of
the truncated output. The multi-line happy path is unchanged.

* test(batches): mock resumable GCS upload in vertex batch prediction test

The vertex batch file-create path now streams to a GCS resumable session via
_aresumable_chunked_upload (httpx send) instead of AsyncHTTPHandler.post, so the
existing test's post mock no longer intercepted the upload and a real request hit
GCS (401). Mock _aresumable_chunked_upload to return the GCS object response; the
resumable protocol itself is covered in test_vertex_ai_files_streaming.py.

* fix(batches): resilient per-row token accounting; no hard-block on count failure

The batch input-file pass iterated a generator whose json.loads raised on a
malformed line; the outer except caught it and stopped the loop, so any body.model
on rows after a bad line was never collected and the model allowlist check ran
against a partial set. It also hard-blocked the batch with a 400 whenever token
counting raised, a backwards-incompatible change from the prior swallow-and-proceed
behavior that breaks legitimate rows the token counter cannot measure (e.g. some
multimodal content).

Iterate the JSONL line-by-line and account each row independently. A malformed line
is skipped (its request cannot run upstream anyway) and a row the counter cannot
measure falls back to a conservative size-based estimate. The loop never aborts, so
the allowlist check always sees every parseable model, and the token total is never
zeroed, so a crafted uncountable row still cannot evade the TPM limit, without
hard-rejecting a legitimate batch.

* perf(vertex/files): unblock async upload; drop empty finalize; widen batch MIME types

Three review follow-ups on the resumable batch upload:
- _aresumable_chunked_upload pulled chunks from a synchronous generator that runs
  the per-row transform inline on the event loop thread, blocking other requests
  between PUTs on large uploads. Each chunk is now produced via asyncio.to_thread.
- _iter_resumable_chunks no longer yields a trailing empty chunk, so an exactly
  chunk-aligned upload finalizes on its last data chunk instead of an extra
  zero-byte PUT; a 0-byte stream still finalizes via the caller's empty request.
- valid_content_type now accepts the MIME types clients label .jsonl batch uploads
  with (text/plain, application/json, ndjson, ...), so such a batch file no longer
  silently bypasses the streaming path into the buffered media upload.

* fix(vertex/files): keep legacy bucket_name as GCS bucket fallback

The rename to gcs_bucket_name dropped the legacy bucket_name key entirely, so an SDK caller passing bucket_name to a Vertex AI file create/retrieve/content call with GCS_BUCKET_NAME unset got ValueError("GCS bucket_name is required") where it previously resolved the bucket. _get_configured_bucket_name now reads gcs_bucket_name, then bucket_name, then the env var, and bucket_name is restored to OPTIONAL_KWARGS_KEYS so it survives get_litellm_params on the retrieve and content paths. gcs_bucket_name keeps precedence when both are present

* style: sort imports in llm_http_handler to satisfy I001 budget

---------

Co-authored-by: Yuneng Jiang <yuneng@berri.ai>
2026-06-24 13:19:57 -07:00
Shivam Rawat
f883d6b134
Merge branch 'litellm_internal_staging' into litellm_realtime_cost_metrics
Some checks failed
LiteLLM Rust / rustfmt, clippy, test (push) Has been cancelled
2026-06-24 13:10:13 -07:00
Sameer Kankute
8bca05d311
fix(anthropic): sanitize tool_use ids on native /v1/messages path (#31094) 2026-06-24 07:57:46 -07:00
mubashir1osmani
e0c8a6b483
fix(proxy): expand all-proxy-models sentinel in direct access lookup (#31153)
A user provisioned with "All Proxy Models" stores the literal
"all-proxy-models" sentinel in user.models. get_direct_access_models looked
that string up as a real model_name via get_model_list, which matched no
deployment, so /v2/model/info marked every model direct_access=false and the
Models + Endpoints page rendered empty for such users when they have no teams.
The model dropdown / Playground worked because get_key_models already expands
the sentinel to the full proxy model list, hence the inconsistency in the
report.

Expand the sentinel to all non-team deployment ids via
get_model_ids(exclude_team_models=True), the same call the PROXY_ADMIN branch
in the caller already uses. This fixes both /v1/model/info and /v2/model/info
since they share _populate_team_access_on_models. Empty user.models stays "no
direct access" to match get_key_models semantics.

Fixes #22791
2026-06-23 22:23:14 -07:00