fix(bedrock): correct a saved chat endpoint on embeddings batches and cover Cohere Embed v3

A Bedrock answer of /v1/embeddings now replaces a saved /v1/chat/completions, so a batch read before the upgrade or by an old pod during a rolling upgrade reports the right endpoint on its next provider sync. The two Cohere Embed v3 cost-map rows gain output_vector_size so they stay embeddings when a batch deployment overrides the model's mode
This commit is contained in:
mateo-berri 2026-11-10 08:56:30 -08:00
parent d54a31f1a8
commit 2617ad780e
6 changed files with 39 additions and 6 deletions

View file

@ -17536,6 +17536,7 @@
"max_input_tokens": 512, "max_input_tokens": 512,
"mode": "embedding", "mode": "embedding",
"output_cost_per_token": 0.0, "output_cost_per_token": 0.0,
"output_vector_size": 1024,
"source": "https://aws.amazon.com/bedrock/pricing/", "source": "https://aws.amazon.com/bedrock/pricing/",
"supports_embedding_image_input": true "supports_embedding_image_input": true
}, },
@ -17545,6 +17546,7 @@
"max_input_tokens": 512, "max_input_tokens": 512,
"mode": "embedding", "mode": "embedding",
"output_cost_per_token": 0.0, "output_cost_per_token": 0.0,
"output_vector_size": 1024,
"source": "https://aws.amazon.com/bedrock/pricing/", "source": "https://aws.amazon.com/bedrock/pricing/",
"supports_embedding_image_input": true "supports_embedding_image_input": true
}, },

View file

@ -1608,7 +1608,10 @@ def batch_cost_poller_is_active() -> bool:
def created_batch_endpoint(provider_batch: "LiteLLMBatch", stored_batch: "LiteLLMBatch | None") -> str: def created_batch_endpoint(provider_batch: "LiteLLMBatch", stored_batch: "LiteLLMBatch | None") -> str:
return stored_batch.endpoint if stored_batch is not None and stored_batch.endpoint else provider_batch.endpoint if stored_batch is None or not stored_batch.endpoint:
return provider_batch.endpoint
provider_named_the_endpoint: Final = provider_batch.endpoint not in ("", "/v1/chat/completions")
return provider_batch.endpoint if provider_named_the_endpoint else stored_batch.endpoint
def _completed_batch_safe_to_retire(response: "LiteLLMBatch") -> bool: def _completed_batch_safe_to_retire(response: "LiteLLMBatch") -> bool:

View file

@ -17536,6 +17536,7 @@
"max_input_tokens": 512, "max_input_tokens": 512,
"mode": "embedding", "mode": "embedding",
"output_cost_per_token": 0.0, "output_cost_per_token": 0.0,
"output_vector_size": 1024,
"source": "https://aws.amazon.com/bedrock/pricing/", "source": "https://aws.amazon.com/bedrock/pricing/",
"supports_embedding_image_input": true "supports_embedding_image_input": true
}, },
@ -17545,6 +17546,7 @@
"max_input_tokens": 512, "max_input_tokens": 512,
"mode": "embedding", "mode": "embedding",
"output_cost_per_token": 0.0, "output_cost_per_token": 0.0,
"output_vector_size": 1024,
"source": "https://aws.amazon.com/bedrock/pricing/", "source": "https://aws.amazon.com/bedrock/pricing/",
"supports_embedding_image_input": true "supports_embedding_image_input": true
}, },

View file

@ -178,7 +178,7 @@ def test_job_status_endpoint_follows_the_job_models_mode(
fake_client, _ = patched_boto3 fake_client, _ = patched_boto3
fake_client.get_model_invocation_job.return_value = {**_fake_boto3_response(), "modelId": job_model_id} fake_client.get_model_invocation_job.return_value = {**_fake_boto3_response(), "modelId": job_model_id}
batch = BedrockBatchesHandler.handle_model_invocation_job_status(batch_id=JOB_ARN) batch: Final = BedrockBatchesHandler.handle_model_invocation_job_status(batch_id=JOB_ARN)
assert batch.endpoint == expected_endpoint assert batch.endpoint == expected_endpoint

View file

@ -14,6 +14,7 @@ URL/ARN handling, and the error class. AWS auth/sigv4 is the only external seam
we mock; everything else runs for real. we mock; everything else runs for real.
""" """
from collections.abc import Mapping
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Final from typing import Final
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@ -932,7 +933,7 @@ def test_retrieve_response_no_errors_when_no_message(config):
def test_retrieve_response_endpoint_follows_the_job_models_mode( def test_retrieve_response_endpoint_follows_the_job_models_mode(
config: BedrockBatchesConfig, job_model_id: str | None, expected_endpoint: str config: BedrockBatchesConfig, job_model_id: str | None, expected_endpoint: str
) -> None: ) -> None:
out = config.transform_retrieve_batch_response( out: Final = config.transform_retrieve_batch_response(
model=None, model=None,
raw_response=_raw({"jobArn": ARN, "status": "InProgress", "modelId": job_model_id}), raw_response=_raw({"jobArn": ARN, "status": "InProgress", "modelId": job_model_id}),
logging_obj=MagicMock(), logging_obj=MagicMock(),
@ -941,12 +942,19 @@ def test_retrieve_response_endpoint_follows_the_job_models_mode(
assert out.endpoint == expected_endpoint assert out.endpoint == expected_endpoint
def _is_bedrock_embedding_model(cost_map_entry: Mapping[str, object]) -> bool:
return cost_map_entry.get("litellm_provider") == "bedrock" and cost_map_entry.get("mode") == "embedding"
@pytest.mark.parametrize(
"job_model_id",
sorted(model_id for model_id, entry in litellm.model_cost.items() if _is_bedrock_embedding_model(entry)),
)
def test_retrieve_response_endpoint_survives_a_batch_deployment_overriding_the_models_mode( def test_retrieve_response_endpoint_survives_a_batch_deployment_overriding_the_models_mode(
config: BedrockBatchesConfig, monkeypatch: pytest.MonkeyPatch config: BedrockBatchesConfig, monkeypatch: pytest.MonkeyPatch, job_model_id: str
) -> None: ) -> None:
job_model_id = "amazon.titan-embed-text-v2:0"
monkeypatch.setitem(litellm.model_cost, job_model_id, {**litellm.model_cost[job_model_id], "mode": "batch"}) monkeypatch.setitem(litellm.model_cost, job_model_id, {**litellm.model_cost[job_model_id], "mode": "batch"})
out = config.transform_retrieve_batch_response( out: Final = config.transform_retrieve_batch_response(
model=None, model=None,
raw_response=_raw({"jobArn": ARN, "status": "InProgress", "modelId": job_model_id}), raw_response=_raw({"jobArn": ARN, "status": "InProgress", "modelId": job_model_id}),
logging_obj=MagicMock(), logging_obj=MagicMock(),

View file

@ -1952,6 +1952,24 @@ async def test_retrieve__a_saved_batch_without_an_endpoint_keeps_the_providers(r
assert retrieve_harness.update_batch_in_db.call_args.kwargs["response"].endpoint == "/v1/embeddings" assert retrieve_harness.update_batch_in_db.call_args.kwargs["response"].endpoint == "/v1/embeddings"
@pytest.mark.asyncio
async def test_retrieve__an_embeddings_endpoint_from_the_provider_corrects_a_saved_chat_endpoint(retrieve_harness):
stored = make_batch(id="batch-unified-blob", status="validating").model_copy(
update={"endpoint": "/v1/chat/completions"}
)
provider_batch = make_batch(id="batch-raw-xyz", status="in_progress").model_copy(
update={"endpoint": "/v1/embeddings"}
)
retrieve_harness.get_batch_from_db.return_value = (MagicMock(status="validating"), stored)
retrieve_harness.router.aretrieve_batch.return_value = provider_batch
with patch.object(endpoints, "is_base64_encoded_unified_file_id", return_value=UNIFIED_BATCH_ID):
resp = await call_retrieve(retrieve_harness, "batch-unified-blob")
assert resp.endpoint == "/v1/embeddings"
assert retrieve_harness.update_batch_in_db.call_args.kwargs["response"].endpoint == "/v1/embeddings"
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize("status", ["validating", "in_progress", "finalizing", "cancelling"]) @pytest.mark.parametrize("status", ["validating", "in_progress", "finalizing", "cancelling"])
async def test_retrieve__executed_batch_served_from_db_in_every_status(retrieve_harness, status): async def test_retrieve__executed_batch_served_from_db_in_every_status(retrieve_harness, status):