* 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>
267 lines
9.1 KiB
Python
267 lines
9.1 KiB
Python
# What is this?
|
|
## Unit Tests for OpenAI Batches API
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import sys
|
|
import traceback
|
|
import tempfile
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv()
|
|
sys.path.insert(
|
|
0, os.path.abspath("../..")
|
|
) # Adds the parent directory to the system-path
|
|
|
|
|
|
import pytest
|
|
from typing import Optional
|
|
import litellm
|
|
from unittest.mock import patch, MagicMock
|
|
import httpx
|
|
|
|
|
|
@pytest.mark.asyncio()
|
|
async def test_async_create_file():
|
|
"""
|
|
1. Create File for Batch completion
|
|
2. Create Batch Request
|
|
3. Retrieve the specific batch
|
|
"""
|
|
litellm._turn_on_debug()
|
|
print("Testing async create batch")
|
|
|
|
file_name = "bedrock_batch_completions.jsonl"
|
|
_current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
file_path = os.path.join(_current_dir, file_name)
|
|
file_obj = await litellm.acreate_file(
|
|
file=open(file_path, "rb"),
|
|
purpose="batch",
|
|
custom_llm_provider="bedrock",
|
|
s3_bucket_name="litellm-proxy-941277531214",
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio()
|
|
async def test_async_file_and_batch():
|
|
"""
|
|
Test file retrieval
|
|
"""
|
|
litellm._turn_on_debug()
|
|
file_name = "bedrock_batch_completions.jsonl"
|
|
_current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
file_path = os.path.join(_current_dir, file_name)
|
|
file_obj = await litellm.acreate_file(
|
|
file=open(file_path, "rb"),
|
|
purpose="batch",
|
|
custom_llm_provider="bedrock",
|
|
s3_bucket_name="litellm-proxy-941277531214",
|
|
)
|
|
print("CREATED FILE RESPONSE=", file_obj)
|
|
|
|
# create batch
|
|
create_batch_response = await litellm.acreate_batch(
|
|
completion_window="24h",
|
|
endpoint="/v1/chat/completions",
|
|
input_file_id=file_obj.id,
|
|
metadata={"key1": "value1", "key2": "value2"},
|
|
custom_llm_provider="bedrock",
|
|
#########################################################
|
|
# bedrock specific params
|
|
#########################################################
|
|
model="us.anthropic.claude-haiku-4-5-20251001-v1:0",
|
|
aws_batch_role_arn="arn:aws:iam::941277531214:role/service-role/AmazonBedrockExecutionRoleForAgents_BB9HNW6V4CV",
|
|
)
|
|
print("CREATED BATCH RESPONSE=", create_batch_response)
|
|
|
|
# retrieve batch
|
|
retrieve_batch_response = await litellm.aretrieve_batch(
|
|
batch_id=create_batch_response.id,
|
|
custom_llm_provider="bedrock",
|
|
model="us.anthropic.claude-haiku-4-5-20251001-v1:0",
|
|
)
|
|
print("RETRIEVED BATCH RESPONSE=", retrieve_batch_response)
|
|
|
|
# Validate the response
|
|
assert retrieve_batch_response.id == create_batch_response.id
|
|
assert retrieve_batch_response.object == "batch"
|
|
assert retrieve_batch_response.status in [
|
|
"validating",
|
|
"in_progress",
|
|
"completed",
|
|
"failed",
|
|
"cancelled",
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio()
|
|
async def test_mock_bedrock_file_url_mapping():
|
|
"""
|
|
Simple test to capture PUT URL and validate mapping to file ID.
|
|
"""
|
|
print("Testing Bedrock file URL mapping")
|
|
|
|
captured_put_url = None
|
|
|
|
async def mock_async_create_file(transformed_request, **kwargs):
|
|
nonlocal captured_put_url
|
|
# Capture PUT URL from transformed request
|
|
if isinstance(transformed_request, dict) and "url" in transformed_request:
|
|
captured_put_url = transformed_request["url"]
|
|
|
|
# Call the real method to get actual response
|
|
from litellm.files.main import base_llm_http_handler
|
|
|
|
return await base_llm_http_handler.__class__.async_create_file(
|
|
base_llm_http_handler, transformed_request, **kwargs
|
|
)
|
|
|
|
with patch(
|
|
"litellm.files.main.base_llm_http_handler.async_create_file",
|
|
side_effect=mock_async_create_file,
|
|
):
|
|
file_obj = await litellm.acreate_file(
|
|
file=open(
|
|
os.path.join(
|
|
os.path.dirname(__file__), "bedrock_batch_completions.jsonl"
|
|
),
|
|
"rb",
|
|
),
|
|
purpose="batch",
|
|
custom_llm_provider="bedrock",
|
|
s3_bucket_name="litellm-proxy-941277531214",
|
|
)
|
|
|
|
print(f"PUT URL: {captured_put_url}")
|
|
print(f"File ID: {file_obj.id}")
|
|
|
|
# Validate URL was captured and response is correct
|
|
assert captured_put_url is not None
|
|
assert file_obj.id.startswith("s3://")
|
|
|
|
# Verify mapping
|
|
from litellm.llms.bedrock.files.transformation import BedrockFilesConfig
|
|
|
|
bedrock_config = BedrockFilesConfig()
|
|
expected_s3_uri, _ = bedrock_config._convert_https_url_to_s3_uri(
|
|
captured_put_url
|
|
)
|
|
assert file_obj.id == expected_s3_uri
|
|
|
|
|
|
@pytest.mark.asyncio()
|
|
async def test_bedrock_retrieve_batch():
|
|
"""
|
|
Test bedrock batch retrieval functionality, validating that input and output file IDs
|
|
are correctly extracted from the Bedrock response and included in the final transformed response.
|
|
"""
|
|
print("Testing bedrock batch retrieval")
|
|
|
|
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": "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"}
|
|
},
|
|
"outputDataConfig": {
|
|
"s3OutputDataConfig": {"s3Uri": "s3://test-bucket/output/"}
|
|
},
|
|
}
|
|
|
|
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")
|
|
|
|
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",
|
|
)
|
|
|
|
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 == "completed"
|
|
assert batch_response.endpoint == "/v1/chat/completions"
|
|
|
|
assert batch_response.input_file_id == "s3://test-bucket/input/test-input.jsonl"
|
|
# 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():
|
|
"""
|
|
Test that s3_encryption_key_id is included in the AWS POST request payload.
|
|
"""
|
|
import json
|
|
import litellm
|
|
|
|
test_kms_key_id = (
|
|
"arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012"
|
|
)
|
|
|
|
captured_request_body = None
|
|
|
|
def mock_post(*args, **kwargs):
|
|
nonlocal captured_request_body
|
|
if "data" in kwargs:
|
|
captured_request_body = kwargs["data"]
|
|
|
|
mock_response = MagicMock()
|
|
mock_response.json.return_value = {
|
|
"jobArn": "arn:aws:bedrock:us-west-2:123456789012:model-invocation-job/test-job",
|
|
"jobName": "test-job",
|
|
"status": "Submitted",
|
|
}
|
|
mock_response.status_code = 200
|
|
mock_response.raise_for_status.return_value = None
|
|
return mock_response
|
|
|
|
with patch(
|
|
"litellm.llms.custom_httpx.http_handler.HTTPHandler.post", side_effect=mock_post
|
|
):
|
|
response = litellm.create_batch(
|
|
completion_window="24h",
|
|
endpoint="/v1/chat/completions",
|
|
input_file_id="s3://test-bucket/input/test.jsonl",
|
|
custom_llm_provider="bedrock",
|
|
model="us.anthropic.claude-haiku-4-5-20251001-v1:0",
|
|
s3_encryption_key_id=test_kms_key_id,
|
|
aws_batch_role_arn="arn:aws:iam::123456789012:role/test-role",
|
|
)
|
|
|
|
assert captured_request_body is not None, "Request body was not captured"
|
|
|
|
request_data = json.loads(captured_request_body)
|
|
print("REQUEST DATA to bedrock batch creation", json.dumps(request_data, indent=4))
|
|
|
|
assert "outputDataConfig" in request_data
|
|
assert "s3OutputDataConfig" in request_data["outputDataConfig"]
|
|
assert "s3EncryptionKeyId" in request_data["outputDataConfig"]["s3OutputDataConfig"]
|
|
assert (
|
|
request_data["outputDataConfig"]["s3OutputDataConfig"]["s3EncryptionKeyId"]
|
|
== test_kms_key_id
|
|
)
|
|
|
|
print("SUCCESS: s3_encryption_key_id properly included in AWS POST request")
|