mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-13 08:52:02 +00:00
fix(bedrock): report the embeddings endpoint from the batch job's model
Bedrock's GetModelInvocationJob has no endpoint field, so a retrieve that has no saved batch to read still answered /v1/chat/completions for an embeddings job. The answer now follows the mode of the job's modelId.
This commit is contained in:
parent
a6b9f4d894
commit
7f51cb1ce7
5 changed files with 65 additions and 7 deletions
|
|
@ -327,7 +327,10 @@ class BedrockBatchesHandler:
|
||||||
# provider uses, so model_list / env / role-assumption configs are
|
# provider uses, so model_list / env / role-assumption configs are
|
||||||
# honored. We instantiate BedrockBatchesConfig (which extends
|
# honored. We instantiate BedrockBatchesConfig (which extends
|
||||||
# BaseAWSLLM) lazily to avoid a circular import at module load.
|
# BaseAWSLLM) lazily to avoid a circular import at module load.
|
||||||
from litellm.llms.bedrock.batches.transformation import BedrockBatchesConfig
|
from litellm.llms.bedrock.batches.transformation import (
|
||||||
|
BedrockBatchesConfig,
|
||||||
|
openai_endpoint_for_bedrock_job_model,
|
||||||
|
)
|
||||||
|
|
||||||
creds: Final = BedrockBatchesConfig().resolve_credentials(AwsAuthParams.model_validate(kwargs), region)
|
creds: Final = BedrockBatchesConfig().resolve_credentials(AwsAuthParams.model_validate(kwargs), region)
|
||||||
control_plane_endpoint: Final = configured_bedrock_control_plane_endpoint(
|
control_plane_endpoint: Final = configured_bedrock_control_plane_endpoint(
|
||||||
|
|
@ -419,7 +422,7 @@ class BedrockBatchesHandler:
|
||||||
request_counts=_record_counts_from_response(response),
|
request_counts=_record_counts_from_response(response),
|
||||||
metadata=openai_batch_metadata,
|
metadata=openai_batch_metadata,
|
||||||
completion_window="24h",
|
completion_window="24h",
|
||||||
endpoint="/v1/chat/completions",
|
endpoint=openai_endpoint_for_bedrock_job_model(response.get("modelId")),
|
||||||
input_file_id=input_uri,
|
input_file_id=input_uri,
|
||||||
output_file_id=output_file_uri if openai_status == "completed" else None,
|
output_file_id=output_file_uri if openai_status == "completed" else None,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Final, Literal, cast
|
||||||
from httpx import Headers, Response
|
from httpx import Headers, Response
|
||||||
from pydantic import TypeAdapter, ValidationError
|
from pydantic import TypeAdapter, ValidationError
|
||||||
|
|
||||||
|
from litellm.exceptions import ModelNotMappedError
|
||||||
from litellm.litellm_core_utils.aws_partition import is_bedrock_arn
|
from litellm.litellm_core_utils.aws_partition import is_bedrock_arn
|
||||||
from litellm.litellm_core_utils.cloud_storage_security import (
|
from litellm.litellm_core_utils.cloud_storage_security import (
|
||||||
BEDROCK_MANAGED_S3_BATCH_PREFIX,
|
BEDROCK_MANAGED_S3_BATCH_PREFIX,
|
||||||
|
|
@ -33,6 +34,7 @@ from ..base_aws_llm import BaseAWSLLM
|
||||||
from ..common_utils import (
|
from ..common_utils import (
|
||||||
CommonBatchFilesUtils,
|
CommonBatchFilesUtils,
|
||||||
bedrock_control_plane_base_url,
|
bedrock_control_plane_base_url,
|
||||||
|
get_cached_model_info,
|
||||||
merge_bedrock_aws_request_params,
|
merge_bedrock_aws_request_params,
|
||||||
resolve_s3_bucket_owner,
|
resolve_s3_bucket_owner,
|
||||||
resolve_s3_encryption_key_id,
|
resolve_s3_encryption_key_id,
|
||||||
|
|
@ -83,6 +85,18 @@ def _validate_bedrock_tags(raw_tags: object) -> list[BedrockTag]:
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
def openai_endpoint_for_bedrock_job_model(
|
||||||
|
model_id: str | None,
|
||||||
|
) -> Literal["/v1/chat/completions", "/v1/embeddings"]:
|
||||||
|
if not model_id:
|
||||||
|
return "/v1/chat/completions"
|
||||||
|
try:
|
||||||
|
mode: Final = get_cached_model_info()(model=model_id, custom_llm_provider="bedrock").get("mode")
|
||||||
|
except ModelNotMappedError:
|
||||||
|
return "/v1/chat/completions"
|
||||||
|
return "/v1/embeddings" if mode == "embedding" else "/v1/chat/completions"
|
||||||
|
|
||||||
|
|
||||||
def titan_embedding_usage_from_batch_output(model_output: Mapping[str, object]) -> Usage | None:
|
def titan_embedding_usage_from_batch_output(model_output: Mapping[str, object]) -> Usage | None:
|
||||||
"""Titan embedding batch lines report usage as a top-level inputTextTokenCount, not a usage block."""
|
"""Titan embedding batch lines report usage as a top-level inputTextTokenCount, not a usage block."""
|
||||||
if "embedding" not in model_output and "embeddingsByType" not in model_output:
|
if "embedding" not in model_output and "embeddingsByType" not in model_output:
|
||||||
|
|
@ -641,7 +655,7 @@ class BedrockBatchesConfig(BaseAWSLLM, BaseBatchesConfig):
|
||||||
return LiteLLMBatch(
|
return LiteLLMBatch(
|
||||||
id=job_arn,
|
id=job_arn,
|
||||||
object="batch",
|
object="batch",
|
||||||
endpoint="/v1/chat/completions",
|
endpoint=openai_endpoint_for_bedrock_job_model(response_data.get("modelId")),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
input_file_id=input_file_id,
|
input_file_id=input_file_id,
|
||||||
completion_window="24h",
|
completion_window="24h",
|
||||||
|
|
|
||||||
|
|
@ -165,6 +165,24 @@ def test_handle_model_invocation_job_status_completed(patched_boto3):
|
||||||
assert batch.metadata["output_s3_uri"] == OUTPUT_PREFIX
|
assert batch.metadata["output_s3_uri"] == OUTPUT_PREFIX
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"job_model_id,expected_endpoint",
|
||||||
|
[
|
||||||
|
("amazon.titan-embed-text-v2:0", "/v1/embeddings"),
|
||||||
|
("anthropic.claude-3-5-sonnet-20240620-v1:0", "/v1/chat/completions"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_job_status_endpoint_follows_the_job_models_mode(
|
||||||
|
patched_boto3: tuple[MagicMock, MagicMock], job_model_id: str, expected_endpoint: str
|
||||||
|
) -> None:
|
||||||
|
fake_client, _ = patched_boto3
|
||||||
|
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)
|
||||||
|
|
||||||
|
assert batch.endpoint == expected_endpoint
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("success_count,error_count", [(100, 0), (86, 14)])
|
@pytest.mark.parametrize("success_count,error_count", [(100, 0), (86, 14)])
|
||||||
def test_completed_job_maps_provider_record_counts(patched_boto3, success_count, error_count):
|
def test_completed_job_maps_provider_record_counts(patched_boto3, success_count, error_count):
|
||||||
fake_client, _ = patched_boto3
|
fake_client, _ = patched_boto3
|
||||||
|
|
|
||||||
|
|
@ -918,6 +918,28 @@ def test_retrieve_response_no_errors_when_no_message(config):
|
||||||
assert out.errors is None
|
assert out.errors is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"job_model_id,expected_endpoint",
|
||||||
|
[
|
||||||
|
("amazon.titan-embed-text-v2:0", "/v1/embeddings"),
|
||||||
|
("arn:aws:bedrock:us-west-2::foundation-model/amazon.titan-embed-text-v2:0", "/v1/embeddings"),
|
||||||
|
("anthropic.claude-3-5-sonnet-20240620-v1:0", "/v1/chat/completions"),
|
||||||
|
("a-model-the-cost-map-does-not-list", "/v1/chat/completions"),
|
||||||
|
(None, "/v1/chat/completions"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_retrieve_response_endpoint_follows_the_job_models_mode(
|
||||||
|
config: BedrockBatchesConfig, job_model_id: str | None, expected_endpoint: str
|
||||||
|
) -> None:
|
||||||
|
out = config.transform_retrieve_batch_response(
|
||||||
|
model=None,
|
||||||
|
raw_response=_raw({"jobArn": ARN, "status": "InProgress", "modelId": job_model_id}),
|
||||||
|
logging_obj=MagicMock(),
|
||||||
|
litellm_params={},
|
||||||
|
)
|
||||||
|
assert out.endpoint == expected_endpoint
|
||||||
|
|
||||||
|
|
||||||
def test_retrieve_response_enriches_metadata(config):
|
def test_retrieve_response_enriches_metadata(config):
|
||||||
body = {
|
body = {
|
||||||
"jobArn": ARN,
|
"jobArn": ARN,
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ from contextlib import ExitStack
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from collections.abc import Callable
|
||||||
from typing import Any, Dict, Final, Optional
|
from typing import Any, Dict, Final, Optional
|
||||||
from unittest.mock import DEFAULT, AsyncMock, MagicMock, patch
|
from unittest.mock import DEFAULT, AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
|
@ -1909,10 +1910,10 @@ async def test_retrieve__db_non_terminal_state_syncs_with_provider(retrieve_harn
|
||||||
assert retrieve_harness.update_batch_in_db.call_count == 1
|
assert retrieve_harness.update_batch_in_db.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def _hook_rewriting_the_batch_id_to(batch_id):
|
def _hook_rewriting_the_batch_id_to(batch_id: str) -> Callable[..., LiteLLMBatch]:
|
||||||
def hook(**kwargs):
|
def hook(*, response: LiteLLMBatch, **_: object) -> LiteLLMBatch:
|
||||||
kwargs["response"].id = batch_id
|
response.id = batch_id
|
||||||
return kwargs["response"]
|
return response
|
||||||
|
|
||||||
return hook
|
return hook
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue