litellm/tests/integration/authorization/test_guardrail_submissions.py
devin-ai-integration[bot] 02f56c6319
Some checks are pending
CodSpeed Benchmarks / benchmarks (push) Waiting to run
Deploy main to gateway-dev / dispatch (push) Waiting to run
Publish lint base counts / publish (basedpyright, scripts/type_check_gate.py) (push) Waiting to run
Publish lint base counts / publish (openapi-docs, scripts/openapi_docs_gate.py) (push) Waiting to run
Publish lint base counts / publish (ruff-strict, scripts/ruff_strict_gate.py) (push) Waiting to run
Publish lint base counts / publish (test-quality, scripts/test_quality_gate.py) (push) Waiting to run
Publish lint base counts / publish (type-discipline, scripts/type_discipline_gate.py) (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
LiteLLM Rust / rust-lint (push) Waiting to run
LiteLLM Rust / rust-test (push) Waiting to run
LiteLLM Rust / rust-wheel (push) Waiting to run
unit / proxy-server-core (push) Blocked by required conditions
unit / root (push) Blocked by required conditions
unit / proxy-utils (push) Blocked by required conditions
unit / proxy-infra (push) Blocked by required conditions
unit / proxy-infra-root (push) Blocked by required conditions
unit / responses-caching-types (push) Blocked by required conditions
unit / router (push) Blocked by required conditions
unit / rust-bridge-harness (push) Blocked by required conditions
unit / auth-checks (push) Blocked by required conditions
unit / budgets (push) Blocked by required conditions
unit / jwt-and-keys (push) Blocked by required conditions
unit / key-generation (push) Blocked by required conditions
unit / logging-misc (push) Blocked by required conditions
unit / mcp-oauth (push) Blocked by required conditions
unit / proxy-runtime (push) Blocked by required conditions
unit / assert-shard-coverage (push) Waiting to run
unit / proxy-server (push) Blocked by required conditions
unit / endpoints (push) Blocked by required conditions
unit / enterprise-package (push) Blocked by required conditions
unit / llms-vertex-ai (push) Blocked by required conditions
unit / proxy-feature-endpoints (push) Blocked by required conditions
unit / docs (push) Waiting to run
unit / OpenAI 2.20.0 compatibility (push) Waiting to run
unit / redis-py 8.0.1 compatibility (push) Blocked by required conditions
unit / coverage (push) Blocked by required conditions
unit / unit passed (push) Blocked by required conditions
unit / custom-logging (push) Blocked by required conditions
unit / db-and-spend (push) Blocked by required conditions
unit / endpoints-and-responses (push) Blocked by required conditions
unit / enterprise-routing (push) Blocked by required conditions
unit / guardrails-hooks (push) Blocked by required conditions
unit / rust-bridge (push) Waiting to run
unit / enterprise-managed-files (push) Blocked by required conditions
unit / lens-python-310 (push) Blocked by required conditions
unit / caching-local (push) Blocked by required conditions
unit / core-utils (push) Blocked by required conditions
unit / enterprise-repositories-secrets (push) Blocked by required conditions
unit / integrations (push) Blocked by required conditions
unit / llms-anthropic (push) Blocked by required conditions
unit / llms-bedrock (push) Blocked by required conditions
unit / llms-openai-meta (push) Blocked by required conditions
unit / llms-providers (push) Blocked by required conditions
unit / mcp-elicitation (push) Blocked by required conditions
unit / mcp-server (push) Blocked by required conditions
unit / proxy-auth (push) Blocked by required conditions
unit / proxy-endpoints (push) Blocked by required conditions
unit / proxy-extras (push) Blocked by required conditions
unit / proxy-hooks-client (push) Blocked by required conditions
unit / ui-unit (push) Waiting to run
unit / helm (push) Waiting to run
unit / OpenAI 3.0.0 compatibility (push) Waiting to run
unit / OpenAI 3.25.0 compatibility (push) Waiting to run
unit / redis-py 5.3.1 compatibility (push) Blocked by required conditions
unit / redis-py 6.4.0 compatibility (push) Blocked by required conditions
unit / redis-py 7.4.1 compatibility (push) Blocked by required conditions
GitHub Actions Security Analysis / zizmor (push) Waiting to run
test(integration): cover guardrails_policies endpoint contracts (#44722)
* test(integration): cover guardrails_policies endpoint contracts

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

* test(integration): let the admin submissions list carry other rows

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

* test(integration): close guardrails policies review gaps

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

* test(integration): cover guardrail auth and policy attachments

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

* test(integration): tidy guardrail policy test helpers

* test(integration): keep plain non-admin submission refusal live

* test: expand policy attachment integration coverage

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

* test(integration): accept the intended 403 for plain submission keys

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

* test(integration): pin multi-tag header and spend row tags for policy tag scope

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

* test(integration): drop the duplicate pydantic import left by the main merge

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

---------

Co-authored-by: mateo <mateo@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Devin <noreply@devin.ai>
2026-10-14 04:07:26 +00:00

521 lines
22 KiB
Python

import json
import uuid
from contextlib import ExitStack
from datetime import datetime
from hashlib import sha256
from typing import Final
import httpx
import pytest
from integration._support.client import Gateway, object_value
from integration._support.database import read_rows
from integration._support.wire import Reply, Request, wire_server
from pydantic import BaseModel, ConfigDict, JsonValue, TypeAdapter
_JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue])
_PROMPT_PENDING: Final = "pending submission must not run"
_PROMPT_APPROVED: Final = "approved submission runs"
_PROMPT_REJECTED: Final = "rejected submission must not run"
_APPLY_TEXT: Final = "apply guardrail after approval"
_PROVIDER_RESPONSE: Final = {
"id": "chatcmpl-guardrail-submission",
"object": "chat.completion",
"created": 1,
"model": "gpt-4o-mini",
"choices": [
{"index": 0, "message": {"role": "assistant", "content": "provider response"}, "finish_reason": "stop"}
],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
}
class _RegisterResponse(BaseModel):
model_config = ConfigDict(extra="forbid")
guardrail_id: str
guardrail_name: str
status: str
submitted_at: datetime
class _GuardrailRow(BaseModel):
guardrail_id: str
status: str
team_id: str
class _ProxyError(BaseModel):
model_config = ConfigDict(extra="forbid")
message: str
type: str
param: str | None
code: str
class _ErrorResponse(BaseModel):
model_config = ConfigDict(extra="forbid")
error: _ProxyError
class _SubmissionForbiddenResponse(BaseModel):
model_config = ConfigDict(extra="forbid")
detail: str
class _AdminActionResponse(BaseModel):
model_config = ConfigDict(extra="forbid")
guardrail_id: str
status: str
message: str
class _ApplyResponse(BaseModel):
model_config = ConfigDict(extra="forbid")
response_text: str
class _ChatMessage(BaseModel):
role: str
content: str | None
class _ChatChoice(BaseModel):
index: int
message: _ChatMessage
finish_reason: str | None
class _ChatResponse(BaseModel):
choices: list[_ChatChoice]
class _SubmissionItem(BaseModel):
guardrail_id: str
class _SubmissionsResponse(BaseModel):
submissions: list[_SubmissionItem]
summary: dict[str, int]
def _delete_guardrail(gateway: Gateway, guardrail_id: str) -> None:
deleted: Final = gateway.request("DELETE", f"/guardrails/{guardrail_id}")
assert deleted.status_code == 200, deleted.text
assert read_rows('SELECT guardrail_id FROM "LiteLLM_GuardrailsTable" WHERE guardrail_id=%s', (guardrail_id,)) == []
def _assert_submission_status(guardrail_id: str, status: str, team_id: str) -> None:
rows: Final = read_rows(
'SELECT guardrail_id, status, team_id FROM "LiteLLM_GuardrailsTable" WHERE guardrail_id=%s',
(guardrail_id,),
)
assert len(rows) == 1, rows
assert _GuardrailRow.model_validate(rows[0]) == _GuardrailRow(
guardrail_id=guardrail_id, status=status, team_id=team_id
), rows
def _assert_submission_actions_are_forbidden(responses: tuple[httpx.Response, ...]) -> None:
assert tuple(response.status_code for response in responses) == (403, 403, 403, 403), tuple(
response.text for response in responses
)
expected_error: Final = {"detail": "Admin access required"}
assert tuple(
_SubmissionForbiddenResponse.model_validate_json(response.content).model_dump() for response in responses
) == (expected_error,) * 4, tuple(response.text for response in responses)
def _mask_user_id(user_id: str) -> str:
return f"{user_id[:6]}{'*' * (len(user_id) - 8)}{user_id[-2:]}"
def _register(
gateway: Gateway,
scenario_cleanups: ExitStack,
name: str,
team_id: str | None,
key: str,
api_base: str,
) -> str:
params: Final[dict[str, JsonValue]] = {
"guardrail": "generic_guardrail_api",
"mode": "pre_call",
"default_on": False,
"api_base": api_base,
"api_key": "synthetic-submission-guardrail-key",
}
request_body: Final[dict[str, JsonValue]] = {"guardrail_name": name, "litellm_params": params}
complete_body: Final[dict[str, JsonValue]] = {
**request_body,
**({"team_id": team_id} if team_id is not None else {}),
}
response: Final = gateway.request("POST", "/guardrails/register", complete_body, key=key)
assert response.status_code == 200, response.text
created: Final = _RegisterResponse.model_validate_json(response.content)
assert (created.guardrail_name, created.status) == (name, "pending_review"), response.text
scenario_cleanups.callback(_delete_guardrail, gateway, created.guardrail_id)
return created.guardrail_id
def _chat(gateway: Gateway, model: str, key: str, text: str, guardrail: str) -> _ChatResponse:
response: Final = gateway.request(
"POST",
"/v1/chat/completions",
{"model": model, "messages": [{"role": "user", "content": text}], "guardrails": [guardrail]},
key=key,
)
assert response.status_code == 200, response.text
parsed: Final = _ChatResponse.model_validate_json(response.content)
assert parsed.choices[0].message.content == "provider response", response.text
return parsed
def _assert_provider_request(request: Request, text: str) -> None:
assert (request.method, request.target) == ("POST", "/v1/chat/completions")
payload: Final = _JSON_OBJECT.validate_json(request.body)
assert payload == {"model": "gpt-4o-mini", "messages": [{"role": "user", "content": text}]}, payload
def _assert_guardrail_request(
request: Request,
text: str,
model: str | None,
key: str,
*,
key_alias: str | None = None,
) -> None:
assert (request.method, request.target) == ("POST", "/beta/litellm_basic_guardrail_api")
payload: Final = _JSON_OBJECT.validate_json(request.body)
assert set(payload) == {
"input_type",
"litellm_call_id",
"litellm_trace_id",
"structured_messages",
"images",
"tools",
"texts",
"request_data",
"request_headers",
"litellm_version",
"additional_provider_specific_params",
"tool_calls",
"model",
}, payload
assert {
field: payload[field]
for field in (
"input_type",
"structured_messages",
"images",
"tools",
"texts",
"additional_provider_specific_params",
"tool_calls",
"model",
)
} == {
"input_type": "request",
"structured_messages": [{"role": "user", "content": text}] if model is not None else None,
"images": None,
"tools": None,
"texts": [text],
"additional_provider_specific_params": {},
"tool_calls": None,
"model": model,
}, payload
assert payload["litellm_call_id"] is None or isinstance(payload["litellm_call_id"], str), payload
assert payload["litellm_trace_id"] is None or isinstance(payload["litellm_trace_id"], str), payload
assert payload["litellm_version"] is None or isinstance(payload["litellm_version"], str), payload
request_data: Final = object_value(payload["request_data"])
if model is None:
assert request_data == {}, payload
else:
assert set(request_data).issubset(
{
"user_api_key_hash",
"user_api_key_alias",
"user_api_key_user_id",
"user_api_key_user_email",
"user_api_key_team_id",
"user_api_key_team_alias",
"user_api_key_end_user_id",
"user_api_key_org_id",
}
), request_data
assert request_data["user_api_key_hash"] == sha256(key.encode()).hexdigest(), request_data
assert request_data.get("user_api_key_alias") == key_alias, request_data
assert all(value is None or isinstance(value, str) for value in request_data.values()), request_data
request_headers: Final = payload["request_headers"]
assert request_headers is None or isinstance(request_headers, dict), payload
if isinstance(request_headers, dict):
assert "authorization" not in request_headers
assert request.headers["x-api-key"] == "synthetic-submission-guardrail-key"
def test_non_admin_keys_scoped_to_the_submission_routes_get_403_from_the_admin_check(gateway: Gateway) -> None:
with (
wire_server(lambda _request: Reply(body=json.dumps(_PROVIDER_RESPONSE).encode())) as provider,
wire_server(lambda _request: Reply(body=b'{"action":"NONE"}')) as guardrail,
gateway.scenario() as scenario,
):
model: Final = scenario.model(api_base=f"{provider.url}/v1", api_key="synthetic-provider-key")
team_id: Final = scenario.team(models=[model])
member: Final = scenario.member(team_id)
team_admin: Final = scenario.member(team_id, role="admin")
registration_key: Final = scenario.key(user_id=member, team_id=team_id, models=[model])
plain_member_key: Final = scenario.key(user_id=member, team_id=team_id, models=[model])
plain_team_admin_key: Final = scenario.key(user_id=team_admin, team_id=team_id, models=[model])
member_key: Final = scenario.key(
user_id=member,
team_id=team_id,
models=[model],
allowed_routes=["/guardrails/submissions"],
)
team_admin_key: Final = scenario.key(
user_id=team_admin,
team_id=team_id,
models=[model],
allowed_routes=["/guardrails/submissions"],
)
name: Final = f"submitted-auth-{uuid.uuid4().hex}"
guardrail_id: Final = _register(
gateway,
scenario.cleanups,
name,
team_id,
registration_key,
f"{guardrail.url}/beta/litellm_basic_guardrail_api",
)
plain_responses: Final = (
gateway.request("POST", f"/guardrails/submissions/{guardrail_id}/approve", key=plain_team_admin_key),
gateway.request("POST", f"/guardrails/submissions/{guardrail_id}/reject", key=plain_team_admin_key),
gateway.request("POST", f"/guardrails/submissions/{guardrail_id}/approve", key=plain_member_key),
gateway.request("POST", f"/guardrails/submissions/{guardrail_id}/reject", key=plain_member_key),
)
expected_routes: Final = tuple(
f"/guardrails/submissions/{guardrail_id}/{action}" for action in ("approve", "reject", "approve", "reject")
)
expected_user_ids: Final = (team_admin, team_admin, member, member)
expected_plain_errors: Final = tuple(
_ErrorResponse(
error=_ProxyError(
message=(
"Authentication Error, Only proxy admin can be used to generate, delete, update info for "
f"new keys/users/teams. Route={route}. Your role=internal_user. "
f"Your user_id={_mask_user_id(user_id)}"
),
type="auth_error",
param="None",
code="401",
)
)
for route, user_id in zip(expected_routes, expected_user_ids, strict=True)
)
route_check_refusals: Final = tuple((401, error.model_dump()) for error in expected_plain_errors)
handler_forbidden: Final = _SubmissionForbiddenResponse(detail="Admin access required").model_dump()
handler_refusals: Final = ((403, handler_forbidden),) * 4
response_models: Final = {401: _ErrorResponse, 403: _SubmissionForbiddenResponse}
observed: Final = tuple(
(
response.status_code,
(
response_models[response.status_code].model_validate_json(response.content).model_dump()
if response.status_code in response_models
else response.text
),
)
for response in plain_responses
)
assert observed in (route_check_refusals, handler_refusals), tuple(
response.text for response in plain_responses
)
_assert_submission_status(guardrail_id, "pending_review", team_id)
responses: Final = (
gateway.request("POST", f"/guardrails/submissions/{guardrail_id}/approve", key=team_admin_key),
gateway.request("POST", f"/guardrails/submissions/{guardrail_id}/reject", key=team_admin_key),
gateway.request("POST", f"/guardrails/submissions/{guardrail_id}/approve", key=member_key),
gateway.request("POST", f"/guardrails/submissions/{guardrail_id}/reject", key=member_key),
)
_assert_submission_actions_are_forbidden(responses)
assert guardrail.drain() == ()
response: Final = _chat(gateway, model, registration_key, _PROMPT_PENDING, name)
assert response.choices[0].message.content == "provider response"
assert guardrail.drain() == ()
def test_team_admins_and_internal_users_get_403_on_guardrail_submission_actions(gateway: Gateway) -> None:
pytest.skip(
"BUG: team admins and internal users get 401 from the route check instead of the "
"documented 403 on guardrail submission approve and reject"
)
with (
wire_server(lambda _request: Reply(body=json.dumps(_PROVIDER_RESPONSE).encode())) as provider,
wire_server(lambda _request: Reply(body=b'{"action":"NONE"}')) as guardrail,
gateway.scenario() as scenario,
):
model: Final = scenario.model(api_base=f"{provider.url}/v1", api_key="synthetic-provider-key")
team_id: Final = scenario.team(models=[model])
member: Final = scenario.member(team_id)
team_admin: Final = scenario.member(team_id, role="admin")
member_key: Final = scenario.key(user_id=member, team_id=team_id, models=[model])
team_admin_key: Final = scenario.key(user_id=team_admin, team_id=team_id, models=[model])
name: Final = f"submitted-auth-{uuid.uuid4().hex}"
guardrail_id: Final = _register(
gateway,
scenario.cleanups,
name,
team_id,
member_key,
f"{guardrail.url}/beta/litellm_basic_guardrail_api",
)
responses: Final = (
gateway.request("POST", f"/guardrails/submissions/{guardrail_id}/approve", key=team_admin_key),
gateway.request("POST", f"/guardrails/submissions/{guardrail_id}/reject", key=team_admin_key),
gateway.request("POST", f"/guardrails/submissions/{guardrail_id}/approve", key=member_key),
gateway.request("POST", f"/guardrails/submissions/{guardrail_id}/reject", key=member_key),
)
_assert_submission_actions_are_forbidden(responses)
_assert_submission_status(guardrail_id, "pending_review", team_id)
assert guardrail.drain() == ()
def test_team_guardrail_submissions_require_admin_approval(
gateway: Gateway,
) -> None:
with (
wire_server(lambda _request: Reply(body=json.dumps(_PROVIDER_RESPONSE).encode())) as provider,
wire_server(lambda _request: Reply(body=b'{"action":"NONE"}')) as guardrail,
gateway.scenario() as scenario,
):
model: Final = scenario.model(api_base=f"{provider.url}/v1", api_key="synthetic-provider-key")
team_id: Final = scenario.team(models=[model])
member: Final = scenario.member(team_id)
team_admin: Final = scenario.member(team_id, role="admin")
member_key_alias: Final = f"submission-member-{uuid.uuid4().hex}"
member_key: Final = scenario.key(
user_id=member,
team_id=team_id,
models=[model],
key_alias=member_key_alias,
)
team_admin_key: Final = scenario.key(
user_id=team_admin,
team_id=team_id,
models=[model],
key_alias=f"submission-team-admin-{uuid.uuid4().hex}",
)
other_team_id: Final = scenario.team(models=[model])
api_base: Final = f"{guardrail.url}/beta/litellm_basic_guardrail_api"
first_name: Final = f"submitted-member-{uuid.uuid4().hex}"
second_name: Final = f"submitted-admin-{uuid.uuid4().hex}"
other_name: Final = f"submitted-other-team-{uuid.uuid4().hex}"
first_id: Final = _register(gateway, scenario.cleanups, first_name, team_id, member_key, api_base)
second_id: Final = _register(gateway, scenario.cleanups, second_name, None, team_admin_key, api_base)
other_id: Final = _register(gateway, scenario.cleanups, other_name, other_team_id, gateway.key, api_base)
_assert_submission_status(first_id, "pending_review", team_id)
_assert_submission_status(second_id, "pending_review", team_id)
_assert_submission_status(other_id, "pending_review", other_team_id)
pending_apply: Final = gateway.request(
"POST", "/apply_guardrail", {"guardrail_name": first_name, "text": _APPLY_TEXT}
)
assert pending_apply.status_code == 404, pending_apply.text
assert _ErrorResponse.model_validate_json(pending_apply.content).model_dump() == {
"error": {
"message": (
f"Guardrail '{first_name}' not found. "
"Please ensure the guardrail is configured in your LiteLLM proxy."
),
"type": "internal_server_error",
"param": None,
"code": "404",
}
}, pending_apply.text
pending_chat: Final = _chat(gateway, model, member_key, _PROMPT_PENDING, first_name)
assert len(pending_chat.choices) == 1
assert guardrail.drain() == ()
_assert_submission_status(first_id, "pending_review", team_id)
approved: Final = gateway.request("POST", f"/guardrails/submissions/{first_id}/approve")
assert approved.status_code == 200, approved.text
assert _AdminActionResponse.model_validate_json(approved.content).model_dump() == {
"guardrail_id": first_id,
"status": "active",
"message": "Guardrail approved",
}, approved.text
_assert_submission_status(first_id, "active", team_id)
approved_chat: Final = _chat(gateway, model, member_key, _PROMPT_APPROVED, first_name)
assert len(approved_chat.choices) == 1
active_apply: Final = gateway.request(
"POST", "/apply_guardrail", {"guardrail_name": first_name, "text": _APPLY_TEXT}
)
assert active_apply.status_code == 200, active_apply.text
assert _ApplyResponse.model_validate_json(active_apply.content).response_text == _APPLY_TEXT, active_apply.text
rejected: Final = gateway.request("POST", f"/guardrails/submissions/{second_id}/reject")
assert rejected.status_code == 200, rejected.text
assert _AdminActionResponse.model_validate_json(rejected.content).model_dump() == {
"guardrail_id": second_id,
"status": "rejected",
"message": "Guardrail rejected",
}, rejected.text
_assert_submission_status(second_id, "rejected", team_id)
rejected_chat: Final = _chat(gateway, model, member_key, _PROMPT_REJECTED, second_name)
assert len(rejected_chat.choices) == 1
rejected_apply: Final = gateway.request(
"POST", "/apply_guardrail", {"guardrail_name": second_name, "text": _APPLY_TEXT}
)
assert rejected_apply.status_code == 404, rejected_apply.text
assert _ErrorResponse.model_validate_json(rejected_apply.content).model_dump() == {
"error": {
"message": (
f"Guardrail '{second_name}' not found. "
"Please ensure the guardrail is configured in your LiteLLM proxy."
),
"type": "internal_server_error",
"param": None,
"code": "404",
}
}, rejected_apply.text
team_list: Final = gateway.request("GET", "/guardrails/submissions", key=member_key)
assert team_list.status_code == 200, team_list.text
team_submissions: Final = _SubmissionsResponse.model_validate_json(team_list.content)
assert {item.guardrail_id for item in team_submissions.submissions} == {first_id, second_id}, team_list.text
admin_list: Final = gateway.request("GET", "/guardrails/submissions")
assert admin_list.status_code == 200, admin_list.text
admin_submissions: Final = _SubmissionsResponse.model_validate_json(admin_list.content)
assert {first_id, second_id, other_id} <= {item.guardrail_id for item in admin_submissions.submissions}, (
admin_list.text
)
provider_requests: Final = provider.drain()
assert [(request.method, request.target) for request in provider_requests] == [
("POST", "/v1/chat/completions"),
("POST", "/v1/chat/completions"),
("POST", "/v1/chat/completions"),
]
for request, text in zip(provider_requests, (_PROMPT_PENDING, _PROMPT_APPROVED, _PROMPT_REJECTED), strict=True):
_assert_provider_request(request, text)
guardrail_requests: Final = guardrail.drain()
assert [(request.method, request.target) for request in guardrail_requests] == [
("POST", "/beta/litellm_basic_guardrail_api"),
("POST", "/beta/litellm_basic_guardrail_api"),
]
_assert_guardrail_request(
guardrail_requests[0],
_PROMPT_APPROVED,
model,
member_key,
key_alias=member_key_alias,
)
_assert_guardrail_request(guardrail_requests[1], _APPLY_TEXT, None, gateway.key)