Commit graph

11767 commits

Author SHA1 Message Date
Yassin Kortam
82d3116be9
fix(ui): reflect REDIS_* env cache config and stop the UI overwriting the stored password (#34160)
The Cache Settings page read only the database row, so a response cache
pointed at Redis purely through REDIS_* env vars showed a blank page
while the cache worked. It also masked credentials on read with a
partial-reveal string and re-persisted whatever the form submitted, so
an admin who edited an unrelated field and pressed Save wrote the mask
string over the real Redis password, breaking auth.

GET /cache/settings now overlays the same REDIS_* kwargs the runtime
resolves from when the stored config leaves a field unset, and redacts
credentials with a fixed marker. POST /cache/settings restores the
stored secret behind any credential echoed back as the marker or omitted,
and drops an env-sourced marker rather than persisting it; the response
no longer echoes plaintext credentials. The connection test resolves a
redacted credential back to the stored value the same way. The dashboard
never prefills a credential and drops the marker from the save payload,
mirroring the Coordination Redis tab.

Resolves LIT-4315
2026-07-21 18:36:26 -07:00
Mateo Wang
cc33a310ae
Merge pull request #34052 from BerriAI/litellm_a2a_e2e_tests
test(e2e): add live A2A agent e2e suite
2026-07-21 18:23:48 -07:00
yucheng-berri
065faf6e69
chore(proxy): clean up request parameter validation and provider destination handling (#34189) 2026-07-22 00:57:58 +00:00
yuneng-jiang
5081e0cf79
test(logging): pin compression_savings in the gcs pubsub spend log fixture (#34204)
The spend-log metadata schema gained a compression_savings key, so the
gcs pubsub v1 payload now carries it. The golden fixture was never
updated, and the comparator flags any key present in the payload but
absent from the fixture, so test_async_gcs_pub_sub_v1 failed on every
run. Pin the key as null rather than adding it to ignored_keys; the
value is deterministic on this path, so ignoring it would leave the
assertion blind to the field entirely.
2026-07-22 00:56:20 +00:00
ryan-crabbe-berri
e967bc8c4f
test(e2e): cover 12 non-core LLM coverage registry cells (#34123)
* fix(e2e): reference client.proxy in mid-conversation native providers test

EndpointsClient exposes the shared ProxyClient as .proxy and has never had a
.gateway attribute, so these two calls raised AttributeError at runtime and
failed the tests/e2e basedpyright zero-error gate for any PR touching e2e
files. Introduced in 23b5b7d199.

* test(e2e): cover 12 non-core LLM coverage registry cells

Raises Non-Core LLMs registry coverage from 24/50 to 36/50 (overall 51.9%
to 54.8%). Four cells were already asserted by existing tests and only
gain their covers marker (openai embeddings, openai image generation,
openai TTS, cohere rerank); one is dual-marked onto the existing
spend-tracking embeddings test rather than duplicated.

New tests: bedrock and vertex embeddings, streaming TTS (asserts chunked
transfer encoding so a buffered body cannot pass), audio transcriptions
via the realtime suite's wav fixture, moderations flag/pass pair, and
files list/retrieve in the batches suite.

Harness: e2e_http.upload generalized to any form model with a
file_content_type override (batches path unchanged), new stream_binary
primitive + BinaryStream for binary chunked responses, transcribe and
moderations client methods, file retrieve/list client methods.

* fix(e2e): close streamed TTS response on error paths and surface the error body

With stream=True a non-2xx response returned with the body unread, keeping
the socket checked out until garbage collection; the sibling
_streaming_outcome already consumes resp.text on error. The response now
closes on every path and BinaryStream carries a bounded error_body so a
failed stream call is triageable.

* test(e2e): assert streamed TTS response carries no content-length
2026-07-22 00:43:41 +00:00
mubashir1osmani
e7b9357bc2
fix(e2e): drop httpbin.org from passthrough headers test, use real Anthropic (#34159)
httpbin.org is an external dependency prone to transient 503s (caused the
stage failure); its echo-body assertion also doesn't exercise a real LLM
provider. Point the custom pass-through endpoint at the real Anthropic
Messages API instead. Anthropic doesn't echo headers back, but it gates
real behavior on two of them, which is enough to prove forwarding: a
static x-api-key configured on the endpoint (never supplied by the caller)
must reach upstream or the call 401s, and an invalid x-pass-anthropic-version
sent by the caller must reach upstream with the prefix stripped, which
Anthropic echoes verbatim in its 400 body. Verified live against a local
proxy and the real Anthropic API: valid version returns a real completion,
invalid version returns the exact marker in the 400 body.
2026-07-21 17:42:20 -07:00
Yassin Kortam
1ea7db2111
fix(rust): route agentic-completion-hook /messages requests to Python for all stream modes (#34126) 2026-07-22 00:36:47 +00:00
mateo-berri
1255094de3 fix(e2e): retry transient turn failures and move the weekly anomaly run to Saturday before the stable release cut 2026-07-21 16:55:44 -07:00
shivam
93f27641ae fix(batches): stabilize managed batch pagination with unified_object_id tie-breaker
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-21 23:41:16 +00:00
mateo-berri
0bfdb37266 test(e2e): route external agent card fetch through the typed transport
Adds get_external to e2e_http.py for absolute third-party GETs (no proxy base url or auth, same Result classification) and rewires fetch_agent_card through it, dropping the urllib.request escape hatch. Creates tests/code_coverage_tests/check_e2e_no_raw_requests.py, the checker tests/e2e/CLAUDE.md already referenced, and wires it into the code-quality workflow so raw HTTP client imports outside the transport fail CI; pre-existing uses (root conftest liveness probe, claude_code version resolver) are grandfathered and exception-type-only imports stay allowed.
2026-07-21 16:35:02 -07:00
shivam
d8c7e25296 fix(batches): paginate managed batch list by unified_object_id cursor
GET /batches served from the managed-objects table paged with a
where id > after filter, but the after cursor clients send back is a
batch's unified_object_id (the value returned as .id and last_id), and
id is the table's random-uuid primary key. Comparing the two unrelated
fields, while ordering by created_at desc but filtering with gt, made
pages repeat the same last_id (pagination loops) and silently drop
batches. Switch to Prisma cursor pagination on the unique
unified_object_id column so listing walks every batch exactly once in
reverse-chronological order, matching OpenAI

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-21 23:25:26 +00:00
Yassin Kortam
bc374fcd9f
test(e2e): add Azure AI Foundry and Anthropic /v1/messages coverage for the Rust bridge (#34021)
* test(e2e): add Azure AI Foundry and Anthropic /v1/messages coverage

* test(e2e): add Azure AI Foundry + Anthropic messages coverage for the Rust bridge

* test(e2e): guard against an empty SSE stream in the Azure Foundry tool-use streaming case
2026-07-21 16:22:47 -07:00
mateo-berri
48295df0e9 test(e2e): raise warm cache read floor to 0.65 from measured healthy and regression baselines 2026-07-21 16:21:45 -07:00
Yassin Kortam
8a56899e1e
test(e2e): cover config and misc management routes for Management/UI coverage (#34120) 2026-07-21 16:20:50 -07:00
ryan-crabbe-berri
e17f3b6e1a
fix(proxy): populate user_email on UserAPIKeyAuth for JWT auth (#34174)
Some checks are pending
CodSpeed Benchmarks / benchmarks (push) Waiting to run
GitHub Actions Security Analysis / zizmor (push) Waiting to run
JWT auth built UserAPIKeyAuth without user_email even though the resolved
user row and the JWT email claim were both available, so the user_email
label on Prometheus metrics and user_api_key_user_email in
StandardLogging/SpendLogs metadata were always None for JWT traffic.

Plumb user_email through JWTAuthBuilderResult: auth_builder returns the
user row email when set, falling back to the user_email_jwt_field claim
(covers the scope-based proxy-admin path where no user row is loaded).
The JWT branch now stamps it on the proxy-admin return, the standard
valid_token, and the auto-registered virtual key object.

Resolves LIT-4238
2026-07-21 16:11:47 -07:00
mateo-berri
1692170264 fix(e2e): count aborted-session turns as failures and require a spend stability window 2026-07-21 15:34:56 -07:00
yassin
51f0f40c2f test(e2e): invoke a real published a2a agent and assert it replies
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-21 22:33:07 +00:00
Mateo Wang
9d714b0b29
Merge pull request #34172 from BerriAI/litellm_scrub_customer_name
chore(tests): replace a customer name and domain with neutral placeholders
2026-07-21 15:22:35 -07:00
tin-berri
869ef0cbfb
Merge pull request #34072 from BerriAI/litellm_mcp_ema_assertion_store
feat(mcp): store the enterprise IdP identity assertion at SSO login for EMA egress
2026-07-21 15:10:35 -07:00
mateo-berri
fa025fc474 chore(tests): replace a customer name and domain with neutral placeholders 2026-07-21 15:09:15 -07:00
Yassin Kortam
c2ae52709c
feat(proxy): make DB config-reload interval configurable via config.yaml and UI (#34130)
The add_deployment and get_credentials background jobs that keep a multi-pod
deployment in sync with config-in-DB objects (models, credentials, guardrails,
general settings, etc.) polled the database on a hardcoded 30s interval, with
no way to trade convergence latency against DB load.

Expose it as the general_setting proxy_config_reload_interval_seconds (env
PROXY_CONFIG_RELOAD_INTERVAL_SECONDS parsed via get_env_int, default 30),
threaded like the existing proxy_batch_polling_interval knob, and surface it on
the admin general-settings page so it is reachable from the dashboard and
persists to the DB for all pods. Non-positive values are rejected at the UI
(gt=0) and fall back to 30s with a warning on the env/config/DB paths.
2026-07-21 22:03:22 +00:00
Yassin Kortam
090c4b8dd8
test(e2e): cover model, tag and access group routes for Management/UI coverage (#34118) 2026-07-21 21:59:34 +00:00
Tin Chi Lo
ddae6eac6b fix(mcp): judge EMA retention only against the config and DB authorities, never the registry snapshot 2026-07-21 14:54:37 -07:00
Tin Chi Lo
cfa074d970 fix(mcp): make the EMA retention gate authoritative across pods and tighten the assertion store 2026-07-21 14:54:37 -07:00
Tin Chi Lo
83e147b553 feat(mcp): store the enterprise IdP identity assertion at SSO login for EMA egress 2026-07-21 14:54:37 -07:00
mateo-berri
b572cb80d1 test(e2e): add weekly session-anomaly load test against real providers 2026-07-21 14:54:02 -07:00
mateo-berri
43402b04bd test(e2e): register the verbatim a2a-sdk 0.3.x card and guard malformed protocolVersion 2026-07-21 14:43:09 -07:00
Yassin Kortam
48fdaaa5cd
fix(router): honor request-level num_retries over global litellm_settings.num_retries (#34124)
The async @client wrapper stamped the global litellm.num_retries onto the raised
exception via setattr(e, "num_retries", ...), even on router calls where the
request-level num_retries had already been popped and resolved. async_function_with_retries
then adopted that stamped global value, overwriting the request-level num_retries it had
correctly resolved. So a per-request num_retries (request body or x-litellm-num-retries
header) was silently ignored whenever litellm_settings.num_retries was set.

Only stamp num_retries on the exception when the call itself carried one (an explicit
request value or a deployment's litellm_params.num_retries), never the global fallback.
The router already resolves the global via self.num_retries, so leaving the exception
unset preserves the request-level value and lets the per-deployment path set it when present.

Resolves LIT-4516
2026-07-21 21:32:34 +00:00
Yassin Kortam
7f258264c7
test(e2e): cover budget, customer, user and org routes for Management/UI coverage (#34117) 2026-07-21 21:29:53 +00:00
Yassin Kortam
b3415f426c
test(e2e): cover key management routes for Management/UI coverage (#34114) 2026-07-21 21:29:21 +00:00
mateo-berri
c8e0253cd5 Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_a2a_e2e_tests 2026-07-21 14:29:15 -07:00
Yassin Kortam
6eb3cf2c50
test(e2e): cover team management routes for Management/UI coverage (#34115) 2026-07-21 21:29:13 +00:00
Yassin Kortam
e9ac84dc8b
fix(proxy): stop save_config from snapshotting environment_variables into the DB (#34119)
save_config wrote the entire merged config to the DB config table on every call. Because get_config() resolves os.environ/ placeholders to plaintext and merges the environment_variables section, any endpoint that does get_config() then mutates one section then calls save_config() (/add/allowed_ip, delete_callback, model and cost-tracking settings, and others) accidentally persisted an environment_variables row holding all YAML/OS-sourced env vars. Once that row existed the DB overlay shadowed YAML and container env on every subsequent startup, so config/env changes were silently ignored

save_config now pops environment_variables from the DB write unless the caller passes include_env_vars=True. The dedicated /config/update path already writes env vars per-section via _upsert_section, so no current caller needs to opt in

Resolves LIT-2009
2026-07-21 21:23:28 +00:00
Yassin Kortam
fcaf673418
fix(router): stop per-deployment num_retries from double-counting as provider max_retries (#34129)
* fix(router): stop per-deployment num_retries from double-counting as provider max_retries

A model group with one deployment and num_retries set in the deployment's
litellm_params sent (1 + num_retries) ** 2 requests upstream instead of
1 + num_retries. The deployment's num_retries reached litellm.completion, which
copied it onto max_retries and set it on the provider client, so the provider SDK
retried num_retries times inside each of the Router's 1 + num_retries attempts.

The Router is the sole retry owner for routed calls, so completion() now forces the
provider-SDK max_retries to 0 whenever the call originates from the Router/proxy
(detected via model_group in the request metadata) and only keeps the num_retries
to max_retries alias for direct, non-routed litellm calls (the instructor use case).
This also stops a request- or deployment-level max_retries from nesting on top of
the Router's retries.

Resolves LIT-4385

* fix(router): make router-origin check robust and close test clients

Address review: detect the router marker in both metadata and litellm_metadata
independently (a non-empty metadata without model_group no longer hides a
model_group in litellm_metadata), and close the injected async clients in the
test fixture.
2026-07-21 21:13:32 +00:00
tin-berri
914079a68c
Merge pull request #34065 from BerriAI/litellm_lit4629_issuer_discovery
fix(mcp): let an admin-pinned issuer drive OAuth discovery for url-less servers
2026-07-21 14:06:55 -07:00
mubashir1osmani
ac5b51253a
test(e2e): add Other suite and Guardrails coverage incl. an MCP tool-call guardrail (#34149)
* test(e2e): add other suite covering master-key auth and health lifecycle

Covers the other.* holding-pen cells that were uncovered: master-key
valid_allows/invalid_denied on the admin /user/list gate, and the
lifecycle probes liveness.ping, readiness.public_probe,
readiness.reports_db_status, and readiness_details.authenticated_diagnostics.

New tests/e2e/other/ suite on the shared ProxyClient; the health probes
send no auth header to prove the public routes need no credential, and the
details route is asserted to reject an anonymous caller while exposing
version/db diagnostics to the master key.

* test(e2e): cover block_code_execution and openai_moderation guardrails

Extends the guardrails suite with two built-in guardrails registered per
request (default_on=False, opted in via the chat body's guardrails selector)
so neither intercepts unrelated traffic on the shared proxy.

block_code_execution.pre_call.blocks: a python code block plus a run-this
request is intercepted with the canned content-blocked message and the model
never runs, while the same code block asked about with don't-run-it reaches
the model. Verified live.

openai_moderations.pre_call.blocks: a flagged prompt is rejected 400 naming
the moderation policy while a benign prompt passes. The guardrail calls
OpenAI's moderation API; verifying it needs an OpenAI key with moderation
quota (this account currently 429s the moderation endpoint).

Adds a shared create_backend_model helper and a generic register() plus
per-request guardrails/max_tokens on the client so more built-ins can reuse
the same path.

* test(e2e): cover presidio PII masking (pre_call + post_call)

Registers a presidio guardrail per request (default_on=False) with the
analyzer/anonymizer bases supplied in the registration params, so the test
controls its own dependency and needs no proxy restart.

presidio.pre_call.masks: a repeat-verbatim request comes back with the
<EMAIL_ADDRESS> placeholder and never the raw email, proving the prompt was
anonymized before the model saw it.

presidio.post_call.masks: with apply_to_output the model's own emitted email
is masked on the way out, so the caller never receives the raw value.

Both verified live against real presidio analyzer + anonymizer containers.
logging_only is intentionally not covered: /spend/logs exposes no prompt
messages to read back the masked log, and a logging_only run also masked the
response, contradicting its contract; noted in the module docstring for a
follow-up.

* test(e2e): cover presidio logging_only masking via OTEL read-back

Adds the third presidio cell, guardrail.presidio.logging_only.masks. The
logging_only contract (mask what is logged, do not block) is verified by
reading the request's gen-AI span back from the real OTEL destination: the
span's gen_ai.input.messages attribute carries the <EMAIL_ADDRESS> placeholder,
never the raw email, and the call itself is not blocked.

Reads the trace via the shared OtelReader, promoted from logging/ to the suite
root so both suites use it. The masked prompt is polled to a deadline because
logging_only masks the payload asynchronously and the span can briefly export
before the mask lands. Drops the throwaway chat_send in favor of the existing
transport.send for the call-id capture.

* fix(e2e): tolerate cross-pod guardrail sync delay in team-opt-out test

Stage runs multiple gateway pods behind the shared key. POST /guardrails
registers a new default-on guardrail in-process immediately only on the
pod that served the create call; every other pod picks it up on its next
periodic DB sync (proxy_server.py, every 30s), so the very next chat call
can race a pod that has not synced yet. Poll to a 40s deadline instead of
asserting on the first response, matching the existing pattern in
test_budget_reset_advances_e2e.py.

* test(e2e): cover a guardrail on the MCP tool-call path (content_filter pre_mcp_call)

Adds guardrail.litellm_content_filter.pre_mcp_call.blocks: against the real
Datadog MCP server, a content_filter guardrail configured mode=pre_mcp_call
blocks a banned keyword in an MCP tool call's arguments with HTTP 400 attributed
to the pre_mcp_call hook, and lets a clean argument reach the upstream server.

The guardrail attaches with default_on because per-key/request guardrail
selection is dropped from the synthetic MCP request the hook sees; the banned
keyword is unique per run so default_on only intercepts this test's own call.
mode must be pre_mcp_call - a pre_call config silently no-ops on tools/call
because the event type is rewritten for call_mcp_tool.

Drives the tool directly via /mcp-rest/tools/call for a deterministic check of
the same pre_mcp_call enforcement the OpenAI-SDK chat path hits when a model
invokes an MCP tool.

* fix(e2e): mid-conversation messages test uses client.proxy not client.gateway

EndpointsClient exposes .proxy after the Gateway->ProxyClient rename; the
mid-conversation system test still referenced .gateway, which fails the e2e
basedpyright gate. Aligns it with the rest of the harness.

* test(e2e): address review on the guardrail coverage

MCP tool-call guardrail: poll the banned call until the guardrail is enforced
instead of asserting on the first call, so the control-plane -> data-plane
guardrail sync cannot race the check into a false pass-through; add a repeat
banned call after enforcement to guard against a partial-propagation state.

OpenAI moderation: distinguish a moderation-endpoint 429 (rate limit / no
moderation quota) from a guardrail failure, so an account-capability gap reads
as such rather than as "did not block". Runs green with a moderation-capable key.

* test(e2e): close partial-propagation false-pass in MCP guardrail block test

The single post-block repeat call could be load-balanced back to the same
already-synced data-plane pod, so the test could pass while another pod still
lacked the guardrail and let the banned MCP call reach Datadog. Anchor a wait to
the guardrail create time (every pod is guaranteed to have DB-synced only after a
full ~30s sync interval), then require the banned call to stay blocked across
several attempts; a pass-through after that window is a real leak, not a race.

* test(e2e): drop xfail-style rate-limit branch from openai_moderation test

OpenAI's /v1/moderations is free and returns 200 with the env key (verified
directly), so the RateLimitedError branch mislabeled the failure: a 429 there is
insufficient_quota (no account billing), not throttling. The branch also only
printed a softer message before failing anyway, an xfail-in-disguise the e2e rules
forbid. A 429 now falls through and fails loudly with the full result.
2026-07-21 14:06:29 -07:00
ryan-crabbe-berri
76c9eca25d
refactor(auth): derive temp budget increase without mutating the token (#34121)
* refactor(auth): derive temp budget bump without mutation, tz-aware auth datetimes

_update_key_budget_with_temp_budget_increase mutated max_budget in place, so correctness depended on every resolution path handing it a fresh copy of the cached token; one future re-cache of a live token would compound the bump per request. Return a model_copy instead so no caller can leak an increased budget into shared state.

Also fixes the three remaining DTZ005 naive datetime.now() calls in user_api_key_auth.py (auth span start, builder start_time, service-log end_time; all consumers convert to epoch or subtract same-pair datetimes) and ratchets the DTZ005 strict budget 244 -> 241.

* test: pin non-mutation of the temp budget helper input

Adversarial mutation-testing showed reverting the helper to in-place mutation still passed every test: the cache's copy-on-read layer masks the mutation in the integration test and the direct unit test only inspected the return value. Assert the input object is left untouched and the result is a distinct object so the purity guarantee itself is load-bearing.
2026-07-21 21:02:40 +00:00
Yassin Kortam
30ed840ff5
fix(e2e): use EndpointsClient.proxy after Gateway to ProxyClient rename (#34127)
The mid-conversation-system messages test still referenced the removed
client.gateway attribute, so the tests/e2e basedpyright gate reported 9
errors and went red on every e2e PR. EndpointsClient exposes .proxy, so
point the /v1/messages post helper at client.proxy.transport.
2026-07-21 20:53:40 +00:00
Tin Chi Lo
6959d9de69 test(mcp): pin the token and register wall messages for url-less servers 2026-07-21 13:53:14 -07:00
Tin Chi Lo
930676a9bf fix(mcp): let an admin-pinned issuer drive OAuth discovery for url-less servers 2026-07-21 13:53:14 -07:00
tin-berri
1d36290551
Merge pull request #34063 from BerriAI/litellm_lit4629_openapi_oauth_egress
fix(mcp): attach resolved OAuth credentials to OpenAPI spec_path tool calls
2026-07-21 13:51:17 -07:00
Mateo Wang
a7e4c83013
Merge pull request #34154 from BerriAI/litellm_a2a_protocol_version_semver
fix(a2a): accept semver protocolVersion values like 0.3.0 in agent cards
2026-07-21 13:37:05 -07:00
ryan-crabbe-berri
efa997dfe0
feat(budgets): add configurable budget_reset_time of day (#31007)
Budgets reset at midnight in the configured timezone with no way to control
the time of day, so a drained daily budget surfaces as an overnight incident.
Add a litellm_settings.budget_reset_time option (e.g. "12:00") that shifts
day/week/month resets to a configurable wall-clock time in the existing
timezone, so the end of the budget window lands during business hours.

The reset time is parsed once into an immutable BudgetResetSettings and
injected into the reset job (constructor) and computation, rather than read
from a module-level global at call time. A malformed value fails fast at
startup. Sub-day durations ignore the offset. Unset preserves midnight resets.
2026-07-21 13:35:01 -07:00
mateo-berri
2310211531 fix(a2a): reject malformed protocolVersion suffixes while keeping semver prereleases 2026-07-21 13:24:58 -07:00
yassin
1315ebd1f9 test(e2e): guard 0.3.0-style semver protocolVersion registration
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-21 20:14:21 +00:00
mateo-berri
062e58fb1d fix(a2a): accept semver protocolVersion values like 0.3.0 in agent cards 2026-07-21 13:03:47 -07:00
yuneng-jiang
fcd236097e
fix(interactions): add queued to the Interaction status enum (#34135)
Google added a queued value to Interaction.status in the live Interactions
OpenAPI spec, so the compliance canary test_status_enum_values started
failing on every open PR. The exact-match assertion is deliberate; it is
how we find out the spec moved, so this adds the new value rather than
loosening the check, and mirrors it into the generated Status enums so
InteractionStatus stays truthful.
2026-07-21 12:36:21 -07:00
yucheng-berri
049c6836d2
fix(model_armor): sanitize error details by default (#33908)
* fix(model_armor): sanitize error details by default

Generated with AI

Co-Authored-By: Claude Code

* fix(model_armor): sanitize handler-raised HTTP errors and redact scanned content in guardrail logging

The async HTTP handler raises MaskedHTTPStatusError on any non-2xx via
raise_for_status, so the non-200 branch in make_model_armor_request never ran
against a live API and the raw upstream body reached callers and logs. Catch
the raised error and build the sanitized detail from the response status

Replace the empty-dict guardrail logging payload with field-level redaction of
the keys that echo scanned content (text, sanitizedText, findings) so guardrail
traces keep filter states and block reasons while scanned content stays out

Restore the upstream status code in the sanitized error detail, read guardrail
metadata from the same key the hooks write, and keep guardrail_status within
its typed literal values

* fix(model_armor): bound redactor recursion depth and allowlist it in the recursion detector

_redact_scanned_content walks provider JSON bounded by _REDACT_MAX_DEPTH=20 and
fails closed by returning the redaction sentinel at the cap

* fix(model_armor): honor fail_on_error for upstream API failures

API failures now raise a dedicated ModelArmorAPIError so hooks can tell them
apart from content-block HTTPExceptions; fail_on_error=False lets the request
proceed on a Model Armor outage again while fail-closed configs get the same
sanitized 400 as before

Also addresses review notes: sanitize_error_detail constructor annotation
matches the nullable config field, redaction is owned by the metadata write
sites so _process_response no longer re-applies it, and the request and
response debug log branches move into helpers

* test(model_armor): cover fail_on_error routing on during-call, post-call, streaming, and file-scan paths

* chore: remove accidentally committed pytest cache files

* fix(model_armor): keep sanitize_error_detail coerced across in-memory config reloads

update_in_memory_litellm_params assigns raw LitellmParams fields, so a hot
reloaded config carrying an explicit null would silently disable sanitization;
re-apply the only-explicit-False-opts-out coercion after the update

* fix(model_armor): redact matched malicious URIs and reuse the shared recursion depth constant

maliciousUriMatchedItems echoes the caller-supplied URL including path and
query, so it joins the scanned-content key set; the redactor depth cap now
comes from DEFAULT_MAX_RECURSE_DEPTH in litellm constants instead of a local
literal

* fix(model_armor): keep API failures out of the intervention trace status

Fail-closed upstream failures re-raise ModelArmorAPIError instead of
converting to HTTPException(400), so the shared guardrail logging keeps
recording them as guardrail_failed_to_respond while content blocks stay
guardrail_intervened. Callers see the same 500 shape as before this PR,
with the sanitized message

* chore(model_armor): drop explanatory comment per repository comment policy

---------

Co-authored-by: eugene-yao-zocdoc <eugene.yao@zocdoc.com>
2026-07-21 10:28:24 -07:00
mateo-berri
e411d637b3 feat(gemini): day-0 pricing for gemini-3.6-flash and gemini-3.5-flash-lite 2026-07-21 08:50:40 -07:00
Mateo Wang
7e66f00fca
Merge pull request #34068 from BerriAI/litellm_codex_auto_drop_params 2026-07-20 23:45:29 -04:00