Commit graph

1778 commits

Author SHA1 Message Date
michelligabriele
f74cd07419
feat(proxy): add project-level guardrails support (#25087) 2026-04-03 20:20:44 -07:00
ishaan-berri
c6aa3ea452
Litellm ishaan april1 try2 (#25110)
* Litellm ishaan april1 (#25103)

* fix(proxy): enforce upperbound key params on key/update and add custom_key_update hook

The /key/update endpoint did not enforce upperbound_key_generate_params,
allowing users to bypass configured limits (tpm_limit, rpm_limit,
max_budget, duration, budget_duration) by updating an existing key
instead of generating a new one.

Extract the upperbound enforcement logic from _common_key_generation_helper()
into a standalone _enforce_upperbound_key_params() function and call it from
both the generate and update paths. For updates, None values are skipped
(not filled with defaults) since they mean "don't change this field".

Also adds a custom_key_update config option and user_custom_key_update global,
mirroring the existing custom_key_generate pattern, so custom key validation
logic can fire during key updates as well.

* fix(proxy): invoke custom_key_update hook in bulk update path

The user_custom_key_update hook was only called in update_key_fn
(single key update) but not in _process_single_key_update (bulk
update path), allowing custom validation to be bypassed via the
/key/update/bulk endpoint. Mirror the hook invocation in both paths.

* fix(proxy): pass UpdateKeyRequest to hook in bulk path, not BulkUpdateKeyRequestItem

Move the custom_key_update hook invocation to after UpdateKeyRequest
is constructed so the hook receives the same type in both single and
bulk update paths. Previously the bulk path passed
BulkUpdateKeyRequestItem (5 fields only), which would cause
AttributeError for hooks accessing fields like tpm_limit or models.

* fix(bedrock): promote cache usage to message_delta for Claude Code (#24850)

Ensure Bedrock/Anthropic-compatible streaming exposes cache usage where Claude Code reads it by promoting message_stop usage onto message_delta and preserving usage fields in fake-streamed message_delta events.

Made-with: Cursor

* fix(search): Support self-hosted Firecrawl response format in search transform (#24866)

The `transform_search_response` method only handled Firecrawl Cloud (v2)
response format where `data` is a dict with `web`/`news` keys. Self-hosted
Firecrawl (v1) returns `data` as a flat list of result objects, causing an
`AttributeError: 'list' object has no attribute 'get'`.

Detect the response format by checking if `data` is a list (self-hosted)
or dict (cloud) and handle both cases.

Cloud format:  {"data": {"web": [...], "news": [...]}}
Self-hosted:   {"success": true, "data": [{"url": "...", "title": "...", ...}]}

Co-authored-by: Synergy <synergyoclaw@gmail.com>

* feat: add environment and user tracking to prompt management (#24855)

* feat: add environment and user tracking to prompt management

- Add environment (development/staging/production) and created_by columns to LiteLLM_PromptTable
- Update unique constraint to [prompt_id, version, environment]
- All CRUD endpoints support environment filtering and user tracking
- Redesigned prompt detail page with environment tabs and version history
- UI: environment filter on list page, environment selector in editor
- 8 new tests for environment and user tracking

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

* fix: Black formatting and add environments to PromptInfoResponse TypeScript type

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

* fix: address Greptile review findings

- P1: delete_prompt scopes in-memory cleanup to environment when provided
- P2: dotprompt_content parsed directly regardless of environment flag
- P2: use distinct for environments query
- P2: fix double-fetch on initial mount in prompt_info.tsx
- fix: remove unsupported select kwarg from find_many

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

* fix: address remaining Greptile review comments

- Remove unused useCallback import (index.tsx)
- Remove unused ENV_COLORS variable (prompt_info.tsx)
- P1: in-memory fallback in get_prompt_versions now respects environment filter
- P1: reset selectedEnv when promptId changes to avoid stale state
- Cyclic imports are pre-existing pattern, not introduced by this PR

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

* fix: scope patch_prompt to environment using primary key

- Add environment query param to patch_prompt endpoint
- Look up target row by composite key (prompt_id + version + environment)
- Update by primary key (id) to target exactly one row
- Fixes Greptile finding: patch with multiple environments no longer ambiguous

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

---------

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

* fix: use actual start_time for failed request spend logs (#24906)

async_post_call_failure_hook set both start_time and end_time to
datetime.now(), making all failed requests show duration=0. Use the
actual start_time from litellm_logging_obj instead, so spend logs
reflect the real request duration on timeout and other failures.

Fixes #24888

* feat(bedrock): add nova canvas image edit support (#24869)

* feat(bedrock): add nova canvas image edit support

* fix(bedrock): support PathLike inputs for nova image edit

* chore: sync schema.prisma copies from root

* fix(mypy): correct type-ignore code for delta_usage arg-type

* fix(mypy): cast status_code to str, suppress intentional str yield

* fix(lint): extract _create_content_block_chunks to fix PLR0915

* fix(lint): extract helpers to fix PLR0915 in prompt endpoints

---------

Co-authored-by: michelligabriele <gabriele.michelli@icloud.com>
Co-authored-by: Sameer Kankute <sameer@berri.ai>
Co-authored-by: redhelix <amin.lalji@gmail.com>
Co-authored-by: Synergy <synergyoclaw@gmail.com>
Co-authored-by: Talha Anwar <37379131+talhaanwarch@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: madhu19991 <madhu@thunkai.com>
Co-authored-by: Srikanth @adobe <devarakondasrikanth@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(test): update model armor streaming test to handle string or int error code

---------

Co-authored-by: michelligabriele <gabriele.michelli@icloud.com>
Co-authored-by: Sameer Kankute <sameer@berri.ai>
Co-authored-by: redhelix <amin.lalji@gmail.com>
Co-authored-by: Synergy <synergyoclaw@gmail.com>
Co-authored-by: Talha Anwar <37379131+talhaanwarch@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: madhu19991 <madhu@thunkai.com>
Co-authored-by: Srikanth @adobe <devarakondasrikanth@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-03 14:57:44 -07:00
michelligabriele
a6dfd02610
fix(guardrails): return HTTP 400 instead of 500 for Model Armor streaming blocks (#24693)
When Model Armor blocks a streaming response, it correctly raises
HTTPException(status_code=400) but create_response() catches it with a
bare except Exception and hardcodes a 500 response, discarding the
original status code.

Fix create_response() to preserve status_code from HTTPException instead
of hardcoding 500. Also update Model Armor's streaming hook to yield an
SSE error event instead of raising (matching the Prisma Airs pattern),
and fix make_model_armor_request() to return 400 for upstream API
failures instead of passing through the upstream status code.
2026-04-02 21:28:52 -07:00
yuneng-jiang
3604b600d3
[Infra] Merge internal dev branch with main (#25036)
* fix(proxy): enforce key-level model allowlist for custom auth

custom_auth_run_common_checks only runs common_checks (team/user/project model checks).
Custom auth now also enforces key-level model restrictions via can_key_call_model.

Move the custom-auth key-access regression tests to test_user_api_key_auth.py and keep test_custom_auth_end_user_budget.py focused on end-user budget behavior.

Made-with: Cursor

* fix(proxy): gate custom-auth key model checks behind opt-in

Keep key-level model allowlist enforcement in custom auth behind `custom_auth_run_common_checks` to preserve backwards compatibility, and update tests to verify default non-enforcement and opt-in enforcement behavior.

Made-with: Cursor

* test(proxy): isolate custom auth default check from shared settings state

Patch `proxy_server.general_settings` to an empty dict in the default custom-auth key-access test so it remains deterministic under shared module state.

Made-with: Cursor

* test(proxy): strengthen custom auth post-check assertions

Tighten custom auth regression tests by asserting exact can_key_call_model args and remove an unused common_checks mock from the default behavior path.

Made-with: Cursor

* fix(agentcore): parse A2A JSON-RPC responses in AgentCore provider

* fix(prompt-templates): ensure_alternating_roles handles tool-call chains

* feat(auth): add JWT claim routing overrides for OAuth2 validation

Made-with: Cursor

* docs(auth): document JWT-to-OAuth2 routing overrides

Add generic docs for running JWT and OAuth2 together, including routing_overrides YAML examples and list-based selector behavior for iss/client_id/aud.

Made-with: Cursor

---------

Co-authored-by: Milan <milan@berri.ai>
Co-authored-by: michelligabriele <gabriele.michelli@icloud.com>
2026-04-02 16:38:01 -07:00
David Chen
d1df4e838b
Litellm fix update bedrock models (#24947)
* update bedrock models in tests

* updated more tests and model_prices_and_context_window

* fix model id and pricing

* replace more sonnet models

* update tests

* git push

* update pricing

* flaky total cost

* monkey patch

* relax the cost change

* fix and revert some changes

* revert the pricing

* chore: move cost/pricing changes to bedrock-cost-fixes branch

* chore: split Bedrock file-api beta stripping to separate branch

Removes strip_unsupported_file_api_betas_for_bedrock_invoke from this branch;
see litellm_bedrock_invoke_strip_file_api_betas for that fix.

Made-with: Cursor
2026-04-01 19:22:54 -07:00
michelligabriele
adedae2cfa
fix(auth): enforce budget for models not in cost map (#24949)
* fix(auth): enforce budget for models not in cost map

* fix log injection in debug messages + isolate test global state
2026-04-01 19:10:30 -07:00
Yuneng Jiang
6a2b03a850
[Infra] Merge main and resolve dependency pin conflicts
Resolve conflicts between pinned versions and main's caret ranges,
keeping exact pins. Add pytest-cov==5.0.0 from main. Regenerate
poetry.lock.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 09:55:13 -07:00
Yuneng Jiang
7b277d36cd
[Fix] Fix test failures and Docker build from pinned dependency upgrade
pytest-asyncio 1.x no longer provides an implicit event loop in sync
fixtures/tests. Make async-dependent fixtures and tests async, and
replace deprecated asyncio.get_event_loop() in tests. Switch
Dockerfile.build_from_pip from Alpine to Debian slim since
pyroscope-io 0.8.x has no musl wheels.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 09:43:33 -07:00
michelligabriele
283375f4d6
fix(proxy): eliminate race condition in streaming guardrail_information logging (#24592)
asyncio.create_task in CSW.__anext__ scheduled the deferred logging
callback as an independent task that raced with unified_guardrail's
end-of-stream block. For short-stream providers (Vertex AI, Azure,
Anthropic), the logging fired before guardrail_information was written,
causing post_call guardrail entries to be missing from
StandardLoggingPayload.

Move the deferred callback trigger from CSW.__anext__ to
ProxyLogging.async_post_call_streaming_iterator_hook (after the full
streaming pipeline completes). CSW now stores the assembled response
args; the outer consumer fires the callback after all guardrail
end-of-stream blocks finish. Also skip apply_guardrail guardrails in
_run_deferred_stream_guardrails to eliminate duplicate API calls.
2026-04-01 08:06:56 -07:00
ryan-crabbe-berri
2f1cfb0548
Merge pull request #24751 from BerriAI/litellm_ryan-march-28
litellm ryan march 28
2026-03-31 17:25:30 -07:00
Yuneng Jiang
e014dfb8e0
fix: mock encrypt_value_helper in lifecycle test
CI fails because LITELLM_MASTER_KEY is not set, causing
encrypt_value_helper to error on NoneType.encode(). Patch it
to pass through values unencrypted in the test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 21:36:40 -07:00
Yuneng Jiang
b48c037712
test: replace test_add_and_delete_models integration test with mock
Skip the integration test that requires a live proxy and OPENAI_API_KEY
(removed from CI/CD). Add deterministic mock test covering the same
add → delete → double-delete-fails lifecycle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 21:30:57 -07:00
Krrish Dholakia
5d6c76aa1a
Merge pull request #24823 from jaydns/fixes
chore: fixes
2026-03-30 19:35:40 -07:00
jayden
57c05459ae
chore: fixes 2026-03-30 18:36:58 -07:00
Krrish Dholakia
5596728cae
Merge pull request #24753 from BerriAI/litellm_dev_03_27_2026_p1
Fix returned model when batch completions is used - return picked model, not comma-separated list
2026-03-30 17:53:48 -07:00
Krrish Dholakia
4c00a14ce0 fix: fix ci/cd + handle oidc jwt tokens 2026-03-30 16:12:58 -07:00
Yuneng Jiang
6522d282b5
[Fix] Correct kwarg name in test_user_api_key_auth tests
PR #24755 renamed `azure_api_key_header` to `AZURE_AI_API_KEY_header` in
the test file but did not update the actual function signatures of
`get_api_key()` and `_user_api_key_auth_builder()`, causing TypeError
on all affected test cases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 14:51:09 -07:00
Krrish Dholakia
bc829d51f2 test: test 2026-03-28 19:17:38 -07:00
Ryan Crabbe
a59acc8f8a
[Fix] Derive use_substring_matching from role, not auto-set path
Substring matching for user_id and key_alias on /key/list is now gated
by caller role (admin only) rather than by whether user_id was auto-set.
This closes the gap where a non-admin explicitly passing their own
user_id still got contains-based filtering, which could surface keys
belonging to other users with overlapping IDs.

Adds three unit tests covering the substring, exact-match, and default
code paths in _build_key_filter_conditions. Restores the existing
member-team test to assert exact matching as a security regression guard.
2026-03-28 16:50:09 -07:00
Ryan Crabbe
65df501f15
Simplify substring search — remove admin-only gating
Substring matching for user_id and key_alias now applies unconditionally.
The admin-only flag was unnecessary since user_ids are typically UUIDs/emails
where substring collisions are not a practical concern. Updated existing test
to assert the new filter structure.
2026-03-28 14:50:31 -07:00
Ryan Crabbe
8d9bbc6eb2
[Fix] Include access group models in UI model listing
Models associated with a team only through access groups (not directly in
team.models) were not appearing on the /ui/?page=models page. The API
authorization path already resolved access groups correctly, but the
/v2/model/info listing endpoint only checked team.models.

Add _add_access_group_models_to_team_models() which batch-fetches all
distinct access groups in a single find_many query, then resolves each
team's access group models into deployments and merges them into the
team_models dict.
2026-03-28 12:32:12 -07:00
Yuneng Jiang
7100ed5d0a
[Fix] Test isolation for agent health checks and documentation test path resolution
Fix agent health check tests failing with 500 errors in parallel CI by
mocking prisma_client to None. Fix documentation validation tests using
CWD-relative paths that break depending on the working directory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 11:00:22 -07:00
ryan-crabbe-berri
2eb3c20e76
Merge pull request #24718 from BerriAI/litellm_ryan-march-26
litellm ryan march 26
2026-03-28 09:01:11 -07:00
Krrish Dholakia
32adda8a49 fix: return winning model name instead of comma-separated list for fastest_response
When fastest_response=true with comma-separated models, the response
model field was stamped with the entire comma-separated string. Now
uses the x-litellm-model-group header from the winning response to
return the correct model name.

Made-with: Cursor
2026-03-27 22:34:26 -07:00
ryan-crabbe-berri
726a34627c
Merge pull request #24717 from BerriAI/litellm_fix-user-cache-invalidation
fix(jwt): invalidate user cache after role/team sync updates
2026-03-27 19:50:41 -07:00
Ryan Crabbe
dd11e77852
fix: add explicit TTL to cache writes and test coverage for user cache invalidation
Add DEFAULT_MANAGEMENT_OBJECT_IN_MEMORY_CACHE_TTL to both async_set_cache
calls in sync_user_role_and_teams for consistency with all other user cache
writes. Add 3 tests covering cache invalidation on role change, team change,
and no-op when nothing changes.
2026-03-27 19:45:13 -07:00
ryan-crabbe-berri
5b651048f2
Merge pull request #24706 from BerriAI/litellm_fix-jwt-none-guard
fix(auth): guard JWTHandler.is_jwt() against None token
2026-03-27 18:06:24 -07:00
yuneng-jiang
846e4b44b6
Merge pull request #24682 from michelligabriele/fix/budget-spend-counters
fix(proxy): enforce budget limits across multi-pod deployments via Redis-backed spend counters
2026-03-27 16:59:23 -07:00
Ryan Crabbe
8e3755931d
test(auth): add regression tests for JWTHandler.is_jwt(None)
Add None-token test cases to both proxy_unit_tests and test_litellm
to cover the guard added in the previous commit. Also add -> bool
return type annotation to is_jwt().
2026-03-27 16:51:08 -07:00
Ryan Crabbe
e24819afef
fix(sso): pass decoded JWT access token to role mapping during SSO login
During SSO login, bearer tokens are stripped from the OAuth response
before role mapping runs. Custom role claims encoded inside the JWT
access token are lost, so map_jwt_role_to_litellm_role() returns None
and the user falls back to internal_user_viewer.

process_sso_jwt_access_token() now returns the decoded JWT payload, and
a new _sync_user_role_from_jwt_role_map() receives it so
jwt_litellm_role_map works correctly during SSO login.
2026-03-27 13:50:30 -07:00
michelligabriele
d533b432fd
fix(proxy): enforce budget limits across multi-pod deployments via Redis-backed spend counters
Budget checks on API keys, teams, and team members were not enforced in
multi-pod deployments because user_api_key_cache is intentionally
in-memory-only. Each pod tracked spend independently, so with N pods
the effective budget was N × max_budget.

Introduces a separate spend_counter_cache (DualCache wired to
redis_usage_cache) with atomic increment/read helpers:
- increment_spend_counters(): awaited in cost callback (not create_task)
  to update both in-memory and Redis before the next auth check
- get_current_spend(): reads Redis first (cross-pod authoritative),
  falls back to in-memory, then to cached object .spend from DB

Budget check functions (_virtual_key_max_budget_check,
_team_max_budget_check, _check_team_member_budget) now read spend via
get_current_spend() instead of cached object .spend fields.

When Redis is not configured, falls back to in-memory-only counters
(same as current single-instance behavior).

Fixes #23714
2026-03-27 20:39:52 +01:00
yuneng-jiang
1b111d23f3
Merge pull request #24688 from Sameerlite/litellm_litellm_team-model-group-name-routing-fix
fix(team-routing): preserve sibling deployment candidates for team public models
2026-03-27 12:00:34 -07:00
Sameer Kankute
92a07e2d6e
fix(proxy): address Greptile review feedback
- Remove HTTP_PROXY/HTTPS_PROXY from blocklist (legitimately used in corporate envs)
- Add NO_PROXY/no_proxy to blocklist (prevents bypassing proxy monitoring)
- Remove dead code in _is_valid_user_id (space exception was unreachable)
- Update tests accordingly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 20:38:36 +05:30
Sameer Kankute
8112fbf274
fix(proxy): sanitize user_id input and block dangerous env var keys
Add input validation to get_user_id_from_request (length limit, control char rejection) and a blocklist of dangerous environment variable keys in _load_environment_variables to prevent PATH/LD_PRELOAD/PYTHONPATH override via config.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 20:38:36 +05:30
Sameer Kankute
2321d77599
fix(router): address remaining Greptile review comments
- Cache LITELLM_ENABLE_TEAM_STALE_ALIAS_BYPASS at module level to avoid hot-path secret lookups
- Add clarifying comments for should_include_deployment team isolation logic
- Add negative assertion for update_team.assert_not_called() in test
- Add docstring clarification for _get_team_deployments helper pattern
- Add explicit assertion message in test_get_model_list_alias_optimization

Made-with: Cursor
2026-03-27 20:11:28 +05:30
Sameer Kankute
1a0b30aaac
Fix greptile reviews and mock test 2026-03-27 20:11:28 +05:30
Sameer Kankute
c6cc0341f6
Fix greptile reviews and mock test 2026-03-27 20:11:28 +05:30
Sameer Kankute
316a742945
Fix greptile comments 2026-03-27 20:11:28 +05:30
Sameer Kankute
173695f5e0
Fix greptile comments 2026-03-27 20:11:27 +05:30
Sameer Kankute
e8fb7762b3
perf(routing): optimize team model checks and improve test coverage
- Use O(1) team index lookup instead of map_team_model in alias guard
- Fix MockPrismaClient to validate where clause filters
- Add comment explaining DB query trade-off for team deployments

Made-with: Cursor
2026-03-27 20:11:27 +05:30
Sameer Kankute
8aa58bdcaa
fix(routing): prevent stale model_aliases from interfering with team routing
- Skip model_aliases rewrite if model resolves to team deployments
- Add test coverage for sibling-preservation branch
- Update MockPrismaClient to support sibling deployment scenarios

Made-with: Cursor
2026-03-27 20:11:27 +05:30
Sameer Kankute
f5b7298854
fix(management): query DB directly for sibling deployments on rename
- Add clarifying comments to test assertions
- Query prisma DB instead of in-memory router to avoid stale state
- Prevents incorrect deletion of old public name when siblings exist

Made-with: Cursor
2026-03-27 20:11:27 +05:30
Sameer Kankute
aeb932d707
fix(team-routing): keep team model routing on public names
Remove team model_alias rewrites and resolve team deployments by team_public_model_name with team_id so sibling deployments stay in the routing candidate pool, with explicit logs showing candidate selection before load balancing.

Made-with: Cursor
2026-03-27 20:11:27 +05:30
Sameer Kankute
5534b40ab3
fix(team-routing): use deterministic team model group names
Use a deterministic internal model_name for team-scoped deployments so sibling deployments with the same public model share a routing group. This makes team alias writes idempotent and preserves multi-deployment failover/load balancing behavior.

Made-with: Cursor
2026-03-27 20:11:27 +05:30
Ryan Crabbe
0aadf51342 fix(proxy): ignore return_to in SSO when control_plane_url is not configured
Instead of returning a 400 error when return_to is passed without
control_plane_url configured, silently ignore it and proceed with
the normal same-origin SSO flow.
2026-03-23 21:54:29 -07:00
Krrish Dholakia
26d162ccf4 fix(test): add user_api_key_project_alias to spend logs expected keys
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 18:12:50 -07:00
michelligabriele
fa7ccf0893 fix(test): add request_data param to test mock + black formatting 2026-03-23 15:43:05 +01:00
michelligabriele
4625ccbaa2 fix(proxy): anchor metadata dict in _process_response/_process_error so pop() mutates the real dict 2026-03-23 15:39:23 +01:00
michelligabriele
d8fd9a20ed fix(proxy): address Greptile review — streaming request_data, OCR backward compat, test coverage
- Pass request_data to end-of-stream process_output_streaming_response call
- Restore inputs.update() in OCR handler for third-party guardrail providers
- Add streaming end-to-end test for guardrail logging passthrough
2026-03-23 15:39:23 +01:00
michelligabriele
ae454fd700 fix(proxy): OpenAI Moderation post-call guardrail response not captured for logging
Two independent bugs prevented post-call OpenAI Moderation guardrail
results from reaching downstream logging callbacks (Langfuse, Datadog).

Bug 1: process_output_response() created a throwaway request_data dict,
so guardrail info written by @log_guardrail_information was discarded.
Fixed by threading the real request_data from the unified guardrail
dispatcher through all 13 BaseTranslation handlers, with litellm_metadata
injection preserved for third-party guardrails (Zscaler, Prompt Security).
Also extended to process_output_streaming_response for consistency.

Bug 2: The @log_guardrail_information decorator collapsed the full
moderation API response (categories, scores, flagged status) to "allow".
Fixed by overriding _process_response/_process_error on
OpenAIModerationGuardrail to stash and log the full response, following
the established Model Armor pattern.
2026-03-23 15:39:22 +01:00