Commit graph

83 commits

Author SHA1 Message Date
yucheng-berri
f4f59ec4c3
fix(guardrails): honor configured timeout in Zscaler AI Guard (#36110)
The shared `timeout` guardrail param already parsed into LitellmParams, but
the Zscaler initializer never forwarded it and _send_request hardcoded a 5
second constant, so a configured value was silently ignored and slow scans
failed with `Timeout passed=5` regardless of config.

Forward litellm_params.timeout through to the HTTP call, keep 5 seconds as
the default, fall back to it for non-positive values, and declare the field
on the config model so the dashboard renders it.
2026-08-07 00:25:52 +00:00
jwang-gif
bb58f019a0
fix(proxy): fix zguard httpcode when block input (#31948)
* fix(zscaler_ai_guard): return 400 on guardrail block

* fix(zscaler_ai_guard): don't log error on intentional BLOCK

A BLOCK is expected guardrail behavior, not a failure. Before this
fix, raising HTTPException inside the try block caused the generic
except to log it as "Failed to apply guardrail", producing spurious
error-level noise for every normal block event.

Added except HTTPException: raise before the generic handler (matching
the existing pattern in make_zscaler_ai_guard_api_call), and a
regression test that asserts logger.error is not called on a BLOCK.

---------

Co-authored-by: yucheng-berri <yucheng@berri.ai>
2026-08-05 01:46:05 +00:00
yucheng-berri
9ad8698aab
feat: add deepkeep as custom guardrail (#33844)
* adding deepkeep as custom guardrail

* adding deepkeep as a custom guardrail

* adding deepkeep as a custom guardrail (hooks)

* adding litellm/proxy/_experimental/out/ to .gitignore

* adding deepkeep as custom guardrail in litellm

* removing sentinel_fortress

* comparing schema.prisma files

* fix(deepkeep): address greptile review comments

- extra_headers: fix type annotation (list -> Dict[str, str]) and actually
  merge them into _build_request_headers() so user-configured headers
  reach the DeepKeep API
- user_api_key_hash: only fall back to user_api_key_token when no
  explicit hash is already set, avoiding silent overwrite
- apply_guardrail: preserve tool_calls and structured_messages in the
  return value so downstream callers don't lose that content

Adds tests for all four fixes.

* fix(deepkeep): address greptile review comments

- extra_headers: fix type annotation (list -> Dict[str, str]) and actually
  merge them into _build_request_headers() so user-configured headers
  reach the DeepKeep API
- user_api_key_hash: only fall back to user_api_key_token when no
  explicit hash is already set, avoiding silent overwrite
- apply_guardrail: preserve tool_calls and structured_messages in the
  return value so downstream callers don't lose that content

Adds tests for all four fixes.

* fix: add missing __init__.py and allowlist entries for upstream merge

- tests/test_litellm/proxy/client/__init__.py: fixes pytest collection
  collision with tests/test_litellm/models/test_models.py (same basename)
- tests/test_litellm/models/__init__.py: same fix
- backend/routes/allowlist.py: add /config_overrides/ and /v1/unified_access_group
  prefixes for new routes added by upstream

* fix(ui/tests): resolve frontend-lint failures in new test files

- useLogDetails.test.ts: add Wrapper.displayName, replace 'null as any'
  with null, type resolveCall promise resolver properly
- usePaginatedDailyActivity.test.ts: remove unused waitFor import,
  add Wrapper.displayName, change Record<string,any> to Record<string,unknown>
- UsageViewSelect.adminFiltering.test.tsx: replace all props:any with
  explicit SelectProps/BadgeProps/SelectOption types, replace (X as any).displayName
  with direct X.displayName assignment

no-explicit-any count: 2034 (budget: 2040). Prettier check: clean.

* fix(ui): sync proxy/_experimental/out/ exactly to upstream

245 stale JS chunk files from earlier merges were left in the out/
directory but had been deleted in upstream. The Docker image in CI is
built by copying this directory verbatim, so the stale artifacts caused
the SERVER_ROOT_PATH redirect E2E to fail.

Synced by: git checkout upstream/litellm_internal_staging -- out/ (adds
new files) + git rm on every file present in HEAD but absent from
upstream.

* Update litellm/proxy/guardrails/guardrail_hooks/deepkeep/deepkeep.py

Co-authored-by: veria-ai[bot] <224490171+veria-ai[bot]@users.noreply.github.com>

* fix(makefile): fall back to upstream/litellm_internal_staging for strict-budget gate

origin/litellm_internal_staging exists on BerriAI's CI but not on forks
that use a different remote name (e.g. Azure DevOps as origin).  Fall
back to upstream/litellm_internal_staging when the origin ref is absent.

* linter reformat

* fix(deepkeep): apply guardrail tool/tool_call redactions from API response

When DeepKeep returns GUARDRAIL_INTERVENED with redacted tools or
tool_calls, the previous code ignored those redactions and forwarded
the original (potentially sensitive) values to the model — a guardrail
bypass for content embedded in tool schemas or function arguments.

Fix: prefer response_json["tools"] / response_json["tool_calls"] when
present, falling back to the originals only when the guardrail did not
return replacements — consistent with the existing pattern for texts and
images.

Refactor _build_return_inputs() into a private static helper to keep
apply_guardrail() under the PLR0915 statement limit (50).

Adds test_apply_guardrail_applies_tool_redactions_from_response to
assert that redacted tool payloads from the API response are used.

* Update litellm/proxy/guardrails/guardrail_hooks/deepkeep/deepkeep.py

Co-authored-by: veria-ai[bot] <224490171+veria-ai[bot]@users.noreply.github.com>

* fix(lint): move base-ref fallback into ruff_strict_gate.py; revert Makefile

The previous Makefile fix had a shell bug: 'git rev-parse --verify'
writes the resolved SHA to stdout, so the $$(...) substitution captured
both the SHA and the echo output, handing '--base <sha>\norigin/...' as
two tokens to the Python script, causing exit code 1 in CI.

Fix: revert Makefile to its original single-line invocation and add
_resolve_base() to ruff_strict_gate.py. The function checks whether the
requested ref resolves; if not, it tries the 'upstream/' equivalent
before falling back to the original ref (letting git emit a clear error).

Behaviour in BerriAI CI: origin/litellm_internal_staging resolves → used
as before, no change.
Behaviour on forks with a different 'origin': falls back to
upstream/litellm_internal_staging transparently.

* fix(lint): fix UP006/UP045/F401 in changed files; add depth guard to check_any_discipline

- Replace Dict/List/Optional/Tuple typing imports with built-in equivalents
  (UP006, UP045) across files touched in this PR diff, then clean up
  the now-unused typing imports (F401).
- Add _MAX_CONTAINS_ANY_DEPTH guard to check_any_discipline.contains_any()
  to prevent RecursionError on deeply-nested mypy types.

* fix(lint): resolve all three CI lint job failures

1. lint (ruff_strict_gate) — UP006/UP045/F401 violations introduced on
   changed lines. Fixed Dict/List/Optional/Tuple → built-in equivalents
   across every file in the PR diff; cleaned up now-unused typing imports.

2. any-discipline — RecursionError in check_any_discipline.contains_any()
   on deeply-nested mypy types. Upstream fixed this by converting to an
   iterative stack-based algorithm (merged). Also added deepkeep.py to
   any-discipline-budget.json via 'make lint-any-budget-update' so the
   new file's Any count is baselined instead of failing against the
   zero-baseline default.

3. basedpyright reportMissingParameterType — **kwargs in DeepKeepGuardrail
   __init__ lacked a type annotation. Added **kwargs: Any.

* Update litellm/deepkeep_tilt_config.yaml

Co-authored-by: veria-ai[bot] <224490171+veria-ai[bot]@users.noreply.github.com>

* fix(lint): black reformat after merge

* fix(deepkeep): honour empty-list replacements in _build_return_inputs

When DeepKeep returns GUARDRAIL_INTERVENED with an intentional empty
replacement (e.g. texts:[], tool_calls:[]) the previous truthiness check
treated [] as absent and forwarded the original content downstream —
a guardrail bypass for any case where the firewall wants to fully clear
a field.

Fix: replace all response_json.get(field) truthiness checks with
'is not None' comparisons so that an empty list is respected as a
deliberate replacement. Applies to texts, images, tools, tool_calls,
and the original-input fallback guards.

Adds test_apply_guardrail_honours_empty_list_replacements.

* fix(test): replace live httpbin.org call with mocked transport in test_pass_through_with_httpbin_redirect

Root cause of OOM: the test made a real HTTP request to https://httpbin.org
inside a pytest-xdist worker. Under memory pressure the worker's httpx client
and redirect-following logic allocated enough virtual memory to trip the OOM
killer (confirmed by ulimit -v 16GB reproducing the crash with 'node down: Not
properly terminated' on this exact test).

Fix: replace the real network call with a custom httpx.AsyncBaseTransport that
returns a pre-built 302 -> 200 response sequence in-memory. The test now runs
hermetically with no network dependency and no excess memory allocation.

ulimit -v 16GB: 24,284 passed (0 crashes) after this fix.

* fix: merge upstream/litellm_internal_staging (197 commits), resolve conflicts

7 conflicts resolved:
- 6 Python files: upstream added new code with old-style typing (Optional,
  Dict, List) on lines where we had ruff-fixed modern syntax (str | None,
  dict, list). Took upstream's version then re-ran ruff UP006/UP045/F401
  --fix to keep both the new content and ruff compliance.
- test_openapi_compliance.py: upstream replaced 'role' with 'steps' in
  output_fields and updated the spec comment. Took upstream's version.

Also: added _resolve_base() fallback to type_check_gate.py and removed
the hard 'git fetch origin litellm_internal_staging' from the Makefile's
lint-basedpyright target (same pattern as ruff_strict_gate.py fix).

* fix: merge upstream (41 commits), resolve .gitignore conflict, fix BLE001

- .gitignore: upstream removed package.json/out/ ignore entries; took theirs
- deepkeep.py: added '# noqa: BLE001' on catch-all Exception handler
  (BLE001 rule newly enforced in ruff-strict-budget)
- type_check_gate.py: added _resolve_base() fallback for basedpyright gate
- Makefile: removed hard 'git fetch origin' from lint-basedpyright target

* fix: merge upstream (57 commits), resolve conflicts

- Makefile: upstream added lint-fetch-base target; made it tolerant of
  missing origin/litellm_internal_staging (git fetch || true)
- test_websearch_chat_completion.py: took upstream's new assertions and
  skipif marker
- anthropic_cache_control_hook.py: upstream added new code using List/Dict/Tuple
  which were undefined after our earlier UP006 cleanup; replaced with
  built-in list/dict/tuple

* fix(coverage): revert ruff UP006/UP045 changes on upstream files

The previous ruff fixes (Dict→dict, Optional→X|None) on 7 upstream files
added ~500 changed lines of pure type-annotation no-ops to our PR diff.
codecov/patch penalised these uncovered lines, dropping patch coverage
to 51.35% (target 61.83%).

Fix: revert these files to exactly match upstream/litellm_internal_staging.
The ruff_strict_gate still passes because the violations exist equally in
both the base and HEAD (total == base_count → no breach).

* fix: merge upstream (130 commits), resolve Makefile + base_email conflicts

- Makefile: upstream changed lint deps to $(LINT_DEP_INSTALL)/$(LINT_DEP_BASE);
  kept our --base removal (handled by _resolve_base in Python scripts)
- base_email.py: took upstream's dedup cache addition
- deepkeep.py: ruff format after merge

* chore: remove lint/format-only changes and non-feature files

Revert all lint-infra and black/ruff-reformat-only changes back to
upstream/litellm_internal_staging so the PR diff shows only the DeepKeep
guardrail feature:
- Makefile, scripts/ruff_strict_gate.py, scripts/type_check_gate.py
  (lint-gate infra)
- credential_migration.py + enterprise/* + assorted test files
  (black-reformat / xdist test-isolation drift)
- backend/routes/allowlist.py (merge glue)
Remove non-feature local artifacts: build-and-push.sh,
deepkeep_tilt_config.yaml, stray __init__.py collision shims, and
unrelated UI test files.

* fix(lint): add reason to BLE001 noqa to satisfy type-discipline gate (LIT003)

The type-discipline budget ratcheted LIT003's ceiling to 292 as upstream
fixed reasonless suppressions, so our '# noqa: BLE001' (code but no
reason) tipped the total to 293 and failed CI. Add a reason per the
required '# noqa: CODE  # <reason>' shape.

* fix(deepkeep): apply structured_messages redactions returned by the guardrail API

_build_return_inputs dropped any structured_messages the DeepKeep API returned and
always forwarded the original input, so redactions on that field never took effect.
Check the response first, same as texts/images/tools/tool_calls

* chore(ui): drop redundant preserve prop from the guardrail form

preserve defaults to true in rc-field-form (isMergedPreserve falls back to true when
unset), so the explicit prop changed nothing and only widened this PR's blast radius
to every guardrail provider in the shared form

* fix(deepkeep): stop extra_headers list from crashing the guardrail call and name the real firewall id config key

litellm_params.extra_headers is a list of header names to forward, so passing it
straight into dict.update raised ValueError and, under fail_closed, took the request
down with it. Only merge mapping values and warn otherwise

The docstring example and the missing-secret error both said firewall_id, but
initialize_guardrail only reads deepkeep_firewall_id, so anyone following them
had their value silently ignored

* refactor(proxy): drop normalize_callback change; split to its own PR (#33905)

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

---------

Co-authored-by: Yaniv Israel <yaniv@deepkeep.ai>
Co-authored-by: DK-yaniv <164404355+DK-yaniv@users.noreply.github.com>
Co-authored-by: veria-ai[bot] <224490171+veria-ai[bot]@users.noreply.github.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-20 19:27:40 -07:00
yucheng-berri
6eed38bcfb
fix(guardrails/bedrock): honor disable_exception_on_block by raising ModifyResponseException (#32289)
* fix(guardrails/bedrock): honor disable_exception_on_block by raising ModifyResponseException

The Bedrock-specific GuardrailInterventionNormalStringError predates the
unified guardrails refactor and no proxy code path handles it, so a block
with the flag set surfaced as an uncaught Exception -> HTTP 500 in pre_call
mode and was silently discarded in during_call mode (model call proceeded
in the parallel asyncio.gather; the block hook's data["mock_response"]
mutation happened after route_request had already unpacked kwargs).

Convert the block to ModifyResponseException at the raise site inside
make_bedrock_api_request. That exception is the industry-standard proxy
contract already caught in proxy_server, anthropic_endpoints, response_api
_endpoints, and pass_through_endpoints; it turns into a 200 response with
finish_reason=content_filter and the block message as content, which is
exactly what the flag was documented to yield. Post-call blocks attach
the LLM response to original_response so the synthetic reply reports the
upstream call's real token usage instead of zero.

Deletes the now-orphaned GuardrailInterventionNormalStringError class and
the dead create_guardrail_blocked_response / mock_response plumbing in the
Bedrock hooks; updates the existing tests that had locked in the buggy
contract.

Resolves LIT-4186

* chore(guardrails/bedrock): drop dead str branch in _update_messages_with_updated_bedrock_guardrail_response

Follow-up to the disable_exception_on_block fix. That method used to
receive either a BedrockGuardrailResponse or a plain string (the block
message, when the flag was set). Now that a block always raises
ModifyResponseException before this method runs, the string branch is
unreachable; tighten the type to BedrockGuardrailResponse and delete
the guard.

* fix(guardrails/bedrock): streaming post_call block yields synthetic stream instead of surfacing as SSE 500

Regression from the LIT-4186 refactor: pre-refactor, the streaming
post_call iterator caught GuardrailInterventionNormalStringError locally
and replaced the assembled response with a synthetic content-filter
message, then re-emitted it as chunks via MockResponseIterator. After
the refactor the exception was re-raised as ModifyResponseException,
which async_streaming_data_generator serializes as a proxy 500 error
frame because the SSE response headers are already flushed by the time
the block fires.

Non-streaming paths still let ModifyResponseException propagate to the
endpoint handler (which converts it into a 200). Streaming can't do
that, so keep the local synthesis: on the exception, rebind the
assembled response to a ModelResponse whose single choice carries the
block message as content and finish_reason=content_filter, and let the
downstream MockResponseIterator emit it as chunks. Same shape a
non-streaming block produces.

Adds a mapped-file regression test that mutation-kills the raise
behavior and locks in the synthetic-stream contract.

* fix(guardrails/bedrock): preserve upstream usage on streaming post_call block

Non-streaming post_call blocks report the upstream LLM call's real
token usage via ModifyResponseException.original_response, which the
endpoint handler unwraps through _blocked_response_usage. Streaming
post_call synthesizes its own ModelResponse locally (the exception
can't escape the SSE generator), and previously left .usage unset,
so the client saw accurate billing on non-streaming blocks and zero
on streaming blocks -- silent revenue leak.

Copy the assembled response's .usage onto the synthetic block
response before yielding. Pre-refactor code had the same gap
(create_guardrail_blocked_response never set usage); this is a net
improvement, not a regression fix.
2026-07-09 13:18:51 -07:00
Sameer Kankute
cfcdf8714a
feat: litellm oss 110626 (#30202)
* Add gpt-realtime-whisper Realtime transcription support (OpenAI + Azure) (#29775)

* Add gpt-realtime-whisper Realtime transcription support (OpenAI + Azure)

Adds first-class support for the gpt-realtime-whisper streaming speech-to-text
model, which uses the Realtime transcription session API rather than the
file-based /audio/transcriptions path.

Model registration: registers gpt-realtime-whisper and azure/gpt-realtime-whisper
with audio-duration pricing (input_cost_per_second = 0.017/60, matching the
published $0.017/minute input audio rate).

REST endpoint: implements POST /v1/realtime/transcription_sessions (plus /realtime
and /openai/v1 aliases) to mint an ephemeral transcription session for the
WebRTC flow. Adds request/response types, OpenAI and Azure URL builders, a shared
base handler (refactored from the client_secrets handler), the
acreate_realtime_transcription_session SDK function, and route registration. The
proxy encrypts the ephemeral key returned under client_secret.value and records
the session type in the token so the follow-up /realtime/calls replays
type=transcription rather than type=realtime.

WebSocket: forwards intent=transcription through to the Azure handler (OpenAI
already received it) with URL-encoding, so gpt-realtime-whisper opens a
transcription session. Transcription-only sessions no longer trigger an
erroneous response.create.

Cost tracking: transcription sessions emit no response.done events; their usage
arrives on conversation.item.input_audio_transcription.completed as
{type: duration, seconds}. That usage is captured out-of-band (usage only, no
transcript duplication) and billed by input_cost_per_second, with a token-billed
fallback for token-priced transcription models.

Adds tests for pricing math, URL builders, request/response types, the proxy
route and SDK function, WebSocket intent forwarding, transcription-session
streaming behavior, and the /realtime/calls session-type replay.

* Address PR review: URL-encode all Azure WS query params; forward query_params through provider_config branch

* Address PR review: session_type validation, model auth fix, cost perf, billing fallback, detail/docs cleanup

* Improve test coverage: detection from backend, error paths, unknown usage type, resolved_model None

* Backport realtime transcription websocket fixes

* Enforce authorized realtime transcription model

* Enforce realtime transcription model access

* Enforce realtime resolved model scopes

* Enforce WebRTC transcription model scope

* Lazy evaluate debug log in pass-through endpoint (#30177)

* Pass through debug lazy logging

* fix(proxy): convert remaining eager pass-through debug logs to lazy formatting

* fix(parallel_ai): migrate search integration from v1beta to v1 endpoint (#30157)

* fix(parallel_ai): migrate search integration from v1beta to v1 endpoint

The Parallel Search API moved from /v1beta/search (processor: base/pro,
parallel-beta header) to /v1/search (mode: turbo/basic/advanced, no beta
header). Request fields moved too: max_results, source_policy, and excerpt
settings are now nested under advanced_settings, and source_policy uses
include_domains/exclude_domains. The v1 response returns publish_date per
result, which now maps to SearchResult.date instead of being hardcoded to
None. The legacy processor param is mapped to the equivalent mode so
existing callers keep working.

* fix(parallel_ai): default mode to basic and simplify param handling

The v1 API defaults to advanced mode when mode is omitted, while v1beta
defaulted to the base processor. Without an explicit default, callers who
pass no mode would be silently upgraded to a tier costing 2.25x more while
litellm's cost map reports the basic-tier price. Sending mode=basic
preserves the v1beta default and keeps cost tracking accurate.

Also replaces the handled_params set with pop-as-consumed param handling so
mapped params no longer need to be tracked in two places, and extends the
tests to pin the default mode, processor=base mapping, mode-over-processor
precedence, and top-level v1 param passthrough.

* fix(parallel_ai): avoid double /v1 when api_base is already versioned

A PARALLEL_AI_API_BASE like https://api.parallel.ai/v1 previously produced
.../v1/v1/search. Strip a trailing /v1 before appending the search path and
cover the api_base variants with a parametrized test.

---------

Co-authored-by: shin-berri <shin-laptop@berri.ai>
Co-authored-by: yuneng-jiang <yuneng@berri.ai>

* feat(focus): add Mavvrik destination for FOCUS export (#29935)

* fix: preserve responses streaming flag (#30189)

* fix: preserve responses streaming flag

* test: cover async responses streaming flag

* fix(spend/daily-activity): stable offset pagination via id tiebreaker (#30164) (#30167)

date alone is not a unique sort key for LiteLLM_DailyUserSpend or
LiteLLM_DailyTeamSpend (many rows per date: api_key x model x
model_group x provider x endpoint). Offset pagination over a
non-unique sort landed on arbitrary boundaries, so a client paging
through all results and summing per-page metrics (the Usage dashboard)
got non-deterministic totals - sometimes inflated, sometimes deflated,
different at different page_size values.

Adding the row's UUID id (present on both tables) as a secondary sort
gives every page a stable cursor. order=[{date desc}, {id asc}].

Fixes #30164

* fix(oci): inject a default maxTokens so omitted max_tokens doesn't truncate responses (#30018)

* fix(oci): inject default maxTokens so omitted max_tokens doesn't truncate

OCI GenAI applies a tiny server-side maxTokens default (~20 tokens) when the
request omits it, so any call that doesn't send max_tokens comes back cut off
mid-string with finishReason "length". MLflow judges never send max_tokens, so
their JSON responses arrived as unterminated strings and json.loads failed in
MLflow's gateway adapter.

When no maxTokens/maxCompletionTokens target is set, inject
DEFAULT_OCI_CHAT_MAX_TOKENS (env-overridable, defaults 4096), mirroring the
Anthropic config's default-max-tokens behaviour. An explicit max_tokens still
wins, and reasoning models still route to maxCompletionTokens. Used a fixed
default rather than the catalog max_output_tokens because the catalog value is
unreliable for some models (grok-4 reports max_output_tokens equal to its
context window, not a real output cap, which would risk 400s).

Adds TestOCIDefaultMaxTokens covering Cohere and generic injection, the
explicit-override case, and the reasoning maxCompletionTokens branch.

* test(oci): e2e regression that omitted max_tokens isn't truncated

Real-proxy integration test asserting a chat completion that omits max_tokens
completes with finish_reason "stop" instead of being cut off at OCI's ~20-token
server default. Fails before the maxTokens-default injection (finish_reason
"length", ~19 tokens), passes after.

* test(oci): update cohere default-params test for injected maxTokens

test_cohere_default_parameters asserted no maxTokens was injected, encoding the
old behaviour where OCI's ~20-token server default truncated responses. Now
that transform_request injects DEFAULT_OCI_CHAT_MAX_TOKENS, assert maxTokens
equals that default while the other params (topK/topP/frequencyPenalty) stay
pass-through with no hardcoded default.

* fix(oci): make DEFAULT_OCI_CHAT_MAX_TOKENS a plain constant

Drop the os.getenv override. The env knob was not requested and introducing a
new env var forced a cross-repo dependency on litellm-docs (test_env_keys.py
validates every referenced env var against the docs table there). A plain 4096
constant keeps the PR self-contained; callers who want a different limit pass
max_tokens explicitly per request.

* fix(oci): route all OpenAI commercial models to maxCompletionTokens

OCI serves OpenAI models (gpt-4.1, gpt-5.1 through 5.5, o-series) that
the litellm catalog doesn't track, so the supports_reasoning lookup
returned False for them and the provider sent maxTokens, which the
reasoning families reject with HTTP 400. With the injected default
maxTokens this broke every request to those models, not just ones with
an explicit max_tokens. Route the whole openai.* vendor prefix to
maxCompletionTokens since OpenAI accepts max_completion_tokens on every
chat model; the openai.gpt-oss-* open weights are served by OCI's own
stack and keep maxTokens. Verified live against gpt-5.2, gpt-5, gpt-4o,
gpt-4.1, gpt-oss-120b, llama-3.3, command-a and grok-3-mini

* test(oci): hoist transformation imports and drop unused ones

Makes the generic-chat test file ruff-clean: the per-test local imports
of OCIChatConfig/OCIVendors shadowed the module-level import (F811) and
left it unused (F401), and json plus three OCI type imports were never
referenced

* fix(oci): translate response_format json_schema to OCI's accepted shape (#29691)

* fix(oci): translate response_format json_schema to OCI's accepted shape

OCI GenAI rejected every json_schema response_format with HTTP 400
"Please pass in correct format of request", which broke structured-output
callers such as MLflow LLM judges (they always send a json_schema).

The provider forwarded OpenAI's raw json_schema body unchanged. For GENERIC
models OCI's ResponseJsonSchema accepts only name/description/schema/isStrict,
so OpenAI's `strict` key (and any other extra) 400s the request; the key must
be renamed to isStrict and the body whitelisted. For Cohere models there is no
JSON_SCHEMA type at all; the schema has to ride on JSON_OBJECT as
{"type": "JSON_OBJECT", "schema": ...}. Cohere type values must also be the
canonical uppercase TEXT/JSON_OBJECT.

_normalize_response_format now branches by vendor and emits the exact shape
each one accepts (verified live against OCI GenAI for Cohere, Meta, Gemini and
Grok). Drops the unused, incorrect Cohere response-format pydantic models.

Two existing tests asserted the broken behavior (lowercase type, raw
jsonSchema on Cohere); they are rewritten to assert the corrected shape, and
generic/Cohere json_schema regression tests are added.

* fix(oci): raise early on json_schema response_format with no body

A GENERIC model request with {"type": "json_schema"} and no json_schema
object fell through to the JSON_OBJECT branch and emitted a bodyless
{"type": "JSON_SCHEMA"}, which OCI rejects with an opaque HTTP 400. Raise a
descriptive 400 at translation time instead. Cohere is unaffected since it
always maps to JSON_OBJECT.

* test(oci): gateway integration test for response_format json_schema

Added to tests/integration/ (the real-network integration suite) reusing the
existing OCI proxy harness, not tests/llm_translation/ which is mock-only.

---------

Co-authored-by: Sameer Kankute <sameer@berri.ai>

* fix(oci): accept default n=1 on Cohere instead of hard-failing (#29705)

* fix(oci): accept default n=1 on Cohere instead of hard-failing

Cohere on OCI has no numGenerations field, so n was mapped to False and
map_openai_params raised "param `n` is not supported on OCI" whenever a client
sent n. But n=1 (and None) is the OpenAI default single-generation request,
which every OCI model produces anyway, so standard clients that always send
n=1 (such as the MLflow gateway) were rejected with a 500.

Drop n=1/None silently for Cohere; only n>1 is genuinely unsupported and still
raises (or drops under drop_params). Generic models are unaffected and keep
numGenerations, including n>1.

* docs(oci): explain why n is not advertised for Cohere despite tolerating n=1

* test(oci): gateway integration test for Cohere default n=1

Added to tests/integration/ (the real-network integration suite) reusing the
existing OCI proxy harness, not tests/llm_translation/ which is mock-only.

---------

Co-authored-by: Sameer Kankute <sameer@berri.ai>

* fix(oci): drop max_retries instead of hard-failing on OCI (#29727)

max_retries is a litellm-level control param (litellm applies retries itself),
not a generation param OCI accepts. The provider mapped it to False and raised
"param `max_retries` is not supported on OCI" whenever it was present. The
litellm proxy injects max_retries on every request, so any OCI call through the
proxy 500'd unless drop_params was set.

Drop max_retries silently in map_openai_params. Adds a unit test (Cohere and
generic) and a gateway integration test that a plain request succeeds through a
proxy without drop_params.

Co-authored-by: Sameer Kankute <sameer@berri.ai>

* fix(spend-logs): rehydrate metadata JSONB text on ui_view_spend_logs (#29682)

Fixes #29674.

`/spend/logs/ui` raw-SQL path returns the JSONB metadata column as a
string — prisma's query_raw skips the ORM-layer hydration. The UI reads
metadata.status / metadata.error_information as object fields, so
provider-failure rows look like successes.

Fix: json.loads the metadata field right after query_raw, fall back to
{} on malformed JSON.

3 existing error-code/error-message tests called json.loads on
response.data[0]["metadata"] — they were leaning on the bug. Updated
to read the dict directly. Plus 2 new regression tests (failure metadata
roundtrip + invalid-json fallback). Reverting the fix makes both new
tests fail with AssertionError: metadata should be dict, got <class 'str'>.

* fix(proxy): release max_parallel_requests slot when a stream is cancelled mid-flight (#27955) (#30020)

* fix(proxy): release max_parallel_requests slot when a stream is cancelled mid-flight (#27955)

* fix: refund max_parallel_requests on disconnect from outer streaming generators

The cancellation refund previously lived in async_post_call_streaming_iterator_hook,
but that hook is nested inside the outer streaming generators and a nested async
generator only receives GeneratorExit on garbage collection (non-deterministic).
With only the v3 limiter enabled, /chat/completions also bypasses the hook entirely
(needs_iterator_wrap() is false). Move the release into async_data_generator and
async_streaming_data_generator, the generators Starlette closes on client disconnect,
so the refund fires deterministically on every streaming route. Warn when no event
loop is running, and document the window TTL refresh on the decrement

* fix(mcp): propagate model into model_call_details for passthrough tool calls (#30122)

* fix(mcp): propagate model into model_call_details for passthrough tool calls

The @client decorator on call_mcp_tool creates the logging object via
function_setup without a model kwarg, so model_call_details["model"]
starts as None. execute_mcp_tool only set logging_obj.model as an
instance attribute, which the spend-log writer never reads (it reads
kwargs["model"] from model_call_details). MCP passthrough tools/call
rows therefore persisted with model="" while list_tools rows showed
"MCP: list_tools", degrading the Logs UI display and bucketing all MCP
tool spend under an empty model in DailyUserSpend.

Propagate the model into model_call_details alongside the existing
attribute assignment so the StandardLoggingPayload and SpendLogs writer
pick it up. Covers the /mcp passthrough, REST /mcp-rest/tools/call, and
orchestrated paths (the latter already passed model into function_setup,
so this is a no-op there).

* test(mcp): trim regression test docstring

* fix(mcp): surface upstream challenges for delegated OAuth (#30124)

* fix(mcp): surface upstream challenges for delegated OAuth

* docs(mcp): clarify delegated upstream auth comments

* perf(benchmarks): add CPU timing metrics to streaming benchmark (#29980)

* Add CPU timing metrics to streaming benchmark

* Fix spacing around timing sample dataclass

* fix(gemini): don't emit empty choices on metadata-only stream chunks (#29167)

web_search + reasoning makes Gemini stream mid-chunks that carry only
grounding/thought metadata — no content part, no finishReason.
_process_candidates skips content-less candidates and the existing
fallback only ran when finishReason was set, so choices stayed empty
and the downstream streaming handler raised IndexError on choices[0].
Emit an empty-delta choice for content-less chunks regardless of
finishReason.

Fixes #28884

* fix(key): allow /key/update to clear budget_limits with [] or null (#30085)

* Fix /key/update rejecting budget_limits clear requests with HTTP 400

Sending budget_limits: [] or null to /key/update returned HTTP 400, so
once a key had budget windows the last one could never be removed.

prepare_key_update_data only json.dumps'd budget_limits when the value
was truthy, so [] and None passed through raw to the Prisma Json?
column; jsonify_object only serializes dicts, and prisma-client-py has
no DbNull sentinel for Json? writes, so Prisma rejected both shapes.

Serialize the clear case explicitly as the JSON literal null, matching
how memory_endpoints encodes metadata for the same column type. Truthy
values keep the existing reset_at window initialization path.

Fixes #30067.

* Require admin access for budget_limits changes on /key/update

Clearing budget_limits via [] or null is a budget mutation, but
_validate_update_key_data only counted max_budget and spend as budget
changes before deciding whether to skip _check_key_admin_access. A
non-admin key owner or a team member with /key/update could therefore
remove a key's per-window spend caps without admin authorization.

Treat any explicit budget_limits value in the request (set, change, or
clear) as a budget change so it gates through the same admin check as
max_budget. model_fields_set is used because an explicit null is
indistinguishable from an omitted field by value alone.

* fix(proxy): persist guardrail info in spend logs for /v1/responses (#30092)

Pre-call guardrail blocks on /v1/responses wrote guardrail_information
as null in LiteLLM_SpendLogs because _handle_logging_proxy_only_error
splits request_data by LoggedLiteLLMParams keys and litellm_metadata,
where the Responses API stores request metadata including
standard_logging_guardrail_information, was not among them. It fell
into optional_params, so merge_litellm_metadata never saw it. Add
litellm_metadata to LoggedLiteLLMParams so it routes into
litellm_params the same way metadata does on the chat completions path

Fixes #28971.

* fix(proxy): handle non-standard SSE frames in Anthropic passthrough logging (#26000)

Some third-party Anthropic-compatible providers emit non-standard SSE
frames (OpenAI-style [DONE] sentinels, non-JSON keep-alive lines) in
streaming responses. These caused json.JSONDecodeError in
_build_complete_streaming_response, breaking the passthrough logging
pipeline so the request was never logged or billed.

Skip whole-line 'data: [DONE]' sentinels and catch JSONDecodeError per
event. Matching the full line (not a substring) keeps a valid chunk
whose text payload contains '[DONE]' from being dropped.

Co-authored-by: shin-berri <shin-laptop@berri.ai>
Co-authored-by: yuneng-jiang <yuneng@berri.ai>
Co-authored-by: Sameer Kankute <sameer@berri.ai>

* feat(newrelic): Add New Relic extension  (#26989)

* initial New Relic integration.

* Minor fixes for basic observability.

* Implemented basic support for the success path. Generates New Relic
custom events needed by the AI Monitorin interface.

* Supportability metric is sent on first request.

* Emit supportability metric every hour instead of once a day.

* Add the start/end times to the messages before sending them so that the
start time and end time reflect the correct time and both are not set
to 'now'.

* Make use of `turn_off_message_logging` configuration that is available
by default from CustomLogger.

* Enabling New Relic agent to be wired when docker container starts if an environment variable
is set.

* If we cannot find trace information, send the AI events without the
trace ID attached.

* Use a fake trace_id if we cannot find one.

* Implementing a configuration so that users can use litellm configuration
to disable sending LLM messages to New Relic. There is a second method
to do this via New Relic env var.

* Mised file.

* Cleaning up logic to turn off recording content via either the
LiteLLM configuration or an env var.

* Removing debugging.
Fixed logic / comments around how often to send supportability metric.

* Initial version of public doc for New Relic.

* Use a proper name for the doc file.

* Updating newrelic.md document.

* Updating LiteLLM documentation for New Relic extension.

* Moving New Relic imports into the methods to support unit tests.

* Adding unit tests for the New Relic extension.

* Updating linting and the unit tests that are not running in the CI environment.

* Address reviewer feedback on New Relic integration.

- Fix _record_error_metric to use app.record_custom_metric() instead of
  module-level newrelic.agent.record_custom_metric() so the call works
  outside of an active transaction context
- Remove unreachable except ImportError block in _get_trace_context
- Update stale "23 hours" comment to "27 hours" (matches 97200s threshold)
- Remove commented-out debug code from _process_success
- Fix docs typo: NEW_RELIC_CUSTOM_INSIGHTS_EVENTS_MAX_SAMPLES_STOREDA ->
  NEW_RELIC_CUSTOM_INSIGHTS_EVENTS_MAX_SAMPLES_STORED
- Update TestRecordErrorMetric to verify app.record_custom_metric call

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

* Reformating for the linter.

* Addressing additional automated feedback.

- Removed a legacy comment about the New Relic header
- Reordered imports in one file
- Switched another file to use the import at the top of the file instead of inline when used
- Added unit tests for untested methods that were identified

* Addressing new feedback.

- Proper handling of time to floats. Created a util method and updated code to use it.
- added the missing guard to ensure the app is enabled

* Addressing feedback.

- When an error occurs, still check if the periodic supportability metric should be emitted
- Added a check to ensure the extension is ready in the error handler to match _process_success

* Updating the NR event timestamps to more accurately reflect when
the messages were generated.

* Addressing feedback for potential better practice.

* Addressing feedback on accessing default values. Added tests for most of
these cases.

* Adding a new catch exception block based on feedback.

* Addressing feedback about a potential issue around a timestamp for the
supportability metric.

* Addressing minor feedback on length of generated, fallback traceId.

* Addressing feedback.

- A few more cases were found where the dictionary access might not return the correct value.
- Handling cases where `traceparent` is not lower cased

* Addressed feedback where the newrelic options might not apply correctly.

* Addressing some feedback.

* Addressing feedback.

* Validating testing / formatting for our changes.

* Updating linting, adding tests, defining data type for UI.

* Configuration for the logging callback definition.

* Adding a newrelic image for the UI to use.

* Putting the New Relic callback in proper alphabetic order.

* Copying the logo to a committed output directory so it shows up in a locally
built container.

* Adding missing definition of new env vars that were causing a build failure.

* Addressing automated feedback from greptile.

* Adding a few more unit tests to increase the code coverage just a bit more.

* Additional unit tests to push coverage to almost 90%.

* Adding a custom newrelic docker image build process. This removes the need to add the newrelic agent
to the core litellm container or dependencies.

* Clarifying message when the New Relic agent is not installed and someone
is trying to use the newrelic extension. Either use the proper image
when using docker, or install the agent manually when running from source.

* Ensuring pip is available to install the New Relic agent.

* Updating the definition and handling of traceId (no spanId).
Clarifying behavior of env vars vs UI configuration for
the newrelic extension.

* Removing entries from the New Relic logger configuraiton UI as these
values must be set as part of running the image.

* Removing a stale doc file that has moved to the litellm-docs repo.
Cleanup of Dockerfile to remove a LABEL that was incorrect.

* Updating container image name to be the best guess for the new name.

* Addressing feedback from greptile.

- Added a comment around token_count=0
- Updated the boolean parser to allow a wider set of options which matches existing patterns in other parts of LiteLLM.

* Removing option for a separate New Relic container image. The agreement
is to handle this in the New Relic integration docs.

* Updating error message when New Relic agent is not available.

* Wiring in the test message from the LiteLLM callback UX.

* Missed saving one of the file conflicts.

* Fixed a lint error I introduced. Somehow, I dropped another string
and now added it back.

* Adding newrelic to the schema definition.

* Added an admin check on the call before sending test message
as mentioned by the AI code review.

* Updating to use should_redact_message_logging(kwargs) as part of the
logic to determine if message content should be sent to New Relic
or not. This still uses the `record_content` property as well, but
both have to be true in order for content to be included.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add Azure AI Foundry DeepSeek V3.1 and V4 Pro/Flash global pricing to cost map (#30134)

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

* fix(logging): translate Responses bridge result to ModelResponse for spend logs (#28985)

PR #29394 fixed the AnthropicResponse.model_validate crash for the streaming
anthropic_messages -> OpenAI Responses bridge by unwrapping terminal events
and returning the inner ResponsesAPIResponse. The spend_logs row lands and
usage/cost are correct, but the row's response field stores the Responses
API shape (output[...].content[...].text). The proxy UI Logs tab reads
response.choices[0].message via parseMessages in prettyMessagesUtils.ts
with no fallback for the Responses shape, so the OutputCard renders "No
response data available" for every cross-routed call. The same shape
mismatch affects every downstream consumer of spend_logs that assumes the
canonical chat-completion shape

This change keeps the unwrap from #29394 but routes the resulting
ResponsesAPIResponse (and the bare-response non-streaming path) through
LiteLLMResponsesTransformationHandler.transform_response, which is the
same conversion already used by the chat-completion Responses bridge.
Spend_logs now stores a ModelResponse with choices[0].message.content, so
the UI and other consumers see the assistant text. On a translation
failure (eg. empty output on an incomplete response) the handler falls
back to a minimal ModelResponse carrying model and usage so the row still
lands rather than being dropped as a Non-Blocking error

Also corrects a stale comment in the Responses adapter that implied the
call type was reclassified to acompletion; the code preserves
anthropic_messages and the success handler translates back to
ModelResponse for the row

Fixes #28595

* fix(anthropic-adapter): re-emit first delta on streaming content-block transitions (#30024)

* fix(anthropic-adapter): re-emit first delta on streaming content-block transitions

The `/v1/messages` -> `/v1/chat/completions` streaming adapter
(`AnthropicStreamWrapper`) silently dropped the first non-empty delta of
every content block that started via a *transition* (e.g. text -> tool_use ->
text, text -> thinking).

When an upstream chunk both triggers a new content block (its type differs
from the active block) and carries that block's first delta, the wrapper
emitted `content_block_stop` -> `content_block_start` and then only re-queued
the trigger chunk when it was an `input_json_delta` (bundled tool args). The
synthesized `content_block_start` always carries an empty body, so the first
`text_delta` / `thinking_delta` was lost — the client output started from the
second token (e.g. "Hi, how can I help you?" rendered as ", how can I help
you?", or text resuming after a tool call lost its first sentence). This is
especially visible with Claude Code-style clients that consume Anthropic
Messages streaming events strictly.

Fix: re-queue the trigger chunk's translated delta whenever it carries
non-empty content (text/thinking/signature/tool args), via a shared
`_trigger_delta_has_content` helper used by both the sync and async paths.
Empty trigger deltas are still suppressed so no spurious empty
`content_block_delta` is introduced.

Fixes #30014

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

* test(anthropic-adapter): cover all _trigger_delta_has_content branches

Add a direct parametrized unit test for the re-emit predicate so every delta
type (text/input_json/thinking/signature), the empty-payload guards, and the
malformed/non-delta cases are exercised independently of upstream chunk
translation. Raises patch coverage for the new helper.

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

---------

Co-authored-by: shin-berri <shin-laptop@berri.ai>
Co-authored-by: yuneng-jiang <yuneng@berri.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat: add opt-in healthy_only filter to GET /v1/models (#30130)

* feat: add opt-in healthy_only filter to GET /v1/models

Adds an opt-in `healthy_only=true` query parameter to GET /v1/models and
GET /models that hides models whose backing deployments are all marked
unhealthy by background health checks.

- Add Router.async_get_fully_unhealthy_model_names(), mirroring the
  semantics of get_fully_blocked_model_names(): a model is hidden only
  when every backing deployment is unhealthy and the health state is
  not stale (fail open otherwise).
- Reuses the existing DeploymentHealthCache populated by
  _run_background_health_check(), so no new health state is introduced.
- No-op when allowed_fails_policy is set, mirroring
  _async_filter_health_check_unhealthy_deployments semantics.
- team_public_model_name aliases are aggregated alongside model_name.
- Hiding is presentation-only; default behavior is unchanged.

Fixes #30128

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

* docs: address Greptile review notes

- Note team-alias asymmetry vs get_fully_blocked_model_names
- Debug-log when healthy_only is set but no health state is available

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

---------

Co-authored-by: shin-berri <shin-laptop@berri.ai>
Co-authored-by: yuneng-jiang <yuneng@berri.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Dedupe team soft budget alerts by team_id instead of token (#30097)

_team_soft_budget_check sends type="soft_budget" alerts with
event_group=TEAM, but SoftBudgetAlert.get_id always returned the
request token. The alert cache key was therefore scoped per virtual
key, so every active key in a team over its soft budget fired its own
alert within budget_alert_ttl. Branch on event_group so team-level
alerts dedupe by team_id, matching TeamBudgetAlert, while key and
project level alerts keep per-token dedupe.

Fixes #27398.

* feat(bedrock guardrails): support contextual grounding qualifiers (request-side) (#30057)

* test: add failing tests for Bedrock contextual grounding (request-side)

Drive the request-side of Bedrock contextual grounding: callers tag message
content blocks as grounding_source/query, the post_call hook assembles an
ApplyGuardrail(OUTPUT) call carrying source + query + response(guard_content),
and the bedrock converse transform must render the tags as prompt text instead
of silently dropping them. Non-grounding payloads must stay byte-identical.

* feat(bedrock guardrails): support contextual grounding qualifiers

Bedrock contextual grounding scores a model response against a reference
source and the user query, expressed via a per-content-block `qualifiers`
array on ApplyGuardrail. The guardrail hook previously sent plain text only,
so grounding could not be driven through it even though the response-side
contextualGroundingPolicy parsing already existed.

Callers now tag message content blocks `{"type":"grounding_source"}` /
`{"type":"query"}` (mirroring the existing `guarded_text` marker). On the
generate path the bedrock converse transform renders them as plain text; at
post_call the hook harvests them from the request and assembles one
ApplyGuardrail(OUTPUT) call carrying grounding_source + query + the response
(as guard_content). Requests without these tags produce a byte-identical
payload, so existing behaviour is unchanged.

* Feat(guardrail): Adding support for custom Ovalix guardrail (#21887)

* Feat(guardrail): Adding support for custom Ovalix guardrail

* Internal CR comments fixes

* greptileai comments fixes

* fix conflict

* fixes

* fix sha256

* clarify Ovalix actor-id hash is for normalization, not PII protection

* fix(github_copilot): normalize per-event item_id in /responses streaming (#30072)

GitHub Copilot's native /v1/responses stream assigns a different item_id to
every event of a single output item (output_item.added, the part.added /
delta / done events, and output_item.done). Spec-strict clients like the
Vercel AI SDK key streaming parts by item_id and abort with
"reasoning part <id> not found" / "text part <id> not found" when a delta
references an unregistered id.

Override transform_streaming_response in GithubCopilotResponsesAPIConfig to
anchor every event of an output item to the id from its output_item.added.
Copilot accepts that id paired with the final encrypted_content on the next
turn, so multi-turn replay is unaffected.

Fixes #30071

* feat: add /model/block and /model/unblock endpoints (#30125)

* feat: add /model/block and /model/unblock endpoints

Add dedicated proxy-admin POST /model/block and /model/unblock endpoints
over the existing blocked flag on LiteLLM_ProxyModelTable, mirroring the
/key/block and /key/unblock pattern. Calling a model whose deployments are
all blocked now returns a clear 403 "Model is blocked" instead of a generic
no-deployment error, including direct-dispatch route types (e.g. eval) via a
pre-route guard. Includes audit-log entries for block/unblock and unit tests.

Closes #29742

Signed-off-by: AgentGymLeader <264910004+AgentGymLeader@users.noreply.github.com>

* chore: regenerate dashboard API types for model block/unblock endpoints

Regenerate ui/litellm-dashboard/src/lib/http/schema.d.ts from the proxy
OpenAPI spec (npm run gen:api) so it includes the new endpoints.

Signed-off-by: AgentGymLeader <264910004+AgentGymLeader@users.noreply.github.com>

* fix: widen router block-helper param type and add direct unit tests

Type the _are_all_deployments_blocked deployments parameter to match its
callers (DeploymentTypedDict) so mypy passes, and add
tests/test_litellm/test_router_block_helpers.py with direct unit tests for
the three block helper methods so router_code_coverage recognizes them.

Signed-off-by: AgentGymLeader <264910004+AgentGymLeader@users.noreply.github.com>

* fix: restore type-ignore on messages arg after black reflow

Signed-off-by: AgentGymLeader <264910004+AgentGymLeader@users.noreply.github.com>

* refactor: raise model-block 403 in proxy layer, not SDK Router

Keep the SDK Router's documented behavior for blocked deployments (filtered ->
"no healthy deployment") and move the 403 PermissionDeniedError into the proxy
layer (route_llm_request), where model blocking is an admin concept. This avoids
a backwards-incompatible 403 for SDK users who set blocked=True on their own
deployments, per maintainer review.

Signed-off-by: FugoP <264910004+AgentGymLeader@users.noreply.github.com>

---------

Signed-off-by: AgentGymLeader <264910004+AgentGymLeader@users.noreply.github.com>
Signed-off-by: FugoP <264910004+AgentGymLeader@users.noreply.github.com>
Co-authored-by: AgentGymLeader <264910004+AgentGymLeader@users.noreply.github.com>
Co-authored-by: Sameer Kankute <sameer@berri.ai>

* fix: add week unit support to get_next_standardized_reset_time (#30100)

* fix: add week unit support to get_next_standardized_reset_time

The function handled d/h/m/s/mo units but silently fell through to
the default next-midnight branch for the w (week) unit. This was
inconsistent: _extract_from_regex already accepted w in its character
class, and duration_in_seconds already returned value * 604800 for it.

Add the missing elif unit == 'w' branch that delegates to
_handle_day_reset with value * 7, which reuses the existing Monday-
alignment logic for 1w and the generic N-day-from-midnight path for
larger multiples.

Add test_week_based_resets covering 1w from a Wednesday (expects next
Monday) and 2w from a Monday (expects 14 days forward at midnight).

Signed-off-by: FugoP <264910004+AgentGymLeader@users.noreply.github.com>

* test: exercise relative week semantics with non-Monday base dates + add docstring

Signed-off-by: FugoP <264910004+AgentGymLeader@users.noreply.github.com>

---------

Signed-off-by: FugoP <264910004+AgentGymLeader@users.noreply.github.com>
Co-authored-by: FugoP <264910004+AgentGymLeader@users.noreply.github.com>

* fix: black formatting and remove undocumented MAVVRIK_FOCUS_FREQUENCY env var

* fix: black formatting with correct version and sync schema.d.ts for healthy_only param

* fix: resolve mypy errors and add transcription_sessions to JSON schema endpoint enum

* fix: restore MAVVRIK_FOCUS_FREQUENCY guard and exclude it from docs key scan

* fix: address Greptile P2 comments - move constant, use UTC datetime, skip redundant team lookup

* revert: restore original team lookup logic in can_key_call_resolved_model

---------

Signed-off-by: AgentGymLeader <264910004+AgentGymLeader@users.noreply.github.com>
Signed-off-by: FugoP <264910004+AgentGymLeader@users.noreply.github.com>
Co-authored-by: Emerson Gomes <emerson.gomes@thalesgroup.com>
Co-authored-by: nina-hu <nina.huuu@gmail.com>
Co-authored-by: Sahith Jagarlamudi <104647530+s-jag@users.noreply.github.com>
Co-authored-by: shin-berri <shin-laptop@berri.ai>
Co-authored-by: yuneng-jiang <yuneng@berri.ai>
Co-authored-by: Praveen Ghuge <95286176+pghuge-cloudwiz@users.noreply.github.com>
Co-authored-by: alex107ivanov <30668368+alex107ivanov@users.noreply.github.com>
Co-authored-by: hcl <chenglunhu@gmail.com>
Co-authored-by: Fede Kamelhar <federico.kamelhar@oracle.com>
Co-authored-by: Armaan Sandhu <74664101+Ar-maan05@users.noreply.github.com>
Co-authored-by: Teo Xian Zhong Augustine <35527068+auggie246@users.noreply.github.com>
Co-authored-by: King Star <mcxin.y@gmail.com>
Co-authored-by: Saksham Maggo <122939011+SakshamMaggo@users.noreply.github.com>
Co-authored-by: Filippo Menghi <113345637+Cyberfilo@users.noreply.github.com>
Co-authored-by: Kelvin <leikaiwei@outlook.com>
Co-authored-by: Josh Bonczkowski <josh.bonczkowski@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: M. Dennis Turp <mdturp@pm.me>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Piotr Minkina <piotrminkina@users.noreply.github.com>
Co-authored-by: Martín Alcalá Rubí <martin@tryolabs.com>
Co-authored-by: T. Kobayashi <13004314+nix-tkobayashi@users.noreply.github.com>
Co-authored-by: João Costa <13508071+jpv-costa@users.noreply.github.com>
Co-authored-by: Shalom <shalom@ovalix.io>
Co-authored-by: codgician <15964984+codgician@users.noreply.github.com>
Co-authored-by: FugoP <kim@pomsora.com>
Co-authored-by: AgentGymLeader <264910004+AgentGymLeader@users.noreply.github.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-06-11 22:30:26 -07:00
Mateo Wang
f11c12d157
Revert "chore(tests): migrate Bedrock CI to AWS account 941277531214 (#28728)" (#29326)
This reverts the Bedrock CI account migration (#28728). The original account
(888602223428) was put under an AWS security restriction after a leaked key
and has since been reactivated, while the replacement account (941277531214)
lacks access to several models the suites exercise (legacy Bedrock Claude 3
models, Cohere, Nova Canvas image gen, Bedrock batch inference, and flagship
Opus). Pointing CI back at the reactivated account restores that coverage.

This is the exact inverse of #28728: all hardcoded 941277531214 references go
back to 888602223428 (provisioned/imported-model ARNs, AgentCore runtime ARNs
and their suffixes, batch execution role ARN, and the example proxy config),
the S3 buckets revert to litellm-proxy and load-testing-oct, the guardrail IDs
revert to wf0hkdb5x07f and ff6ujrregl1q, the SageMaker endpoint and Knowledge
Base revert to their original ids, and the live-call tests go back to the
legacy model strings. The grid_spec fail_reason workaround for the unentitled
Opus cells is dropped while keeping the unrelated bedrock_effort_ceiling field
added after the migration.

The CircleCI AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY env vars still point at
941277531214 and must be set to the reactivated account's fresh credentials
separately via the CircleCI API; AWS_REGION_NAME stays us-west-2.
2026-05-30 11:26:24 -07:00
Mateo Wang
f9407bc036
chore(tests): migrate Bedrock CI to AWS account 941277531214 (#28728)
* chore(tests): migrate Bedrock CI from AWS account 888602223428 to 941277531214

The original account (888602223428) was put under a security restriction by
AWS after a root access key leaked in a PR comment. While that account works
its way through the AWS Support unlock process, Bedrock-touching CI tests have
been migrated to a fresh account (941277531214).

Changes:
  - Replace 26 hardcoded references to 888602223428 with 941277531214 across
    8 files (provisioned-model ARNs, imported-model ARNs, AgentCore runtime
    ARNs, batch execution role ARN, and example proxy config).
  - The provisioned-model and imported-model ARNs are referenced only from
    mocked unit tests — no AWS resources to recreate.
  - The batch execution IAM role has been recreated in the new account with
    the same name and equivalent permissions.
  - The two AgentCore runtimes (hosted_agent_r9jvp-3ySZuRHjLC,
    hosted_agent_13sf6-cALnp38iZD) are being recreated in the new account
    under the same names — see tools/agentcore-deploy/ in a follow-up.

CircleCI env vars AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_REGION_NAME
were updated separately via the CircleCI API to point at the new account.

Smoke-tested locally against the new account:
  aws bedrock-runtime converse --region us-west-2 \
    --model-id us.anthropic.claude-sonnet-4-5-20250929-v1:0 \
    --messages '[{"role":"user","content":[{"text":"ping"}]}]'
  → 200, model returned 'pong'

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

* chore(tests): refresh AgentCore ARN suffixes to match newly-deployed runtimes

The first migration commit replaced just the account ID, but AgentCore
auto-assigns a random 10-char suffix to every runtime on creation — we
can't reuse the original suffixes (`3ySZuRHjLC`, `cALnp38iZD`) in the
new account. Updated the AgentCore-runtime ARNs in the three files that
reference real runtime IDs (not the mock-based unit-test ARNs).

Deployed runtimes:
  arn:aws:bedrock-agentcore:us-west-2:941277531214:runtime/hosted_agent_r9jvp-Rq79QFC2fp
  arn:aws:bedrock-agentcore:us-west-2:941277531214:runtime/hosted_agent_13sf6-4046UzHSwy

Both runtimes are status=READY and pass a smoke invoke:
  $ aws bedrock-agentcore invoke-agent-runtime --agent-runtime-arn ... --payload '{"prompt":"ping"}'
  → 200, {"result": "echo: ping"}

The agent is a minimal echo (see /tmp/agentcore_deploy/agent.py for the
deploy artifacts). Tests that only verify the SDK wiring will pass; if any
test asserts on agent output content, swap the echo for the real agent.

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

* chore(tests): point Bedrock batch tests at new-account S3 bucket

The account migration (888602223428 -> 941277531214) was a flat
account-ID swap, which only rewrites ARNs that embed the account
number. S3 bucket names carry no account ID, so the live Bedrock
batch tests still uploaded to `litellm-proxy` — a bucket that lives
in the old account. S3 names are globally unique, and the old account
still holds that name, so it can't be recreated in the new account.

Rename to `litellm-proxy-941277531214` (account-ID suffix guarantees
global uniqueness). The bucket must be created in 941277531214 and the
batch execution role granted s3:GetObject/PutObject/ListBucket on it
before this job is run in CI.

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

* chore(tests): point live S3 logging test at new-account bucket

Same account-ID-free blind spot as the batch bucket: `load-testing-oct`
lives in the old account and its name can't be reused globally. The
`logging_testing` CI job is wired into the workflow and runs
test_basic_s3_logging, which uploads to this bucket with the CI env
creds, then lists and deletes objects — a live dependency.

Rename to `load-testing-oct-941277531214`. The bucket must exist in the
new account with the CI IAM principal granted
s3:PutObject/GetObject/ListBucket/DeleteObject before this job runs.

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

* chore(tests): repoint Bedrock guardrail IDs to new-account guardrails

The migration left guardrail IDs untouched (no account ID in them), so
all live guardrail tests failed with "guardrail identifier or version
does not exist" against 941277531214. Recreated both guardrails in the
new account and updated the hardcoded IDs:
  - wf0hkdb5x07f -> zgkmukebruil (PII mask: PHONE + CREDIT_DEBIT_CARD,
    with explicit inputAction=ANONYMIZE so masking applies to INPUT,
    which is the source litellm's moderation hook sends)
  - ff6ujrregl1q -> 4w3d1di3snt5 (blocks "coffee"; blocked message set
    to the exact string the tests assert on)

Updated test_bedrock_guardrails.py, otel_test_config.yaml, and the
guardrailConfig in test_bedrock_completion.py. Verified locally: the 5
previously-failing guardrail tests now pass.

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

* test(bedrock): migrate legacy models to current inference profiles

The new CI account (941277531214) cannot invoke legacy Bedrock models
(AWS gates them: "marked by provider as Legacy... not actively using in
the last 30 days"). Migrated the live-call tests:
  - anthropic.claude-3-sonnet-20240229    -> us.anthropic.claude-sonnet-4-5-20250929-v1:0
  - anthropic.claude-3-haiku-20240307     -> us.anthropic.claude-haiku-4-5-20251001-v1:0
Current Claude models on Bedrock require the us. inference-profile prefix
(bare on-demand ids are rejected).

cohere.command-r-plus has no working replacement (all Cohere is legacy-
gated in the new account): swapped to claude-haiku-4-5 in provider-
agnostic param lists. amazon.titan-image-generator skipped (no working
replacement). Mocked/transformation/cost tests that reference the legacy
strings are intentionally left unchanged. Verified live against the new
account.

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

* test(bedrock): repoint SageMaker + Knowledge Base to new-account resources

These referenced account-scoped resources by hardcoded id that only
existed in the old account, so the migration's account-ID swap missed
them. Recreated in 941277531214 and repointed:
  - SageMaker endpoint jumpstart-dft-hf-textgeneration1-mp-20240815-185614
    -> litellm-ci-textgen (gpt2 on a TGI container, ml.g5.xlarge)
  - Bedrock Knowledge Base T37J8R4WTM -> LCYXFBR2TU (OpenSearch Serverless
    vector store + titan-embed-text-v2, seeded with a LiteLLM doc)
Verified live: test_sagemaker.py (12 passed) and
test_bedrock_knowledgebase_hook.py (12 passed).

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

* test(reasoning_effort_grid): skip bedrock claude-opus-4-7 cells (not entitled on 941277531214)

claude-opus-4-7 is listed in the new Bedrock CI account's foundation
models but invoke is denied (AccessDeniedException: "not available for
this account"). Bedrock access to the flagship Opus requires an AWS
Sales request, not the self-serve model-access toggle, so it can't be
enabled inline with the rest of the account migration.

Add an optional `skip_reason` to ModelEntry and set it on the
bedrock-claude-opus-4-7 entry; the grid test honors it via pytest.skip.
Cell count (231) and route coverage are unchanged, so the structural
asserts still pass. Restore coverage by deleting the one skip_reason
line once access is granted.

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

* test(bedrock): swap/skip legacy-gated models unavailable on new CI account

The migrated AWS account (941277531214) cannot access several models that
the old account could, so the remaining red CI jobs were hitting real
Bedrock "Access denied / Legacy" and "account not authorized" errors:

- image_gen: skip both Nova Canvas test classes (amazon.nova-canvas-v1:0 is
  legacy-gated), matching the existing titan skip.
- batches: skip test_async_file_and_batch (Bedrock batch inference is not
  authorized on the new account; requires an AWS support case).
- litellm_overhead: swap legacy claude-3-5-haiku for the active
  us.anthropic.claude-haiku-4-5 inference profile.
- test_completion_claude_3_function_call: swap legacy claude-3-sonnet for the
  active us.anthropic.claude-sonnet-4-5 inference profile.

https://claude.ai/code/session_01Y7zgHYu9GX29YRwV4yiWAa

* test(bedrock): fix remaining e2e legacy-model + batch failures on new CI account

- e2e_openai_endpoints: skip test_bedrock_batches_api (Bedrock batch inference
  is not authorized on account 941277531214) and migrate the missed
  s3_bucket_name in oai_misc_config.yaml to litellm-proxy-941277531214.
- build_and_test: swap legacy bedrock claude-3-sonnet for the active
  us.anthropic.claude-sonnet-4-5 inference profile in the proxy structured
  output e2e test.

https://claude.ai/code/session_01Y7zgHYu9GX29YRwV4yiWAa

* test(bedrock): make opus-4-7 + batch cells fail loudly and mock image-gen (#28791)

Replace the silent skips added for the new CI account with noisier behavior:
- reasoning-effort grid: opus-4-7 cells now fail (when AWS creds are present)
  instead of skipping, so the missing entitlement stays visible in CI; they
  still skip when AWS creds are absent (local dev)
- Bedrock batch inference tests: drop the skip so they run and fail until
  batch access is granted
- Titan + Nova Canvas image-gen tests: mock the Bedrock HTTP call so the
  transform + cost-tracking path stays under test without live model access

https://claude.ai/code/session_01MT7SWDnXUjv6e6EPG7BDjT

Co-authored-by: Claude <noreply@anthropic.com>

* test(bedrock): use pytest.xfail for known-failing opus-4-7 cells

Replace pytest.fail with pytest.xfail when a model has a fail_reason,
so known-broken cells stay visible as XFAIL without keeping CI red.

Co-authored-by: Yassin Kortam <yassin@berri.ai>

---------

Co-authored-by: Mateo <mateo@Mateos-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Yassin Kortam <yassin@berri.ai>
2026-05-25 12:03:17 -07:00
Mateo Wang
bb448b0031
fix(tests): stabilize image-edit VCR cassettes to stop live gpt-image-1 spend (#28110)
* fix(tests): stabilize image-edit VCR cassettes to stop live gpt-image-1 spend

The image-edit cassettes for ``gpt-image-1`` were accumulating >50
episodes and being refused by the persister
(``tests/_vcr_redis_persister.py``), so every CI run was hitting the
real OpenAI endpoint. The async parametrize was the clearest tell:
``test_openai_image_edit_litellm_sdk[True]`` cached to 1 entry, but the
``[False]`` (async) sibling grew to 51 entries and never replayed.

Two non-deterministic sources were fueling the growth, both fixed
here. After this patch, the cassettes settle at one episode per
unique call and replay for the 24-hour TTL like every other suite.

1. Pin httpx's multipart boundary at the source. The existing
   ``_normalize_multipart_boundary`` rewrites the boundary in the
   ``Content-Type`` header reliably, but on the async transport path
   the body is not always a contiguous ``bytes`` object when
   ``before_record_request`` runs, so the body-side replacement
   silently no-ops and the recorded cassette retains the random
   ``boundary=<hex>`` string. The next CI run gets a fresh random
   boundary, the ``safe_body`` matcher misses, and
   ``record_mode="new_episodes"`` appends another episode. Wrapping
   ``httpx._multipart.MultipartStream.__init__`` so it always uses
   ``vcr-static-boundary`` when no boundary is supplied eliminates
   the variance for both sync and async paths and leaves the normalizer
   in place as a backstop. Exposed as
   ``pin_httpx_multipart_boundary`` so other multipart-heavy suites
   (audio, ocr, batches) can adopt the same fixture later.

2. Pass raw ``bytes`` (not ``BytesIO`` streams) through the
   image-edit fixtures. A ``BytesIO`` whose file pointer is at EOF
   after the first multipart upload silently encodes an empty image on
   the next SDK / Router retry — yet another divergent body that VCR
   records as a new episode. ``bytes`` are immutable and position-less,
   so retries re-encode an identical payload every time. This is also
   a small production-correctness improvement: a customer passing
   ``BytesIO`` today would hit the same empty-body retry bug. The
   BytesIO-specific smoke test
   (``test_openai_image_edit_with_bytesio``) is preserved by giving
   ``get_test_images_as_bytesio`` its own factory instead of aliasing
   the bytes one.

3. Add ``scripts/flush_image_edit_vcr_cassettes.py`` — a one-shot
   Redis SCAN/DEL helper that clears the bloated pre-fix cassettes
   under ``litellm:vcr:cassette:tests/image_gen_tests/test_image_edits/*``.
   Without this, the next CI run still loads the existing 51-entry
   cassette, the new fixed-boundary body still doesn't match any of
   the stale entries, the persister still refuses to save, and the
   bleed continues. Run once with the production
   ``CASSETTE_REDIS_URL`` after merge (dry-run by default).

* DIAGNOSTIC: log VCR body mismatches + per-episode body hashes

Temporary observability boost so we can root-cause why
``test_image_edits.py`` async parametrizes still record fresh
episodes on every CI run even though the multipart boundary is now
pinned (sync parametrizes cache cleanly as VCR HIT). The matcher
currently raises ``AssertionError("request bodies differ")`` with
zero context, so we cannot tell whether the live body genuinely
varies, the matcher is comparing a bytes object to a stream object,
or the normalizer is silently skipping the body because it is not
bytes/str.

Three logs added; the first two are worth keeping permanently, the
third is intended to be reverted after the diagnosis lands:

1. ``_safe_body_matcher`` now emits a structured stderr block on
   mismatch (type of each side, length, SHA-256, first divergent
   byte offset, ±100-byte window). Always-on -- mismatches are
   signal, not noise, and the existing per-test verdict already
   logs once per test. PERMANENT.

2. ``_normalize_multipart_boundary`` now logs to stderr when the
   body type is not bytes/bytearray/str -- the silent ``else:
   return`` branch was masking exactly the case we suspect is
   firing on async (httpx ``MultipartStream`` handed to vcrpy
   before the body is read). PERMANENT.

3. ``_RedisPersister.save_cassette`` now logs every episode's body
   SHA-256, length, and 120-byte preview at save time. This lets
   two consecutive CI runs be diffed: if the same test records a
   different hash run-to-run, the live body genuinely varies; if
   both runs record the same hash but the matcher still misses, the
   bug is in the matcher itself. TEMPORARY -- revert once the
   async variance is identified and fixed.

Once a single ``image_gen_testing`` CI run produces these logs,
revert this commit (or just the persister hash block) with a force
push so the cassette save path is not noisy in steady-state.

* DIAGNOSTIC: route VCR diagnostics through per-PID files (bypass xdist capture)

Re-push of the diagnostic logging from the previous commit, this
time wired so the output actually survives to the CI log. xdist
captures stdout/stderr from every passing test in the worker
process; the body-matcher and normalizer-skip diagnostics fire from
inside vcrpy machinery during the test, so for any test that
ultimately passes (which is all of them once the cassettes are
recorded), the diagnostic lines are silently swallowed.

Fix: write each diagnostic line to a per-PID file under
``test-results/vcr-diagnostics/<pid>.log`` instead of writing to
stderr. The controller's ``pytest_terminal_summary`` aggregates
those files and writes them through ``terminalreporter.write_line``,
which is not subject to per-test capture. As a bonus,
``test-results/`` is already collected by the ``store_test_results``
step in CircleCI, so the raw per-worker logs survive as build
artifacts even after the test session ends.

Three call sites updated:

1. ``_emit_body_mismatch_diagnostic`` (matcher) -- writes the
   structured type/length/sha/window block via ``vcr_diag_write_line``.
2. ``_normalize_multipart_boundary`` -- logs the silent-skip path
   (body not bytes/bytearray/str) the same way.
3. ``_maybe_log_episode_body_hashes`` (persister) -- replaces the
   ``_log.warning`` calls (which the root-logger config also
   swallows in CI) with ``vcr_diag_write_line``.

Image-gen conftest is the only suite wired to dump the aggregated
log at session end. Other suites can opt in by adding
``emit_vcr_diagnostic_log(terminalreporter)`` to their own
``pytest_terminal_summary``. The diagnostic dir is cleared at the
start of each session (controller-only) so a local rerun does not
mix output from prior runs.

Same revert plan as the previous diagnostic commit: keep the
matcher + normalizer skip diagnostics permanently (they only fire
on signal events), revert the persister body-hash dump once the
async variance is identified.

* fix(tests): coalesce iterable request bodies before matching/recording

Root cause of the residual async image-edit cassette leak. The
diagnostic run for ``ba3915d9`` printed:

  [vcr-safe-body-matcher] request body mismatch
    body[a]: type='list_iterator' length=unknown sha256=N/A
    body[b]: type='list_iterator' length=unknown sha256=N/A

httpx's async transport hands vcrpy a ``request.body`` that is a
``list_iterator`` over multipart chunks rather than a contiguous
``bytes`` blob. Two consequences:

1. ``_safe_body_matcher`` compares the two iterator objects with
   ``==``, which is identity comparison for arbitrary iterators -
   semantically identical multipart bodies never compare equal, and
   ``record_mode="new_episodes"`` appends a new episode on every CI
   run until the cassette crosses ``MAX_EPISODES_PER_CASSETTE`` and
   the persister refuses to save (this is exactly what the OVERFLOW
   warning has been catching).
2. ``_normalize_multipart_boundary`` short-circuits its
   ``else: return`` branch because the body is neither bytes nor
   str, so any residual random boundary characters in the body bytes
   are never rewritten.

Sync requests do not hit this code path: httpx's sync transport
hands vcrpy a single ``bytes`` body, so ``==`` works and the
boundary normalizer runs as intended. That is why
``test_openai_image_edit_litellm_sdk[True]`` records to ``entries=1``
and replays cleanly while ``[False]`` (async) kept growing by one
episode per run.

Fix: add ``_materialize_iterable_body`` which coalesces an iterable
``request.body`` into ``bytes`` in-place. Call it from two places:

* The top of ``_before_record_request``, so the boundary normalizer
  and the cassette serializer both see bytes from then on.
* The top of ``_safe_body_matcher``, as defense in depth in case a
  future vcrpy code path invokes the matcher without first going
  through ``_before_record_request``.

The vcrpy ``Request`` is a wrapper used for matching and recording;
the underlying httpx transport sends its own request body
separately, so replacing the iterator on the vcrpy wrapper does
not starve the live HTTP send.

After this lands the async parametrizes should flip from
``[VCR MISS:RECORDED] entries=N+1`` to ``[VCR HIT] entries=N`` on
the next CI run, matching the sync side and dropping the residual
~$3/day to $0.

* fix(tests): handle bytes_iterator + never leave an exhausted body

Follow-up to 8e08272b. The previous attempt at coalescing iterable
request bodies bailed out (``return`` without writing
``request.body``) whenever it could not classify the chunk type.
That was the wrong failure mode for one critical case: vcrpy
sometimes presents the body as ``iter(some_bytes)``, whose Python
type is ``bytes_iterator`` and which yields ``int`` byte values
(0-255), not byte chunks. The old code saw an ``int`` chunk, hit
the ``else: return`` branch, and left ``request.body`` pointing at
the now-exhausted iterator.

The post-fix diagnostic run made this loud:

  [vcr-safe-body-matcher] request body mismatch
    body[a]: type='bytes_iterator' length=unknown sha256=N/A
    body[b]: type='bytes_iterator' length=unknown sha256=N/A

Every async image-edit test then ballooned from entries=2 to
entries=10 in that single CI run -- the exhausted iterator meant
the live multipart upload went out as an empty body, OpenAI
returned 400, the SDK + flaky retries fired, each retry got a
fresh iterator that my hook exhausted again, and ``new_episodes``
recorded each failed attempt as a new cassette episode.

This patch:

* Recognizes ``bytes_iterator`` (chunks are ``int``) and
  reconstructs the buffer via ``bytes(chunks)``.
* Keeps the existing ``list_iterator``-over-bytes-chunks handling
  via ``b"".join(...)``.
* **Always writes a bytes value back to ``request.body`` after
  consuming the iterator.** If the chunk shape is unrecognized,
  ``request.body`` is set to ``b""`` rather than left as an
  exhausted iterator. That is wrong in the sense of "we lost the
  body" but right in the sense of "the failure mode is now visible
  (live API call sends empty body and fails fast) instead of
  invisible (corrupt cassette grows silently)". Combined with the
  matcher diagnostic, any future regression in this code path will
  surface in the CI log immediately.

Local verification covers ``bytes_iterator``, ``list_iterator``
over bytes chunks, generator over bytes chunks, empty iterator,
already-bytes (idempotent), identical-content iterator equality
in the matcher (now matches), and differing-content iterator
inequality (still raises).

* fix(tests): clear vcrpy's sticky _was_iter flag so materialized bodies stay bytes

Actual root cause of the async image-edit cassette leak. The
previous diagnostic run produced this dead giveaway:

  [vcr-episode-body-hash] ... episode[0]: body type='bytes_iterator'
    is not bytes/bytearray/str -- cannot hash
  [vcr-safe-body-matcher] request body mismatch
    body[a]: type='bytes_iterator' length=unknown sha256=N/A
    body[b]: type='bytes_iterator' length=unknown sha256=N/A

Both sides of the matcher were ``bytes_iterator`` **after** the
materializer had supposedly converted them to bytes. That made no
sense until I read vcrpy's ``Request`` class.

vcrpy's ``Request`` keeps two private flags that are set in
``__init__`` from the original body's type and **never cleared by
the setter**:

  def __init__(self, method, uri, body, headers):
      self._was_file = hasattr(body, "read")
      self._was_iter = _is_nonsequence_iterator(body)
      ...

  @property
  def body(self):
      if self._was_file: return BytesIO(self._body)
      if self._was_iter: return iter(self._body)
      return self._body

  @body.setter
  def body(self, value):
      if isinstance(value, str): value = value.encode("utf-8")
      self._body = value   # <-- does NOT touch _was_iter / _was_file

So when httpx's async transport hands vcrpy an iterator body,
``_was_iter`` becomes ``True`` and stays there forever. Even after
``_materialize_iterable_body`` writes plain bytes via
``request.body = out``, the next read of ``.body`` re-wraps the
stored bytes in ``iter()`` -- producing a fresh ``bytes_iterator``
that compares unequal to any other ``bytes_iterator`` via object
identity. The matcher missed every time, the cassette grew by one
episode per run, and the persister saw the same iterator type when
trying to hash the body for the diagnostic log.

Fix: after writing the materialized bytes, also force
``_was_iter`` and ``_was_file`` to ``False``. vcrpy exposes no
public API for this, so we touch the private flags directly --
acknowledged as a pragmatic test-only hack with a clear unit
boundary (the only call site is ``_materialize_iterable_body``).

Local repro reproduces the exact production setup:
``Request('POST', url, iter(b'multipart-content'), {})`` on two
sides, runs the matcher, asserts HIT. Verified the matcher hits on
identical content and still raises on differing content.

Should be the last fix needed. Existing cassettes that contain
oddly-shaped bodies (lists of int chunks, etc. from the previous
``_was_iter=True`` save path) still match because the materializer
canonicalises both sides to bytes before comparison -- no fourth
re-flush required.

* revert(tests): drop the temp per-episode body-hash diagnostic

Removed now that 1c51ad13 has confirmed the root cause (vcrpy's
sticky ``_was_iter`` flag making the body getter re-wrap stored
bytes in ``iter()`` on every access). The hash dump did its job --
the post-1c51ad13 image_gen_testing run shows all five async
image-edit tests as ``[VCR HIT]`` with stable entry counts and
zero billing errors -- and is too noisy to keep on by default
(over 100 lines per session at steady state).

Kept permanently:

* ``_safe_body_matcher`` mismatch diagnostic in
  ``_vcr_conftest_common.py``. Only fires on a body mismatch,
  which is signal worth surfacing whenever it happens.
* ``_normalize_multipart_boundary`` "skipped" log line. Same
  rationale -- only fires when the body shape is something the
  normalizer cannot rewrite in place.
* The ``test-results/vcr-diagnostics/<pid>.log`` per-PID file
  plumbing (``vcr_diag_write_line`` /
  ``emit_vcr_diagnostic_log``). Useful for any future diagnostic
  that needs to bypass xdist stdout/stderr capture; cheap to keep.

* chore(tests): delete unused flush script + wire VCR diagnostic dump everywhere

* Remove ``scripts/flush_image_edit_vcr_cassettes.py``. It was a
  one-shot helper for the initial cassette flush; the iterator and
  ``_was_iter`` fixes mean no future flush should be required, and
  the script was never run anywhere (the actual flushes happened
  inside the CI conftest via the temp hacks that have since been
  reverted).

* The matcher mismatch + normalizer skip diagnostics already write
  per-PID files for every suite that imports the shared VCR
  plumbing, but ``emit_vcr_diagnostic_log`` -- the controller-side
  dump that surfaces those files into the CI log at session end --
  was only wired into ``image_gen_tests``. Add the one-line call to
  the 12 sibling conftests that already use VCR so the diagnostics
  surface in any suite's terminal output if a body matcher ever
  misses. No new output in steady state -- the dump is a no-op when
  no diagnostics were recorded that session.

* chore(tests): trim non-essential comments per project comment policy

Strips docstrings, inline comments, and block comments that this PR
introduced where the code itself was already self-evident. Keeps the
few lines that document non-obvious behaviour (raw-bytes-not-BytesIO
rationale on the image fixtures, the per-PID-files-bypass-xdist note
on the diagnostic directory). Touches only comments this PR added --
no pre-existing comment is removed.

Net: -161 lines of comment/docstring across 3 files, no code
behaviour change.

* chore(tests): forward **kwargs in pin_httpx_multipart_boundary wrapper

Defensive against future httpx MultipartStream.__init__ adding new
optional kwargs. Without the forward, the wrapper would silently drop
them. No behaviour change today.

* chore(tests): canonicalize VCR matchers and surface shouldn't-happen branches

Bundles the "follow-up cleanup PR" into this one so it does not get
lost. Four small changes:

1. Introduce ``_canonical_body(req) -> (bytes, pre_type)`` and route
   ``_safe_body_matcher`` through it. The matcher now operates on
   bytes by construction; the "compare two iterator objects via
   ``==`` and silently get object-identity semantics" failure mode
   (which cost us this entire PR to diagnose) is structurally
   impossible to reintroduce. ``pre_type`` is the body type *before*
   canonicalization, surfaced by the mismatch diagnostic so a future
   regression involving a new body shape is still visible.

2. Add a structured diagnostic to ``_key_fingerprint_matcher``. It
   was previously raising a bare ``AssertionError("API key
   fingerprints differ")`` with zero context -- exactly the
   anti-pattern the body matcher had before this PR.

3. Surface "shouldn't-happen" branches via ``vcr_diag_write_line``:

   * ``_strip_image_b64_payloads`` -- logs when ``response``,
     ``response['body']``, or ``response['body']['string']`` arrives
     in an unexpected shape (vcrpy contract violation).
   * ``_compute_key_fingerprint`` -- logs the ``"no-key"`` fallback
     with the request method/URL so a stripped-auth-header bug is
     visible instead of masked.
   * ``_canonical_body`` -- logs its own empty-bytes fallback when a
     body has a shape ``_materialize_iterable_body`` did not handle.

4. Re-introduce per-episode body-hash logging in
   ``_RedisPersister.save_cassette`` (was reverted in 927c5548 as
   "noisy"). Quantified cost: ~25 KB of CI log per session at peak,
   ~ms-scale CPU, zero output in steady state (no save = no log).
   Trade-off favours keeping it: lets two consecutive CI runs be
   diffed by body hash, which is how we will spot the next regression
   in the same class.

All call sites still work: local repro confirms iter==iter HIT,
iter!=iter raises, plain-bytes HIT, body-hash log emits via the same
per-PID file plumbing as the matcher diagnostics.

* chore(tests): symmetrize diag-log cleanup across every VCR-using conftest

``image_gen_tests/conftest.py`` was the only suite that cleared
``test-results/vcr-diagnostics/*.log`` at session start. The other 12
VCR-using conftests inherited any stale per-PID logs from a previous
local run and would dump them in the terminal summary -- harmless in
CI (fresh container) but confusing locally when running multiple
suites in sequence.

Extracts the cleanup into a ``reset_vcr_diag_dir`` helper in
``tests/_vcr_conftest_common.py`` and calls it from every VCR-using
conftest's ``pytest_configure``. Same single source of truth, no
inline duplication.

* fix(tests): gate body materialization on __next__ and strip PR comments

aiohttp/vcrpy stores the json kwarg as a dict; _materialize_iterable_body
was iterating it via __iter__ and joining the keys, replacing the request
body with concatenated key names ("textlanguageentities"). Gate on
__next__ so containers (dict/list/tuple) are left alone — only single-use
iterators like httpx's bytes_iterator / list_iterator are materialized.
Log diagnostic line when chunk type is unrecognized.

* fix(tests): JSON-encode dict bodies in canonical_body for stable matching

aiohttp stubs store the json kwarg as a dict; the fallback that compared
all dicts as b"" caused concurrent presidio analyze calls to be served
the wrong cassette episode. JSON-encode with sort_keys for stable bytes.

* fix(tests): guard emit_vcr_diagnostic_log against multi-conftest re-emission

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* fix(tests): globalize multipart-boundary pin + stabilize whisper fixtures

Diagnostic shows audio_testing was silently re-recording 50+ live Whisper
episodes per CI run (over MAX_EPISODES_PER_CASSETTE, so the persister
refused to save). Two changes:

* Move the session-autouse _pin_multipart_boundary fixture into the
  shared _vcr_conftest_common module so every VCR-using suite picks it
  up via a single import. image_gen had it inline; the other 12 suites
  silently lacked it.
* Replace the module-level open("rb") audio file handles in test_whisper
  with cached bytes + a per-call (filename, bytes, mimetype) tuple,
  mirroring the image_edits raw-bytes pattern. Stops the file-pointer-
  at-EOF bug where the second test got an empty multipart body.

* chore(tests): drop per-episode body-hash dump and redundant emit guard

---------

Co-authored-by: shin-berri <shin-laptop@berri.ai>
Co-authored-by: yuneng-jiang <yuneng@berri.ai>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Yassin Kortam <yassin@berri.ai>
2026-05-18 09:15:39 -07:00
Mateo Wang
2c733c00f5
chore(ci): modernize model references in tests and configs (#27856)
* test: modernize models used in CircleCI e2e test suites

Replaces obsolete models (gpt-4o, gpt-4o-mini, gpt-3.5-turbo,
claude-3-5-sonnet-20240620, claude-sonnet-4-20250514) with current
equivalents across the e2e_openai_endpoints and
proxy_e2e_anthropic_messages_tests CircleCI jobs.

- gpt-4o -> gpt-5.5 (responses API e2e tests)
- gpt-4o-mini -> gpt-5-mini (websocket responses, oai_misc_config)
- gpt-4o-mini-2024-07-18 -> gpt-4.1-mini-2025-04-14 (fine-tuning,
  still actively fine-tunable)
- gpt-4 / gpt-3.5-turbo target_model_names example -> gpt-5.5 /
  gpt-5-mini
- bedrock claude-3-5-sonnet-20240620 batch entry -> haiku-4-5-20251001
  (also aligning oai_misc_config model_name with what
  test_bedrock_batches_api.py actually requests)
- bedrock claude-sonnet-4-20250514 (deprecated, retires 2026-06-15)
  -> claude-sonnet-4-5-20250929

* test: point bedrock-claude-sonnet-4 alias at Sonnet 4.6, not 4.5

Greptile/Cursor flagged that after the previous commit, the
bedrock-claude-sonnet-4 alias collided with bedrock-claude-sonnet-4.5
(both pointed to claude-sonnet-4-5-20250929). Rename to
bedrock-claude-sonnet-4.6 and point it at the Sonnet 4.6 Bedrock ID
(us.anthropic.claude-sonnet-4-6, already in the litellm model
registry) so the alias name matches the underlying model version.

* test: modernize models across remaining CI-mounted configs & tests

Expands the modernization sweep to all CircleCI-mounted proxy configs
and to test directories where the model literal is a fixture/route key
(not the test's subject).

Config changes:
- proxy_server_config.yaml: bump gpt-3.5-turbo / gpt-3.5-turbo-1106 /
  gpt-4o / gemini-1.5-flash / dall-e-3 underlying models; rename
  gpt-3.5-turbo-end-user-test alias to gpt-5-mini-end-user-test; bump
  text-embedding-ada-002 underlying to text-embedding-3-small. User-
  facing aliases (gpt-3.5-turbo, gpt-4, text-embedding-ada-002, etc.)
  preserved for backward compatibility with tests.
- simple_config.yaml, otel_test_config.yaml, spend_tracking_config.yaml:
  bump gpt-3.5-turbo underlying to gpt-5-mini.
- pass_through_config.yaml: claude-3-5-sonnet / claude-3-7-sonnet /
  claude-3-haiku entries replaced with claude-sonnet-4-5 / claude-
  haiku-4-5 / claude-opus-4-7.
- oai_misc_config.yaml: align alias name with the gpt-5-mini rename.

Test changes (proactive: claude-sonnet-4-20250514 / claude-opus-4-
20250514 retire 2026-06-15):
- tests/llm_translation/test_anthropic_completion.py: bump 3 references
  + paired Vertex AI ID to claude-sonnet-4-5.
- tests/llm_translation/test_optional_params.py: bump 2 references.
- tests/pass_through_unit_tests/test_anthropic_messages_passthrough.py
  and test_bedrock_anthropic_messages_test.py: bump router fixtures
  using the deprecated model IDs.
- tests/pass_through_unit_tests/base_anthropic_messages_tool_search_test.py:
  modernize docstring examples.
- tests/test_end_users.py: update references to renamed alias.

* test: modernize placeholder model literals in router_unit_tests

Mass replace_all on fixture/placeholder model literals across the
router_unit_tests/ suite (model name is a routing key / label, not the
test subject). Sub-agent sweep so far — additional commits will follow
for logging_callback_tests/, enterprise/, top-level tests/test_*.py,
and other CI-mounted dirs.

Mappings applied:
- gpt-3.5-turbo -> gpt-5-mini
- gpt-4 (bare) -> gpt-5.5
- gpt-4o (bare) -> gpt-5
- text-embedding-ada-002 -> text-embedding-3-small
- claude-3-sonnet-20240229 / claude-3-opus-20240229 /
  claude-3-haiku-20240307 / claude-3-5-sonnet-20240620 ->
  claude-sonnet-4-5-20250929 / claude-opus-4-7 /
  claude-haiku-4-5-20251001 as appropriate

Explicitly preserved:
- gpt-4o-mini-* variants (transcribe, tts, etc.) where they're current
- gpt-4-turbo / gpt-4-vision-preview / gpt-4-0613 (subject literals)
- JSONL batch body literals
- Mock LLM response model fields (must match upstream)
- Fake/mock identifiers

* test: modernize placeholder model literals across remaining CI suites

Sub-agent sweep across logging_callback_tests/, guardrails_tests/,
enterprise/, pass_through_unit_tests/, otel_tests/,
llm_responses_api_testing/, batches_tests/, spend_tracking_tests/,
litellm_utils_tests/, unified_google_tests/, and a few top-level
tests/test_*.py files where the model literal is a fixture or
placeholder (router model_list, mock standard logging payload, mock
callback data) rather than the test's subject.

Mappings applied (see scope notes below):
- gpt-3.5-turbo -> gpt-5-mini
- gpt-4 (bare) -> gpt-5.5
- gpt-4o (bare) -> gpt-5.5 (corrected from initial gpt-5 — bare gpt-5
  is not a valid OpenAI alias; only gpt-5.5 / gpt-5.4 / gpt-5.2-codex
  / gpt-5-mini exist)
- gpt-4o-mini (bare) -> gpt-5-mini
- text-embedding-ada-002 -> text-embedding-3-small
- claude-3-sonnet-20240229 -> claude-sonnet-4-5-20250929
- claude-3-opus-20240229 -> claude-opus-4-7
- claude-3-haiku-20240307 -> claude-haiku-4-5-20251001
- claude-3-5-sonnet-20240620/20241022 -> claude-sonnet-4-5-20250929
- claude-3-7-sonnet-20250219 -> claude-sonnet-4-6
- gemini-1.5-flash -> gemini-2.5-flash
- gemini-1.5-pro -> gemini-2.5-pro

Explicitly preserved (not modernized):
- llm_translation/ tests where model is the SUBJECT (provider-specific
  translation/transformation logic). Only the deprecated 20250514
  references were already bumped in a prior commit.
- Cost-calc / tokenizer subject tests in test_utils.py (skip-ranges
  documented by the sub-agent).
- Bedrock model IDs in test_health_check.py path-stripping tests.
- JSONL batch request bodies and mock LLM response bodies (must match
  upstream literal).
- Langfuse expected-request-body JSON fixtures (cost values are exact-
  match-asserted; changing the model would shift response_cost).
- gpt-3.5-turbo-instruct (text-completion endpoint; no modern OpenAI
  equivalent).
- Top-level tests calling the proxy through user-facing aliases
  (gpt-3.5-turbo, gpt-4, text-embedding-ada-002, dall-e-3) — aliases
  in proxy_server_config.yaml stay; only the underlying model was
  bumped.
- tests/test_gpt5_azure_temperature_support.py (the test's whole point
  is model-name handling).
- Fake / mock / openai/fake identifiers.

Notable side fixes:
- test_spend_accuracy_tests.py: UPSTREAM_MODEL now matches what
  spend_tracking_config.yaml's proxy actually routes to (gpt-5-mini),
  resolving a latent inconsistency.
- proxy_server_config.yaml: bare `gpt-5` alias renamed to `gpt-5.5`
  (bare gpt-5 is not a valid OpenAI alias).
- test_batches_logging_unit_tests.py: explicit_models list entries
  kept distinct (gpt-5-mini + gpt-5.5) after bulk rename.

* test: fix CI failures from model modernization sweep

CI surfaced 4 categories of regression from the bulk modernization:

1. Azure deployment names are customer-specific. Reverted:
   - tests/litellm_utils_tests/test_health_check.py: azure/text-
     embedding-3-small -> azure/text-embedding-ada-002 (the CI Azure
     account does not have a text-embedding-3-small deployment).
   - tests/logging_callback_tests/test_custom_callback_router.py:
     same revert for two router fixtures driving aembedding.

2. gpt-5 family does not accept temperature != 1. Tests that pass a
   custom temperature swapped from gpt-5-mini to gpt-4.1-mini (modern
   non-reasoning OpenAI mini that still accepts temperature/logprobs):
   - tests/logging_callback_tests/test_datadog.py
   - tests/logging_callback_tests/test_langsmith_unit_test.py
   - tests/logging_callback_tests/test_otel_logging.py

3. proxy_server_config.yaml's gpt-3.5-turbo-large alias was routing to
   gpt-5.5 (a reasoning model that rejects logprobs). The proxy test
   tests/test_openai_endpoints.py::test_chat_completion_streaming
   exercises logprobs/top_logprobs through that alias. Bumped the
   underlying model to gpt-4.1 (non-reasoning, still modern).

4. tests/logging_callback_tests/test_gcs_pub_sub.py asserts against a
   pinned JSON fixture (gcs_pub_sub_body/spend_logs_payload.json) with
   hardcoded model="gpt-4o" and a model-specific spend value. Reverted
   the litellm.acompletion calls in the test to model="gpt-4o" so the
   fixture's exact-match assertions still hold.

5. tests/pass_through_unit_tests/test_anthropic_messages_passthrough.py:
   anthropic.messages.create routing to openai/gpt-5-mini returned an
   empty content[0] with max_tokens=100 (reasoning-token consumption).
   Swapped to openai/gpt-4.1-mini.

* test: fix Assistants API model + 2 cursor[bot] review nits

1. pass_through_unit_tests/test_custom_logger_passthrough.py: gpt-5.5
   isn't accepted by the /v1/assistants endpoint
   ("unsupported_model"). Switch to gpt-4.1-mini (modern, Assistants-
   API-supported, non-reasoning).

2. example_config_yaml/pass_through_config.yaml: the previous sweep
   bumped the claude-3-7-sonnet alias to claude-opus-4-7, which is a
   tier change (Sonnet -> Opus). Map to claude-sonnet-4-6 to keep the
   Sonnet tier intact. (Cursor bugbot review.)

3. example_config_yaml/simple_config.yaml: model_name was left as
   gpt-3.5-turbo while the underlying was bumped to gpt-5-mini, which
   muddles the "simple" example. Make both sides gpt-5-mini so the
   most basic example is a straight 1:1 mapping again. (Cursor bugbot
   review.)

* fix: revert gpt-4/gpt-3.5-turbo alias underlying to non-reasoning models

tests/test_openai_endpoints.py::test_completion calls the proxy alias
"gpt-4" with temperature=0, and other tests call gpt-3.5-turbo with
custom temperature / logprobs / the legacy /v1/completions endpoint.
The earlier modernization mapped both aliases to gpt-5.5 / gpt-5-mini,
which are reasoning models that reject temperature != 1 and don't
expose /v1/completions. Map the aliases to gpt-4.1 / gpt-4.1-mini
(modern non-reasoning OpenAI models) instead — keeps user-facing
aliases preserved while picking a current underlying that still
supports the parameters/endpoints the tests exercise.
2026-05-15 15:44:28 -07:00
Cursor Agent
b637d9f64a
test(vcr): classify cache verdicts, detect live calls, surface cost leaks
Convert the per-test VCR verdict line from a single 'NOOP / HIT / MISS /
PARTIAL' tag into a classified outcome that distinguishes the cases that
silently bill the live API on every CI run from the ones that don't:

  HIT                         pure replay
  PARTIAL                     mixed replay + new recordings
  MISS:RECORDED               new cassette saved to Redis (cached next run)
  MISS:OVERFLOW               cassette > MAX_EPISODES_PER_CASSETTE; persister
                              refused to save; re-bills every run
  MISS:NOT_PERSISTED          test failed; save_cassette skipped; re-bills
  NOOP                        VCR-marked but no HTTP traffic (mocked elsewhere)
  UNMARKED:LIVE_CALL          test bypassed VCR AND opened a TCP connection
                              to a known LLM provider host -> wasted spend
  UNMARKED:NO_TRAFFIC         test bypassed VCR but didn't call out

The UNMARKED:LIVE_CALL signal is what converts 'this test probably hits
live' into 'this test connected to api.openai.com'. We install a
socket.connect / socket.create_connection wrapper for the duration of
each non-VCR-marked test and record any outbound TCP to a known LLM
provider hostname. The probe sits below the httpx layer so vcrpy and
respx (which both patch above the socket) are unaffected.

Replace the file-level _RESPX_CONFLICTING_FILES blacklists in the
llm_translation and local_testing conftests with per-item respx
detection in apply_vcr_auto_marker_to_items. A test now skips VCR when
it actually carries @pytest.mark.respx or has respx_mock in its fixture
chain - not just because some other test in the same file imports
MockRouter. Items skipped by skip_files are split into respx_conflict
(real conflict, the module wires up respx) vs file_opt_out (dead skip-
list entry whose module never touches respx) so the session summary
makes pruning obvious.

Stabilize the AWS SigV4 fingerprint: the Authorization header on
Bedrock requests rotates its Credential date and Signature on every
call, which previously pushed every Bedrock test past the 50-episode
overflow threshold. Extract the access-key id only
('aws-sigv4:AKIA...') so two requests with the same identity match.

Always emit verdict logging when VCR is active (set
LITELLM_VCR_VERBOSE=0 to opt back into the legacy quiet mode). Add a
session-end classification summary that lists overflow tests, unmarked
live-call tests, and the skip-reason breakdown.

Wire the live-call probe + summary hook into every test directory that
already uses the Redis-backed VCR cache (audio_tests, guardrails_tests,
image_gen_tests, litellm_utils_tests, llm_responses_api_testing,
llm_translation, local_testing, logging_callback_tests, ocr_tests,
pass_through_unit_tests, router_unit_tests, search_tests,
unified_google_tests).

Add tests/llm_translation/test_vcr_classification.py covering the
verdict classifier, skip-reason tagging, AWS SigV4 fingerprint stability,
live-host classification, and session summary rendering.

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>
2026-05-13 00:31:47 +00:00
Mateo Wang
7e13256fee
test: add 24hr Redis-backed VCR cache to additional test suites (#27159)
* test: add 24hr Redis-backed VCR cache to additional test suites

Extracts the existing llm_translation VCR plumbing into a reusable helper
(tests/_vcr_conftest_common.py) and wires it into the conftest.py files
of the test directories listed in LIT-2787:

  audio_tests, batches_tests, guardrails_tests, image_gen_tests,
  litellm_utils_tests, local_testing, logging_callback_tests,
  pass_through_unit_tests, router_unit_tests, unified_google_tests

The same helper is also adopted by the pre-existing llm_translation and
llm_responses_api_testing conftests to remove the copy-pasted VCR setup.

Each consuming conftest:
- registers the Redis persister via pytest_recording_configure
- auto-marks collected tests with pytest.mark.vcr (skipping respx-using
  files where applicable, since respx and vcrpy both patch httpx)
- gates cassette writes on test success via _vcr_outcome_gate

The cache is opt-in via CASSETTE_REDIS_URL; when unset, VCR is disabled
and tests hit live providers as before. LITELLM_VCR_DISABLE=1 still
forces a bypass for ad-hoc local runs.

Test directories that run LiteLLM proxy in Docker (build_and_test,
proxy_logging_guardrails_model_info_tests, proxy_store_model_in_db_tests)
are intentionally not included: VCR.py patches the in-process httpx
transport and cannot intercept calls made from inside a Docker container.
The installing_litellm_on_python* jobs make no LLM calls and don't
benefit from caching.

https://linear.app/litellm-ai/issue/LIT-2787/add-24hr-caching-to-additional-test-suites

* test(vcr): add safe-body matcher to handle JSONL and binary request bodies

vcrpy's stock body matcher inspects Content-Type and unconditionally
runs json.loads on application/json bodies. JSON Lines payloads (used
by the Bedrock batch S3 PUT and other upload paths) crash that with
json.JSONDecodeError: Extra data, before the matcher can return
'not a match'.

This was the root cause of the batches_testing CI job failing on
test_async_create_file once VCR auto-marking was applied to the
batches_tests directory.

Add a conservative byte-equality body matcher and use it in place of
'body' in the shared match_on tuple. The matcher is strictly more
conservative than vcrpy's default — the only thing it gives up is
'different JSON key order is treated as the same body', which doesn't
apply to deterministic litellm-built request payloads. It can never
produce a false positive that the default would have rejected, so
there is no cross-contamination risk.

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

* test(vcr): exclude tests that VCR replay actively breaks

A few tests are incompatible with cassette replay and were failing on
the latest CI run after VCR auto-marking was extended to local_testing
and logging_callback_tests:

- test_amazing_s3_logs.py (logging_callback_tests): the test asserts on
  a per-run response_id that should round-trip through a real S3
  PUT/LIST. vcrpy's boto3 stub intercepts the PUT and the LIST replays
  stale keys, so the freshly-generated id is never found.
- test_async_embedding_azure (logging_callback_tests) and
  test_amazing_sync_embedding (local_testing): the failure branches
  deliberately pass api_key='my-bad-key' to assert that the failure
  callback fires. We scrub auth headers from cassettes (so the bad-key
  request matches the prior good-key request), and vcrpy replays the
  recorded 200 — the failure callback never fires.
- test_assistants.py (local_testing): the OpenAI Assistants polling
  APIs mint fresh thread/run IDs every recording session and then poll
  until status=='completed'. Replays of those polled GETs can never
  match a freshly-generated run id, so every CI run effectively
  re-records and the suite blows past the 15m no_output_timeout.

Skip these from VCR auto-marking so they continue to hit live providers
as they did before this change. The remaining tests in each directory
still get cached.

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

* test(vcr): expand skip lists for second batch of incompatible tests

Followup to the previous commit. After re-running CI on the rebuilt
branch, three more tests surfaced as VCR-replay-incompatible:

- litellm_utils_testing :: test_get_valid_models_from_dynamic_api_key
  Calls GET /v1/models with api_key='123' to assert the result is empty.
  We scrub auth headers, so the bad-key request matches the prior
  good-key cassette and replays the recorded model list.
- litellm_utils_testing :: test_litellm_overhead.py
  Measures litellm_overhead_time_ms as a percentage of total wall-clock
  time. With cached responses the upstream 'network' time collapses to
  microseconds, blowing past the 40%% threshold the test asserts on.
  Skip the whole file (every parametrization is at risk).
- local_testing_part1 :: test_async_custom_handler_completion and
  test_async_custom_handler_embedding
  Same bad-key failure-callback pattern as the already-skipped
  test_amazing_sync_embedding.
- litellm_router_testing :: test_router_caching.py
  Asserts on litellm's own router-level response cache by comparing
  response1.id to response2.id across repeat upstream calls (test
  bypasses litellm cache via ttl=0 and expects upstream to return a
  *new* id). With VCR replay both upstream calls return the same
  cassette body, so the ids are identical. Skip the whole file.
- logging_callback_tests :: test_async_chat_azure (preemptive)
  Same shape as already-skipped test_async_embedding_azure; was masked
  by upstream OpenAI rate-limit failures on baseline.

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

* test(vcr): use item.path and tighten matcher docstring

- Replace pytest's deprecated item.fspath with item.path in
  apply_vcr_auto_marker_to_items so we don't emit deprecation
  warnings under pytest 8.
- Clarify _safe_body_matcher docstring to reflect actual behavior
  (direct == first, then UTF-8 bytes comparison, no repr fallback).

Addresses Greptile review feedback on PR #27159.

* test(vcr): swallow all RedisError on cassette save/load

Cassette persistence is strictly best-effort: any Redis-side failure
(connection blip, timeout, OutOfMemoryError when the maxmemory cap is
hit, READONLY replicas, etc.) should degrade to 'test passed but
cassette not cached' rather than fail the test on teardown.

Previously the persister only caught ConnectionError and TimeoutError,
so OutOfMemoryError — which Redis Cloud raises when the cassette cache
hits its memory cap and there are no evictable keys — propagated out of
vcrpy's autouse fixture and ERRORed otherwise-passing tests on
teardown. This caused the litellm_utils_testing CircleCI job to fail on
the latest commit's run, even though the underlying test was a unit
test that used mock_response and produced no real upstream traffic
(the cassette was dirtied by a background langfuse callback). The
rerun only succeeded because Redis evictions happened to free enough
room before the SET — i.e. it was timing-dependent flakiness.

Catch redis.exceptions.RedisError (the common base of all server- and
client-side Redis exceptions) on both save and load, and parametrize
the regression tests across ConnectionError, TimeoutError, and
OutOfMemoryError to pin the new behavior.

* test(vcr): surface cassette-cache failures with warnings + session banner

When the persister silently swallows a Redis OOM (or any RedisError) on
save/load there is otherwise no visible signal that the cache is
degraded — tests pass, the cassette just isn't persisted, and the next
session still hits the same Redis at the same near-cap memory.

Add three layers of observability so that failure mode is loud:

1. Per-process health counters ("save_failures", "load_failures", and
   the last error string for each), exposed via cassette_cache_health()
   and reset via reset_cassette_cache_health(). The persister
   increments these in addition to logging.

2. VCRCassetteCacheWarning (UserWarning subclass) emitted via
   warnings.warn() inside the persister's except block. Pytest's
   built-in warnings summary at session end automatically lists every
   such warning, so the failure is visible in CI logs without any
   conftest-level wiring.

3. Session-end banner via emit_cassette_cache_session_banner() and a
   stderr-fallback atexit handler registered from
   register_persister_if_enabled(). Two states:
     - red "VCR CASSETTE CACHE DEGRADED" when save_failures or
       load_failures > 0
     - yellow "VCR CASSETTE CACHE NEAR CAPACITY" (no failures, but
       used_memory >= 85% of maxmemory) so the next session knows
       the Redis is approaching OOM before any SET actually fails

Capacity comes from a best-effort INFO memory probe
(cassette_cache_capacity_snapshot) that returns None on any failure or
when maxmemory is uncapped. The atexit handler skips xdist workers so
only the controller emits.

Tests: parametrize the existing save/load swallow-error tests across
ConnectionError/TimeoutError/OutOfMemoryError, add direct tests for
the health counters and warning emission, and a new
test_vcr_conftest_common_banner.py covering banner output for every
state (silent/red/yellow/disabled/xdist-worker).

* test(vcr): bucket cassettes by API key fingerprint, drop bad-key skips

Tests that deliberately call an LLM API with a bad key (e.g. to assert
that the failure callback fires, or that check_valid_key returns False)
were being silently served the prior good-key cassette: we scrub the
real Authorization / x-api-key header from the cassette before storing
it, so a follow-up bad-key call is byte-identical to the good-key call
under the existing match_on tuple.

Add a 'key_fingerprint' custom matcher that distinguishes requests by
the SHA-256 of their API-key headers. The fingerprint is stamped into
a synthetic 'x-litellm-key-fp' header by a new before_record_request
hook, which then strips the real auth headers (we have to do the
scrubbing here instead of via vcrpy's filter_headers knob, because
filter_headers runs *first* and would erase the value we want to hash).

Bad-key requests now get a different cassette bucket than good-key
requests, so vcrpy will not replay a recorded 200 in place of the
expected 401. The fingerprint is a one-way hash of the secret, so
cassettes never contain the key.

This permanently removes the 'bad-key' category of skips:

- tests/local_testing: dropped ::test_amazing_sync_embedding,
  ::test_async_custom_handler_completion,
  ::test_async_custom_handler_embedding
- tests/logging_callback_tests: dropped ::test_async_chat_azure,
  ::test_async_embedding_azure
- tests/litellm_utils_tests: dropped
  ::test_get_valid_models_from_dynamic_api_key

Coverage: 7 new unit tests in tests/test_litellm/test_vcr_safe_body_matcher.py
covering header stripping, fingerprint determinism, no-auth bucketing,
good-vs-bad key discrimination, x-api-key (Anthropic/Azure) discrimination,
and idempotence under replay.

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

* test(vcr): drop redundant comments and docstrings

Trim narration of code that is already self-evident from function and
variable names. Keep the two genuinely non-obvious bits:

- ordering constraint between filter_headers and before_record_request,
  which would invite a maintainer to re-introduce the bug if removed
- the per-directory _VCR_INCOMPATIBLE_FILES rationale, since 'why
  exactly is this skipped' is not knowable from the test name alone

Also drop the 40-line commented-out drop-in conftest snippet at the
bottom of _vcr_conftest_common.py — the consuming conftests are the
canonical reference.

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

* test(vcr): make _before_record_request idempotent

vcrpy invokes before_record_request more than once per request:
can_play_response_for calls it, then __contains__ /
_responses (reached via play_response) call it again on the
result. The second invocation sees a request whose auth headers we
already stripped, so a naive recompute yields "no-key" and
overwrites the real fingerprint stored in the header.

This makes can_play_response_for and play_response disagree on
matchability — the former says "yes, we have a stored response for
this" (matching no-key to no-key) and the latter throws
UnhandledHTTPRequestError because it computes a fresh real
fingerprint that doesn't match the stored no-key.

In CI this manifested as ~30 failing tests across guardrails_testing,
audio_testing, batches_testing, image_gen_testing, llm_responses_api,
litellm_router_unit_testing, etc. Skip the recompute when the header
is already set, so re-applying the hook is a no-op.

Adds a regression test that fires the hook twice on the same dict and
asserts the fingerprint stays put.

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

* test(vcr): drop more redundant docstrings and headers

* test(vcr): enable 24hr cache for ocr_tests and search_tests

These two directories were the only non-dockerized test suites in the
build_and_test workflow that make live LLM/provider API calls but were
not VCR-enabled by this PR. Together they account for 96 tests:

- tests/ocr_tests/ (31): Mistral OCR, Azure AI OCR, Azure Document
  Intelligence, Vertex AI OCR. Pure-unit tests inside the same files
  (e.g. TestAzureDocumentIntelligencePagesParam) make no HTTP calls
  and become benign VCR NOOPs.
- tests/search_tests/ (65): Brave, DataForSEO, DuckDuckGo, Exa,
  Firecrawl, Google PSE, Linkup, Parallel.ai, Perplexity, SearchAPI,
  Searxng, Serper, Tavily.

Both directories use the canonical minimal conftest pattern from
tests/audio_tests/conftest.py with no skip lists. None of the test
files use respx, none assert on per-call upstream non-determinism
(no response1.id != response2.id, no overhead-as-fraction-of-total,
no live polling), so the default match_on tuple should cache cleanly.
If a flake surfaces during the first cassette-recording CI run, we
can add a targeted skip the same way we did for the other dirs.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>
2026-05-05 15:13:31 -07:00
user
f4e7dde2d8 fix(guardrails): cover multi-choice output variants 2026-05-01 19:38:04 -07:00
Milan
9577d87158
fix(proxy): guardrail header dedupe, mypy during_call, test mock kwargs
- Dedupe names in add_guardrail_to_applied_guardrails_header (matches policies).
- Inline unified during_call condition so mypy narrows UserAPIKeyAuth.
- Extend bedrock guardrails test mock for logging_event_type.

Made-with: Cursor
2026-04-22 23:22:35 +03:00
Ishaan Jaffer
e8461b5b97
style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
Ishaan Jaffer
aaf169c91b
resolve merge conflicts: keep null-safety tests + add L3 regression tests from base 2026-04-15 12:34:39 -07:00
kothamah
168b0a05c4 added changes based on the feedback 2026-04-07 16:32:25 -04:00
kothamah
ead822b698
Added test cases for the null type handling 2026-03-19 13:55:36 -04:00
Rohan
bed44f5fe5
Add Akto Guardrails to LiteLLM (#23250)
* akto guardrails support in litellm

* docs(guardrails): add akto to supported values in types/guardrails.py

* frontend changes + fixes

* feat(akto): update Akto guardrail integration with new configuration options and modes

* docs(akto): enhance Akto documentation and configuration descriptions for clarity

* feat(tests): add proxy server request headers to sample request data

* refactor(akto): remove optional account and VXLAN IDs; update documentation and tests

* feat(akto): add event_type parameter for enhanced observability in guardrail logging

* refactor(akto): update environment variable references

* refactor the python codes

* refactor and fix linting

* refactor(akto): remove unused event hook and clean up imports

* refactor(akto): enhance AktoGuardrail with async support and improved logging

* fix: Register DynamoAI guardrail initializer and enum entry (#23752)

* fix: Register DynamoAI guardrail initializer and enum entry

Fix the "Unsupported guardrail: dynamoai" error by:
1. Adding DYNAMOAI to SupportedGuardrailIntegrations enum
2. Implementing initialize_guardrail() and registries in dynamoai/__init__.py

The DynamoAI guardrail was added in PR #15920 but never properly registered
in the initialization system. The __init__.py was missing the
guardrail_initializer_registry and guardrail_class_registry dictionaries
that the dynamic discovery mechanism looks for at module load time.

Fixes #22773

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* Update litellm/proxy/guardrails/guardrail_hooks/dynamoai/__init__.py

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

* Update litellm/proxy/guardrails/guardrail_hooks/dynamoai/__init__.py

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

* test: Add tests for DynamoAI guardrail registration

Verifies enum entry, initializer registry, class registry,
instance creation, and global registry discovery.

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

---------

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

* docs: add v1.82.3 release notes and update provider_endpoints_support.json (#23816)

* Revert "docs: add v1.82.3 release notes and update provider_endpoints_support…" (#23817)

This reverts commit 966124966f.

* Refactor Akto guardrail configuration and tests; update UI description and tags

* add account and vxlan ID parameters to Akto guardrail initialization; update Akto logo format

* enhance Akto guardrail documentation and improve error handling for non-JSON responses

* address greptile issues

* fix: update payload handling to use 'data' instead of 'json' in AktoGuardrail and adjust tests accordingly

---------

Co-authored-by: Harshit Jain <48647625+Harshit28j@users.noreply.github.com>
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Joe Reyna <joseph.reyna@gmail.com>
Co-authored-by: Krish Dholakia <krrishdholakia@gmail.com>
2026-03-17 14:38:04 -07:00
yuneng-jiang
f838bea85b Optimize CI: parallelize router and guardrails test jobs, fix test isolation
- Router testing: add CircleCI parallelism=4 with timing-based test splitting
- Guardrails testing: add pytest-xdist -n 4, suppress DEBUG logs with LITELLM_LOG=WARNING
- Rewrite conftest.py in both test dirs for xdist compatibility (save/restore pattern)
- Fix module-level Router instances in test_router_fallback_handlers, test_router_custom_routing, test_acooldowns_router

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 22:54:44 -07:00
Harshit28j
1ba42d1d99 fix: address req changes 2026-03-10 15:51:10 +05:30
Ishaan Jaff
3ff70598ad
fix: bump litellm-proxy-extras to 0.4.50 and fix 3 failing tests (#22417)
* fix(ci): handle inline table in pyproject.toml for litellm-proxy-extras version check

* fix: bump litellm-proxy-extras to 0.4.50 in pyproject.toml, requirements.txt, and poetry.lock

* fix(tests): set status_code=200 on JWT mocks and pass pii_tokens through data in presidio test
2026-02-28 10:20:03 -08:00
Sameer Kankute
a54cf53ffb Fix test_standard_logging_payload_includes_guardrail_information 2026-02-26 12:13:26 +05:30
Steve G
9806e21871
Add Lakera v2 post-call hook and tests (fixed PII masking) (#21783)
* Add post-call hook for Lakera guardrail and mask PII in responses

* Add post-call hook for Lakera and mask PII in responses

* Fix post-call hook: pass event_type to call_v2_guard

* Address Greptile review: return ModelResponse, fix mutation, add header, test location, mask order

- PII masking path: return ModelResponse instead of dict so deployment hook accepts it
- Avoid mutating request data: deep copy original_messages and messages in _mask_pii_in_messages
- Add guardrail header in PII-only return path
- Add test in tests/test_litellm/ (test_lakera_ai_v2.py) per PR checklist
- Sort PII payload spans by (start,end) descending so multiple spans in one message mask correctly

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

* Updated ponteital for index mismatch when choices have null content and inconsistent on_flagged access pattern

* Update litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py

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

* Update to explicitly state supported endpoints - chat completions

* Fix minor lint error on masked_entity_count

---------

Co-authored-by: Steve <steve.giguere@lakera.ai>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-02-25 17:20:38 -08:00
Ron Zhong
73fd5a41e4
feat: Singapore guardrail policies (PDPA + MAS AI Risk Management) (#21948)
* feat: Singapore PDPA PII protection guardrail policy template

Add Singapore Personal Data Protection Act (PDPA) guardrail support:

Regex patterns (patterns.json):
- sg_nric: NRIC/FIN detection ([STFGM] + 7 digits + checksum letter)
- sg_phone: Singapore phone numbers (+65/0065/65 prefix)
- sg_postal_code: 6-digit postal codes (contextual)
- passport_singapore: Passport numbers (E/K + 7 digits, contextual)
- sg_uen: Unique Entity Numbers (3 formats)
- sg_bank_account: Bank account numbers (dash format, contextual)

YAML policy templates (5 sub-guardrails):
- sg_pdpa_personal_identifiers: s.13 Consent
- sg_pdpa_sensitive_data: Advisory Guidelines
- sg_pdpa_do_not_call: Part IX DNC Registry
- sg_pdpa_data_transfer: s.26 overseas transfers
- sg_pdpa_profiling_automated_decisions: Model AI Governance Framework

Policy template entry in policy_templates.json with 9 guardrail definitions
(4 regex-based + 5 YAML conditional keyword matching).

Tests:
- test_sg_patterns.py: regex pattern unit tests
- test_sg_pdpa_guardrails.py: conditional keyword matching tests (100+ cases)

* feat: MAS AI Risk Management Guidelines guardrail policy template

Add Monetary Authority of Singapore (MAS) AI Risk Management Guidelines
guardrail support for financial institutions:

YAML policy templates (5 sub-guardrails):
- sg_mas_fairness_bias: Blocks discriminatory financial AI (credit/loans/insurance by protected attributes)
- sg_mas_transparency_explainability: Blocks opaque/unexplainable AI for consequential financial decisions
- sg_mas_human_oversight: Blocks fully automated financial decisions without human-in-the-loop
- sg_mas_data_governance: Blocks unauthorized sharing/mishandling of financial customer data
- sg_mas_model_security: Blocks adversarial attacks, model poisoning, inversion on financial AI

Policy template entry in policy_templates.json with 5 guardrail definitions.
Aligned with MAS FEAT Principles, Project MindForge, and NIST AI RMF.

Tests:
- test_sg_mas_ai_guardrails.py: conditional keyword matching tests (100+ cases)

* fix: address SG pattern review feedback

- Update NRIC lowercase test for IGNORECASE runtime behavior
- Add keyword context guard to sg_uen pattern to reduce false positives

* docs: clarify MAS AIRM timeline references

- Explicitly mark MAS AIRM as Nov 2025 consultation draft
- Add 2018 qualifier for FEAT principles in MAS policy descriptions
- Update MAS guardrail wording to avoid release-year ambiguity

* chore: commit resolved MAS policy conflicts

* test:

* chore:
2026-02-23 12:08:22 -08:00
Ishaan Jaff
3d8c042ca5
feat: prompt injection guardrail policy template (#21520)
* add semantic guard constants

* add SEMANTIC_GUARD enum value

* add keyword-based prompt injection policy template

* add semantic prompt injection route template

* add semantic guard route loader

* add semantic guard guardrail

* add semantic guard registration

* add semantic guard tests

* enhance SQL injection keyword category with more patterns and exceptions

* add standalone SQL injection policy template

* add SQL injection semantic guard route template

* add SQL injection guardrail tests

* remove standalone sql_injection policy template, use categories/prompt_injection_sql.yaml instead
2026-02-19 15:06:06 -08:00
Ishaan Jaff
3cc032bc0c
Add French language support for EU AI Act Article 5 guardrail (#21427)
* Add French language support for EU AI Act Article 5 template

- Create eu_ai_act_article5_fr.yaml with comprehensive French keywords
- Includes identifier words: concevoir, créer, développer, noter, classer, etc.
- Includes block words: crédit social, comportement social, émotion des employés, etc.
- Includes always-block keywords for explicit prohibited practices
- Includes exceptions for research, compliance, and legitimate use cases
- Catches circumvention attempts with phrase variations

* Add comprehensive tests for French EU AI Act guardrail

- Test 3 critical scenarios: blocked query, circumvention attempt, safe query
- Test edge cases: case-insensitive, mixed language, research exceptions
- All 7 tests passing
- Validates both blocking and allowing behavior

* Fix content filter to support conditional matching without inherit_from

- Enable conditional matching when identifier_words + additional_block_words are present
- Previously required inherit_from, but EU AI Act templates are self-contained
- Fixes Greptile feedback: conditional matching now works as documented

* Add pure conditional matching test for French guardrail

- Test identifier + block word combinations not in always_block_keywords
- Verifies conditional matching works independently
- Addresses Greptile feedback about test coverage gap

* Fix exception word bypass risk in French template

- Replace short words (film, jeu, juste) with context-specific phrases
- Prevents substring matching bypasses (e.g., enjeu matching jeu)
- Add tests for bypass prevention and legitimate game context
- Addresses Greptile security feedback

* Make conditional match assertion more robust

- Use getattr to safely access exception detail field
- Check if detail is dict before calling .get()
- Addresses Greptile feedback about brittle string assertion
2026-02-17 16:07:54 -08:00
Ishaan Jaff
d17bf84f84
feat: EU AI Act Article 5 policy template for prohibited practices detection (#21342)
* Add 6 new EU PII patterns for GDPR compliance

- fr_nir: French Social Security Number (NIR/INSEE) with validation
- eu_iban_enhanced: Enhanced IBAN detection with specific format
- fr_phone: French phone numbers (+33, 0033, 0 formats)
- eu_vat: EU VAT identification numbers (all 27 member states)
- eu_passport_generic: Generic EU passport format
- fr_postal_code: French postal codes with contextual keywords

* Add GDPR Art. 32 EU PII Protection policy template

- Comprehensive GDPR Article 32 compliance policy
- 4 guardrail groups: National IDs, Financial, Contact Info, Business IDs
- Masks French NIR/INSEE, EU IBANs, French phones, EU VAT numbers
- Includes EU passport numbers and email addresses
- Medium complexity template with indigo icon

* Add comprehensive tests for EU PII patterns

- Test French NIR validation (sex digit, month range)
- Test enhanced IBAN detection (French, German)
- Test French phone number formats
- Test EU VAT numbers
- Test generic EU passport format
- Test French postal code pattern

* Add EU pattern loading and category validation tests

- Verify all 6 EU PII patterns are loaded correctly
- Verify patterns are categorized as 'EU PII Patterns'
- Ensure pattern loading consistency

* Add end-to-end tests for GDPR policy template

- 4 tests for PII that should be masked (NIR, IBAN, phone, VAT)
- 4 tests for text that should pass through (invalid patterns, no PII)
- 1 bonus test for multiple PII types in same message
- All tests verify correct masking behavior

* Add region field to policy templates

- Added region field to all 6 templates (EU, AU, Global)
- Updated both main and backup JSON files
- Enables region-based filtering in UI

* Add region filter to policy templates UI

- Added Radio.Group filter for regions (All, AU, EU, Global)
- Efficient filtering with useMemo hooks
- Clean button-based UI matching existing design
- Defaults missing regions to Global

* feat: add EU AI Act Article 5 policy template

Add policy template for detecting EU AI Act Article 5 prohibited practices using conditional keyword matching.

Coverage:
- Article 5.1.c: Social scoring systems
- Article 5.1.f: Emotion recognition in workplace/education
- Article 5.1.h: Biometric categorization of protected characteristics
- Article 5.1.a: Harmful manipulation techniques
- Article 5.1.b: Vulnerability exploitation

Implementation:
- Uses proven conditional matching pattern (identifier + block words)
- 10 always-block keywords for explicit violations
- 8 exceptions for research/compliance/entertainment
- Zero cost (<5ms), no external APIs, 100% private

* feat: add EU AI Act guardrail config example

Example configuration showing how to enable EU AI Act Article 5 guardrail.

* test: add 40 test cases for EU AI Act Article 5

Comprehensive test coverage:
- 10 always-block keywords (explicit violations)
- 15 conditional matches (identifier + block word)
- 8 exceptions (research, compliance, entertainment)
- 7 no-match cases (legitimate uses)

Tests validate correct blocking/allowing behavior for Article 5 prohibited practices.

* Fix: support standalone conditional matching without inherit_from

- Updated loading logic to activate conditional matching when either:
  1. identifier_words + inherit_from (existing pattern)
  2. identifier_words + additional_block_words (new standalone pattern)
- Modified _load_conditional_category to handle standalone templates
- EU AI Act template now works properly without inherit_from
- All 45 tests passing

Fixes Greptile feedback: conditional matching now activates for templates
that define additional_block_words without requiring inherit_from

* fix: address Greptile code review feedback (2/5 score)

- patterns.json: add keyword_pattern to eu_vat and eu_passport_generic
- patterns.json: fix fr_phone pattern with leading word boundary
- patterns.json: fix eu_iban_enhanced regex efficiency
- policy_templates.json: remove country-specific passport patterns from GDPR template
- policy_templates_backup.json: sync with main templates file
- test_gdpr_policy_e2e.py: update test setup and fix VAT test text

All tests now pass. Keyword guards prevent false positives.

* Fix: address Greptile pattern feedback

- Fix fr_phone: use negative lookbehind (?<!\d) to prevent false matches in digit strings
- Add keyword_pattern to eu_passport_generic to reduce false positives
- Add keyword_pattern to eu_vat for contextual matching

All pattern tests passing

* Update litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/patterns.json

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

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-02-16 15:33:07 -08:00
datzscaler
aab8edde67
fix(guardrails): Zscaler AI Guard bug fixes and support during post-call (#20801)
* fix(guardrails): fixed post-call issue with Zscaler guardrail and invalid headers. Added unittests

* fix(guardrails): Addressed greptil comments. Make policyid hanlding more clear

* fix(guardrails): Address greptile comment

* Update litellm/proxy/guardrails/guardrail_hooks/zscaler_ai_guard/zscaler_ai_guard.py

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

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-02-13 11:28:14 -08:00
jwang-gif
c9df996b77
Add team policy mapping for zguard (#20608)
* support policy mapping on team key level

* update document

* update document

* address comments

* update document

* add unit test for new feature

* add more test case
2026-02-07 22:44:17 -08:00
Alexsander Hamir
69bd4426e8
[Release Day] - Fixed CI/CD issues & changed processes (#19902) 2026-01-28 17:57:24 -08:00
Sameer Kankute
9e1275b76c
Merge branch 'main' into litellm_staging_01_19_2026 2026-01-20 19:19:36 +05:30
Sameer Kankute
2153db5e64 fix: test_convert_to_bedrock_format_post_call_streaming_hook 2026-01-20 18:27:36 +05:30
Sameer Kankute
8b24720638 fix: test_standard_logging_payload_includes_guardrail_information 2026-01-20 18:21:32 +05:30
Ishaan Jaff
5ea0854eda
[Feat] Guardrails Load Balancing - Allow Platform admins to load balance between guardrails (#18181)
* add _aguardrail_helper for LB

* add _aguardrail_helper on router.py

* test_proxy_logging_pre_call_hook_load_balancing

* add _execute_guardrail_with_load_balancing

* add LB TEsting

* docs guard lb

* fix linting

* fix lint
2025-12-19 00:08:03 +05:30
Alexsander Hamir
28821427ce
[Fix] CI/CD #1 - mypy | check_code_and_doc_quality | guardrails_testing (#18195) 2025-12-18 06:31:01 -08:00
Steve G
c94f61b1da
Feature/lakera monitor mode (#18084)
* Add monitor mode support to Lakera guardrail

- Add on_flagged parameter to LakeraV2GuardrailConfigModel (default: 'block')
- Support 'monitor' mode that logs violations without blocking requests
- Support 'block' mode (default) that raises HTTPException on violations
- Update async_pre_call_hook and async_moderation_hook to check on_flagged
- Update guardrail initializer to pass on_flagged from config
- Add documentation with monitor mode examples

This allows users to tune Lakera security policies by monitoring violations
without blocking legitimate requests, similar to Pillar's on_flagged_action.

* Add tests for Lakera guardrail monitor mode

- Test monitor mode allows flagged content through (pre_call hook)
- Test block mode raises HTTPException for violations (pre_call hook)
- Test monitor mode works with during_call (moderation_hook)

These tests verify the on_flagged parameter functionality for both
monitor and block modes across different guardrail hooks.

---------

Co-authored-by: Steve <steve.giguere@lakera.ai>
2025-12-18 19:57:43 +05:30
Alexsander Hamir
96122a8b5a
Fix Presidio guardrail test TypeError and license base64 decoding error (#17538)
Fixed two issues:

1. Presidio guardrail test TypeError:
   - Issue: test_presidio_apply_guardrail() was calling apply_guardrail() with
     incorrect arguments (text=, language=) instead of the correct signature
     (inputs=, request_data=, input_type=)
   - Fix: Updated test to use correct method signature:
     - Changed from: apply_guardrail(text=..., language=...)
     - Changed to: apply_guardrail(inputs={'texts': [...]}, request_data={}, input_type='request')
   - Also updated assertions to extract text from response['texts'][0]

2. License verification base64 decoding error:
   - Issue: verify_license_without_api_request() was failing with
     'Invalid base64-encoded string: number of data characters (185) cannot be
     1 more than a multiple of 4' when license keys lacked proper base64 padding
   - Root cause: Base64 strings must be a multiple of 4 characters. Some license
     keys were missing padding characters (=) needed for proper decoding
   - Fix: Added automatic padding before base64 decoding:
     - Calculate padding needed: len(license_key) % 4
     - Add '=' characters to make length a multiple of 4
     - This makes license verification robust to keys with or without padding

Both fixes ensure the code handles edge cases properly and tests use correct APIs.
2025-12-05 08:45:02 -08:00
Krish Dholakia
be0530a6b3
fix(unified_guardrail.py): correctly map a v1/messages call to the anthropic unified guardrail (#17424)
* fix(unified_guardrail.py): correctly map a v1/messages call to the anthropic unified guardrail

* fix: add more rigorous call type checks

* fix(anthropic_endpoints/endpoints.py): initialize logging object at the beginning of endpoint

ensures call id + trace id are emitted to guardrail api

* feat(anthropic/chat/guardrail_translation): support streaming guardrails

sample on every 5 chunks

* fix(openai/chat/guardrail_translation): support openai streaming guardrails

* fix: initial commit fixing output guardrails for responses api

* feat(openai/responses/guardrail_translation): handler.py - fix output checks on responses api

* fix(openai/responses/guardrail_translation/handler.py): ensure responses api guardrails work on streaming

* test: update tests

* test: update tests

* test: update tests

* fix(bedrock_guardrails.py): fix post call streaming iterator logic

* fix: fix return

* fix(bedrock_guardrails.py): fix
2025-12-03 20:54:56 -08:00
jwang-gif
443bada425
Add Zscaler AI Guard hook (#15691)
* Add Zscaler AI Guard hook

Co-authored-by: Angela Tao <atao@zscaler.com>

* Fix lint error, update document

* Fix lint error, update document

* update document

* fix mypy type error

* fix mypy issue

* fix test

* fix test

* improve document

* remove unuseful code

* use litellm httphandler

* update test cases

* revover guardrail_initializers.py and guardrail_registry.py

* remove unuse import

* app apply_guardrail

* remove functions repleased by apply_guardrail, update test and doc

* remove functions repleased by apply_guardrail, update test and doc

---------

Co-authored-by: Angela Tao <atao@zscaler.com>
2025-11-11 15:34:27 -08:00
Ishaan Jaffer
3b1ff2a004 test_standard_logging_payload_includes_guardrail_information 2025-11-06 17:28:27 -08:00
Ishaan Jaffer
dc71eb12e3 fix _get_spend_logs_metadata 2025-11-06 17:08:53 -08:00
Ishaan Jaffer
0a8ae52014 test_guardrail_status_fields_computation 2025-11-06 16:56:50 -08:00
Ishaan Jaffer
e6d7a0a153 test fixes 2025-11-06 16:29:18 -08:00
Ishaan Jaff
e4d5f00990
[Feat] New Guardrail - Dynamo AI Guardrail (#15920)
* add dynamo types

* fix Dynamo guard

* add dynamo guardrail

* add dynamo ai docs guard

* docs fix

* test dynamo

* test LASSO
2025-10-24 17:11:04 -07:00
Ishaan Jaff
cea318330e
[Feat] Add Guardrails for /v1/messages and /v1/responses API (#15686)
* add get_guardrails_messages_for_call_type

* fix call type for /messages

* add anthropic endpoints

* fix bedrock guardrails

* fix config.yaml

* fix types

* fix async_pre_call_hook

* ruff fix

* fix guard

* fix test bedrock guardrail

* fix linting

* fix linting

* docs guardrails

* fix mypy linting
2025-10-17 18:09:00 -07:00
Ariel Fogel
59c3aa02c3 respond to review comments 2025-10-16 20:38:49 +03:00
Ariel Fogel
b075cf4a6c PLR-2400: support no persistence in litellm proxy 2025-10-16 17:22:58 +03:00
Ishaan Jaffer
4400a6c189 test bedrock guardrails 2025-10-04 09:18:26 -07:00
Patrick Lafleur
8e5efd29df
Fix comment 2025-10-01 16:39:26 -04:00
Patrick Lafleur
7ef71d4885
Fix text 2025-10-01 12:08:03 -04:00