fix(ci): unbreak realtime + bedrock batch tests (#27690)

* fix(tests): drop deprecated OpenAI-Beta realtime header

OpenAI deprecated the 'OpenAI-Beta: realtime=v1' header; the live
service now returns code 4000 invalid_beta with
"Unknown beta requested: 'realtime'.". Two integration tests in
tests/llm_translation/realtime/test_realtime_guardrails_openai.py
hardcoded the header and started failing across all PRs.

Library code is unaffected: the OpenAI realtime handler only
forwards 'OpenAI-Beta: realtime=v1' upstream when the proxy *client*
sends it (litellm/llms/openai/realtime/handler.py). Default proxy
behavior uses the GA protocol.

Connect to OpenAI without the deprecated header, and accept the GA
event name 'response.output_audio_transcript.delta' alongside the
beta-protocol name 'response.audio_transcript.delta' for the
transcript-delta assertion.

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

* fix(logging): post_call tolerates non-JSON-serializable values

post_call() did json.dumps(original_response) without default=str, so
any provider passing a dict containing datetime/Decimal/etc. would
raise TypeError. Bedrock batch retrieval hits this with
get_model_invocation_job() responses that include datetime fields
(submitTime, lastModifiedTime, endTime), failing
tests/batches_tests/test_bedrock_files_and_batches.py::test_async_file_and_batch
across all PRs.

Pass default=str so non-serializable values fall back to str().

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

* fix(tests): mock boto3 in bedrock retrieve batch test

The test patched AsyncHTTPHandler.get, but the bedrock retrieve
handler uses boto3.client('bedrock').get_model_invocation_job
directly, so the real AWS call was being made on every run, failing
with AccessDeniedException because the hardcoded test ARN belongs to
a different AWS account.

- Mock boto3.client and BedrockBatchesConfig.get_credentials so the
  test never touches AWS.
- Use status=Completed in the mock response so output_file_id is
  populated (the handler intentionally leaves it None for
  non-completed jobs).
- Assert the predicted per-job output object URI (matches what the
  handler actually returns) instead of the bare output prefix.

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

* docs(tests): include GA event name in guardrail-block test docstring

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

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>
This commit is contained in:
Mateo Wang 2026-05-11 21:08:14 -04:00 • committed by GitHub
parent afbe864750
commit 6de00e24b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 41 additions and 30 deletions

View file

@ -1212,7 +1212,7 @@ class Logging(LiteLLMLoggingBaseClass):
# Log the exact result from the LLM API, for streaming - log the type of response received
litellm.error_logs["POST_CALL"] = locals()
if isinstance(original_response, dict):
original_response = json.dumps(original_response)
original_response = json.dumps(original_response, default=str)
try:
self.model_call_details["input"] = input
self.model_call_details["api_key"] = api_key

View file

@ -157,16 +157,16 @@ async def test_bedrock_retrieve_batch():
"""
print("Testing bedrock batch retrieval")
# Mock bedrock batch response
mock_bedrock_response = {
"jobArn": "arn:aws:bedrock:us-west-2:123456789012:model-invocation-job/test-job-123",
"jobName": "test-job-123",
"modelId": "us.anthropic.claude-haiku-4-5-20251001-v1:0",
"roleArn": "arn:aws:iam::123456789012:role/service-role/AmazonBedrockExecutionRoleForAgents_TEST",
"status": "InProgress",
"message": "Job is in progress",
"status": "Completed",
"message": "",
"submitTime": "2024-01-01T12:00:00Z",
"lastModifiedTime": "2024-01-01T12:30:00Z",
"endTime": "2024-01-01T13:00:00Z",
"inputDataConfig": {
"s3InputDataConfig": {"s3Uri": "s3://test-bucket/input/test-input.jsonl"}
},
@ -175,43 +175,38 @@ async def test_bedrock_retrieve_batch():
},
}
# Mock the HTTP response
mock_response = MagicMock()
mock_response.json.return_value = mock_bedrock_response
mock_response.status_code = 200
mock_bedrock_client = MagicMock()
mock_bedrock_client.get_model_invocation_job.return_value = mock_bedrock_response
mock_creds = MagicMock(access_key="ak", secret_key="sk", token="tok")
# Print the mock response to debug
print("MOCK RESPONSE DATA:", mock_bedrock_response)
with patch(
"litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.get"
) as mock_get:
mock_response.raise_for_status.return_value = None
mock_get.return_value = mock_response
# Test retrieve batch
with (
patch("boto3.client", return_value=mock_bedrock_client),
patch(
"litellm.llms.bedrock.batches.transformation.BedrockBatchesConfig.get_credentials",
return_value=mock_creds,
),
):
batch_response = await litellm.aretrieve_batch(
batch_id="arn:aws:bedrock:us-west-2:123456789012:model-invocation-job/test-job-123",
custom_llm_provider="bedrock",
model="us.anthropic.claude-haiku-4-5-20251001-v1:0",
)
print("MOCKED BATCH RESPONSE=", batch_response)
# Validate the response
assert (
batch_response.id
== "arn:aws:bedrock:us-west-2:123456789012:model-invocation-job/test-job-123"
)
assert batch_response.object == "batch"
assert (
batch_response.status == "in_progress"
) # Bedrock "InProgress" maps to "in_progress"
assert batch_response.status == "completed"
assert batch_response.endpoint == "/v1/chat/completions"
# Validate input and output file IDs in the final transformed response
assert batch_response.input_file_id == "s3://test-bucket/input/test-input.jsonl"
assert batch_response.output_file_id == "s3://test-bucket/output/"
# Bedrock returns only the output *prefix*; the handler predicts the
# actual output object as <prefix>/<job-id>/<basename(input)>.out.
assert (
batch_response.output_file_id
== "s3://test-bucket/output/test-job-123/test-input.jsonl.out"
)
def test_bedrock_batch_with_encryption_key_in_post_request():

View file

@ -104,7 +104,8 @@ async def test_text_message_blocked_by_guardrail_no_ai_response():
Send a text message containing the blocked phrase.
Guardrail must:
- Send error event (guardrail_violation) to client.
- Send response.audio_transcript.delta with the block message to client.
- Send response.output_audio_transcript.delta (or beta-protocol
response.audio_transcript.delta) with the block message to client.
- NOT forward response.create to OpenAI (no AI response).
"""
import websockets
@ -119,7 +120,6 @@ async def test_text_message_blocked_by_guardrail_no_ai_response():
OPENAI_REALTIME_URL,
additional_headers={
"Authorization": f"Bearer {OPENAI_API_KEY}",
"OpenAI-Beta": "realtime=v1",
},
) as backend_ws:
streaming, input_queue = await _build_streaming(client_events, backend_ws)
@ -182,7 +182,11 @@ async def test_text_message_blocked_by_guardrail_no_ai_response():
transcript_deltas = [
e
for e in client_events
if e.get("type") == "response.audio_transcript.delta"
if e.get("type")
in (
"response.output_audio_transcript.delta",
"response.audio_transcript.delta",
)
]
assert (
len(transcript_deltas) >= 1
@ -298,7 +302,6 @@ async def test_clean_text_message_passes_through_to_openai():
OPENAI_REALTIME_URL,
additional_headers={
"Authorization": f"Bearer {OPENAI_API_KEY}",
"OpenAI-Beta": "realtime=v1",
},
) as backend_ws:
streaming, input_queue = await _build_streaming(client_events, backend_ws)

View file

@ -36,6 +36,19 @@ def test_get_masked_api_base(logging_obj):
assert type(masked_api_base) == str
def test_post_call_serializes_dict_with_datetime(logging_obj):
import datetime
response = {
"status": "InProgress",
"submitTime": datetime.datetime(2026, 5, 11, 23, 49, 13, 132000),
}
logging_obj.post_call(original_response=response)
serialized = logging_obj.model_call_details["original_response"]
assert isinstance(serialized, str)
assert "2026-05-11" in serialized
def test_sentry_sample_rate():
existing_sample_rate = os.getenv("SENTRY_API_SAMPLE_RATE")
try: