mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-29 06:33:28 +00:00
fix(bedrock): adapt native output backports to release interfaces
This commit is contained in:
parent
3bd1854175
commit
e7d432a328
22 changed files with 530 additions and 858 deletions
|
|
@ -1,3 +1,5 @@
|
|||
from pydantic import TypeAdapter
|
||||
|
||||
"""
|
||||
Common utility functions used for translating messages across providers
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1382,6 +1382,11 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
return None
|
||||
return AnthropicConfig.filter_anthropic_output_schema(dict(json_schema))
|
||||
|
||||
def uses_native_structured_output(self, model: str) -> bool:
|
||||
return AnthropicConfig._supports_model_capability(
|
||||
model, "supports_native_structured_output", self._resolved_provider
|
||||
)
|
||||
|
||||
def uses_native_structured_output_for_schema(
|
||||
self,
|
||||
model: str,
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ from litellm.llms.anthropic.chat.transformation import (
|
|||
from litellm.llms.anthropic.common_utils import AnthropicModelInfo
|
||||
from litellm.llms.base_llm.chat.transformation import REQUEST_PARAMS, BaseConfig, BaseLLMException
|
||||
from litellm.llms.bedrock.common_utils import (
|
||||
bedrock_capability_model,
|
||||
bedrock_chat_uses_native_structured_output,
|
||||
bedrock_cleaned_output_schema,
|
||||
bedrock_native_output_format,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
import httpx
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
from litellm.anthropic_beta_headers_manager import filter_and_transform_beta_headers
|
||||
from litellm.constants import RESPONSE_FORMAT_TOOL_NAME
|
||||
|
|
@ -107,18 +109,7 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig):
|
|||
model: str,
|
||||
drop_params: bool,
|
||||
) -> dict:
|
||||
# Force tool-based structured outputs for Bedrock Invoke
|
||||
# (similar to VertexAI fix in #19201) unless the model map advertises
|
||||
# native structured output
|
||||
from litellm.utils import supports_native_structured_output
|
||||
|
||||
original_model: Final = model
|
||||
if "response_format" in non_default_params and not supports_native_structured_output(
|
||||
model=model, custom_llm_provider="bedrock"
|
||||
):
|
||||
# Use a model name that forces tool-based approach
|
||||
model = "claude-3-sonnet-20240229"
|
||||
|
||||
# Clamp ``reasoning_effort`` to the Bedrock effort ceiling before the
|
||||
# parent mapping converts it to ``output_config.effort`` and the
|
||||
# downstream effort gate runs. Mirrors the converse path's
|
||||
|
|
|
|||
|
|
@ -954,6 +954,12 @@ def is_bedrock_application_inference_profile_arn(model: str) -> bool:
|
|||
return ":application-inference-profile/" in model
|
||||
|
||||
|
||||
def bedrock_capability_model(model: str, base_model: object) -> str:
|
||||
if isinstance(base_model, str) and base_model and is_bedrock_application_inference_profile_arn(model):
|
||||
return strip_bedrock_routing_prefix(base_model)
|
||||
return model
|
||||
|
||||
|
||||
def strip_bedrock_routing_prefix(model: str) -> str:
|
||||
"""Strip LiteLLM routing prefixes from model name."""
|
||||
for prefix in ["bedrock/", "converse/", "invoke/", "openai/", "mantle/", "nova-2/", "nova/"]:
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ from litellm.llms.bedrock.common_utils import (
|
|||
bedrock_supports_tool_search,
|
||||
ensure_bedrock_anthropic_messages_tool_names,
|
||||
get_anthropic_beta_from_headers,
|
||||
is_claude_4_5_on_bedrock,
|
||||
is_bedrock_native_output_rejection,
|
||||
is_claude_4_5_on_bedrock,
|
||||
normalize_bedrock_opus_output_config_effort,
|
||||
normalize_custom_field_on_tools,
|
||||
normalize_tool_input_schema_types_for_bedrock_invoke,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import httpx
|
|||
from httpx import USE_CLIENT_DEFAULT
|
||||
from httpx._types import FileContent
|
||||
from openai.types.file_deleted import FileDeleted
|
||||
from pydantic import TypeAdapter
|
||||
from typing_extensions import ReadOnly
|
||||
|
||||
import litellm
|
||||
|
|
@ -2185,7 +2186,9 @@ class BaseLLMHTTPHandler:
|
|||
|
||||
native_output_fallback_params: Final = anthropic_messages_provider_config.native_output_fallback_litellm_params(
|
||||
model=model,
|
||||
anthropic_messages_optional_request_params=_OPTIONAL_PARAMS.validate_python(anthropic_messages_optional_request_params),
|
||||
anthropic_messages_optional_request_params=_OPTIONAL_PARAMS.validate_python(
|
||||
anthropic_messages_optional_request_params
|
||||
),
|
||||
litellm_params=litellm_params,
|
||||
)
|
||||
rebuild_without_native_output: Final = (
|
||||
|
|
@ -2195,7 +2198,9 @@ class BaseLLMHTTPHandler:
|
|||
anthropic_messages_provider_config.transform_anthropic_messages_request,
|
||||
model=model,
|
||||
messages=copy.deepcopy(_JSON_OBJECTS.validate_python(messages)),
|
||||
anthropic_messages_optional_request_params=copy.deepcopy(_JSON_OBJECT.validate_python(anthropic_messages_optional_request_params)),
|
||||
anthropic_messages_optional_request_params=copy.deepcopy(
|
||||
_JSON_OBJECT.validate_python(anthropic_messages_optional_request_params)
|
||||
),
|
||||
litellm_params=native_output_fallback_params,
|
||||
headers=_JSON_OBJECT.validate_python(headers),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,11 +24,11 @@ import pytest
|
|||
import yaml
|
||||
from integration._support.client import Gateway, Scenario, eventually, list_value, object_value, string_value
|
||||
from integration._support.database import read_rows
|
||||
from integration._support.ports import allocate_port
|
||||
from integration._support.process import OwnedProxy, graceful_stop_seconds, owned_proxy_process
|
||||
from integration._support.upstream import aws_event_stream_frame
|
||||
from integration._support.vertex import service_account_json
|
||||
from integration._support.wire import Reply, Request, Wire, wire_server
|
||||
from integration._support.ports import allocate_port
|
||||
from pydantic import JsonValue, TypeAdapter
|
||||
|
||||
_JSON_TOOL: Final = "json_tool_call"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ from pydantic import JsonValue, TypeAdapter
|
|||
|
||||
import litellm
|
||||
from litellm import ModelResponse, Router
|
||||
from litellm.utils import _invalidate_model_cost_lowercase_map # pyright: ignore[reportPrivateUsage] # required cache invalidation
|
||||
from litellm.utils import (
|
||||
_invalidate_model_cost_lowercase_map, # pyright: ignore[reportPrivateUsage] # required cache invalidation
|
||||
)
|
||||
|
||||
_OPUS_4_8: Final = "us.anthropic.claude-opus-4-8"
|
||||
_JSON_TOOL: Final = "json_tool_call"
|
||||
|
|
|
|||
|
|
@ -239,7 +239,9 @@ def isolate_litellm_globals() -> Iterator[None]:
|
|||
original_callbacks: Final = {name: list(getattr(litellm, name) or []) for name in CALLBACK_LISTS}
|
||||
original_reset: Final = {name: getattr(litellm, name) for name in RESET_TO_NONE_GLOBALS}
|
||||
original_restored: Final = {name: getattr(litellm, name) for name in RESTORED_GLOBALS if hasattr(litellm, name)}
|
||||
original_clients: Final = {name: litellm.__dict__[name] for name in MODULE_LEVEL_CLIENTS if name in litellm.__dict__}
|
||||
original_clients: Final = {
|
||||
name: litellm.__dict__[name] for name in MODULE_LEVEL_CLIENTS if name in litellm.__dict__
|
||||
}
|
||||
original_loggers: Final = {
|
||||
logger: (logger.level, logger.disabled, logger.propagate, list(logger.handlers), list(logger.filters))
|
||||
for logger in ALL_LOGGERS
|
||||
|
|
|
|||
|
|
@ -1988,23 +1988,66 @@ def _object_with(properties: Mapping[str, object], **defs: Mapping[str, object])
|
|||
("schema", "unresolvable"),
|
||||
[
|
||||
(_object_with({"a": {"type": "string"}}), False),
|
||||
(_object_with({"a": {"$ref": "#/$defs/A"}, "b": {"$ref": "#/$defs/A"}}, **{"$defs": {"A": {"type": "string"}}}), False),
|
||||
(_object_with({"a": {"$ref": "#/definitions/A"}}, definitions={"A": {"$ref": "#/definitions/B"}, "B": {"type": "integer"}}), False),
|
||||
(_object_with({"a": {"$ref": "/$defs/A"}}, **{"$defs": {"A": {"items": {"$ref": "/$defs/B"}}, "B": {"type": "string"}}}), False),
|
||||
(
|
||||
_object_with(
|
||||
{"a": {"$ref": "#/$defs/A"}, "b": {"$ref": "#/$defs/A"}}, **{"$defs": {"A": {"type": "string"}}}
|
||||
),
|
||||
False,
|
||||
),
|
||||
(
|
||||
_object_with(
|
||||
{"a": {"$ref": "#/definitions/A"}},
|
||||
definitions={"A": {"$ref": "#/definitions/B"}, "B": {"type": "integer"}},
|
||||
),
|
||||
False,
|
||||
),
|
||||
(
|
||||
_object_with(
|
||||
{"a": {"$ref": "/$defs/A"}},
|
||||
**{"$defs": {"A": {"items": {"$ref": "/$defs/B"}}, "B": {"type": "string"}}},
|
||||
),
|
||||
False,
|
||||
),
|
||||
(_object_with({"a": {"$ref": "#/$defs/Missing"}}, **{"$defs": {"A": {"type": "string"}}}), True),
|
||||
(_object_with({"a": {"$ref": "other.json#/A"}}), True),
|
||||
(_object_with({"parent": {"$ref": "#"}}), True),
|
||||
(_object_with({"a": {"$ref": "#/$defs/A"}}, **{"$defs": {"A": {"type": "array", "items": {"$ref": "#/$defs/A"}}}}), True),
|
||||
(_object_with({"a": {"$ref": "/$defs/A"}}, **{"$defs": {"A": {"type": "array", "items": {"$ref": "/$defs/A"}}}}), True),
|
||||
(
|
||||
_object_with(
|
||||
{"a": {"$ref": "#/$defs/A"}}, **{"$defs": {"A": {"type": "array", "items": {"$ref": "#/$defs/A"}}}}
|
||||
),
|
||||
True,
|
||||
),
|
||||
(
|
||||
_object_with(
|
||||
{"a": {"$ref": "/$defs/A"}}, **{"$defs": {"A": {"type": "array", "items": {"$ref": "/$defs/A"}}}}
|
||||
),
|
||||
True,
|
||||
),
|
||||
(
|
||||
_object_with(
|
||||
{"a": {"$ref": "#/$defs/A"}},
|
||||
**{"$defs": {"A": {"properties": {"b": {"$ref": "#/$defs/B"}}}, "B": {"properties": {"a": {"$ref": "#/$defs/A"}}}}},
|
||||
**{
|
||||
"$defs": {
|
||||
"A": {"properties": {"b": {"$ref": "#/$defs/B"}}},
|
||||
"B": {"properties": {"a": {"$ref": "#/$defs/A"}}},
|
||||
}
|
||||
},
|
||||
),
|
||||
True,
|
||||
),
|
||||
],
|
||||
ids=["no-refs", "shared-def", "chained-definitions", "root-path-defs", "missing-def", "external", "root", "self-cycle", "root-path-self-cycle", "mutual-cycle"],
|
||||
ids=[
|
||||
"no-refs",
|
||||
"shared-def",
|
||||
"chained-definitions",
|
||||
"root-path-defs",
|
||||
"missing-def",
|
||||
"external",
|
||||
"root",
|
||||
"self-cycle",
|
||||
"root-path-self-cycle",
|
||||
"mutual-cycle",
|
||||
],
|
||||
)
|
||||
def test_schema_has_unresolvable_ref_flags_refs_that_cannot_be_inlined(schema, unresolvable):
|
||||
assert schema_has_unresolvable_ref(schema) is unresolvable
|
||||
|
|
@ -2013,7 +2056,11 @@ def test_schema_has_unresolvable_ref_flags_refs_that_cannot_be_inlined(schema, u
|
|||
@pytest.mark.parametrize(
|
||||
("schema", "properties", "branches"),
|
||||
[
|
||||
(_object_with({"a": {"type": "string"}, "b": {"type": "string"}, "c": {"type": "string"}}, required=["a"]), 3, 8),
|
||||
(
|
||||
_object_with({"a": {"type": "string"}, "b": {"type": "string"}, "c": {"type": "string"}}, required=["a"]),
|
||||
3,
|
||||
8,
|
||||
),
|
||||
(
|
||||
_object_with(
|
||||
{"x": {"$ref": "#/$defs/Pair"}, "y": {"$ref": "#/$defs/Pair"}},
|
||||
|
|
@ -2030,4 +2077,3 @@ def test_schema_has_unresolvable_ref_flags_refs_that_cannot_be_inlined(schema, u
|
|||
def test_schema_size_counts_every_written_object_once(schema, properties, branches):
|
||||
assert schema_property_count(schema) == properties
|
||||
assert schema_grammar_branch_count(schema) == branches
|
||||
|
||||
|
|
|
|||
|
|
@ -8,15 +8,19 @@ from unittest.mock import MagicMock, patch
|
|||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import get_completion_messages
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
BEDROCK_DOCUMENT_PLACEHOLDER_TEXT,
|
||||
THOUGHT_SIGNATURE_SEPARATOR,
|
||||
BedrockConverseMessagesProcessor,
|
||||
BedrockImageProcessor,
|
||||
_bedrock_converse_messages_pt,
|
||||
_bedrock_tools_pt,
|
||||
_rename_duplicate_bedrock_document_names,
|
||||
_convert_to_bedrock_tool_call_invoke,
|
||||
_convert_to_bedrock_tool_call_result,
|
||||
_rename_duplicate_bedrock_document_names,
|
||||
_sanitize_anthropic_tool_use_id,
|
||||
anthropic_messages_pt,
|
||||
convert_to_anthropic_tool_result,
|
||||
convert_to_gemini_tool_call_result,
|
||||
|
|
@ -73,6 +77,9 @@ def test_ollama_pt_consecutive_user_messages():
|
|||
assert result["prompt"] == expected_prompt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_anthropic_bedrock_thinking_blocks_with_none_content():
|
||||
"""
|
||||
|
|
@ -2008,8 +2015,8 @@ def test_bedrock_tools_unpack_defs_no_oom_with_nested_refs():
|
|||
This test creates a schema with multiple nested $defs that reference each other
|
||||
to verify the fix prevents memory explosion while still correctly resolving refs.
|
||||
"""
|
||||
import sys
|
||||
import copy
|
||||
import sys
|
||||
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import _bedrock_tools_pt
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from unittest.mock import MagicMock, patch
|
|||
import httpx
|
||||
import pytest
|
||||
import respx
|
||||
from httpx import Headers
|
||||
|
||||
import litellm
|
||||
from litellm.constants import (
|
||||
|
|
@ -1331,7 +1332,6 @@ def test_dict_fields_take_the_json_tool_on_a_native_structured_output_model(fiel
|
|||
assert ([tool["name"] for tool in optional_params.get("tools", [])] == ["json_tool_call"]) is not native
|
||||
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("model", "thinking_params"),
|
||||
[
|
||||
|
|
@ -1344,7 +1344,13 @@ def test_dict_fields_take_the_json_tool_on_a_native_structured_output_model(fiel
|
|||
{"tools": [{"type": "function", "function": {"name": "lookup", "parameters": {"type": "object"}}}]},
|
||||
),
|
||||
],
|
||||
ids=["adaptive-thinking", "reasoning-effort", "reasoning-effort-none", "model-without-forced-tools", "caller-tools"],
|
||||
ids=[
|
||||
"adaptive-thinking",
|
||||
"reasoning-effort",
|
||||
"reasoning-effort-none",
|
||||
"model-without-forced-tools",
|
||||
"caller-tools",
|
||||
],
|
||||
)
|
||||
def test_dict_fields_keep_native_output_when_the_json_tool_would_not_be_forced(model, thinking_params):
|
||||
optional_params: Final = AnthropicConfig().map_openai_params(
|
||||
|
|
@ -1370,6 +1376,7 @@ def test_dict_fields_keep_native_output_when_the_json_tool_would_not_be_forced(m
|
|||
assert "output_format" in optional_params
|
||||
assert "json_tool_call" not in [tool["name"] for tool in optional_params.get("tools", [])]
|
||||
|
||||
|
||||
def test_native_structured_output_uses_bundled_capability_when_remote_map_lags(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
|
|
@ -6868,6 +6875,9 @@ def test_chat_dummy_tool_result_for_an_orphaned_tool_call_replays_a_byte_identic
|
|||
assert requests[0]["messages"][2]["content"][0]["type"] == "tool_result"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def test_json_mode_keeps_the_thinking_that_preceded_the_json_tool_call():
|
||||
thinking_block: Final = {"type": "thinking", "thinking": "The user wants a greeting.", "signature": "sig-1"}
|
||||
body: Final = _json_mode_message_body("tool_use", None)
|
||||
|
|
@ -6878,3 +6888,27 @@ def test_json_mode_keeps_the_thinking_that_preceded_the_json_tool_call():
|
|||
assert message.reasoning_content == thinking_block["thinking"]
|
||||
assert [block["signature"] for block in message.thinking_blocks] == [thinking_block["signature"]]
|
||||
|
||||
|
||||
def _json_mode_message_body(stop_reason: str, stop_details: dict | None) -> dict:
|
||||
return {
|
||||
"id": "msg_json_mode",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"model": "claude-fable-5",
|
||||
"stop_reason": stop_reason,
|
||||
"stop_details": stop_details,
|
||||
"usage": {"input_tokens": 12, "output_tokens": 8},
|
||||
"content": [
|
||||
{"type": "tool_use", "id": "toolu_json", "name": RESPONSE_FORMAT_TOOL_NAME, "input": {"answer": "Hello"}}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _transform_json_mode_body(body: dict) -> litellm.ModelResponse:
|
||||
return AnthropicConfig().transform_parsed_response(
|
||||
completion_response=body,
|
||||
raw_response=httpx.Response(status_code=200, headers={}),
|
||||
model_response=litellm.ModelResponse(),
|
||||
json_mode=True,
|
||||
prefix_prompt=None,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ Related to: https://platform.claude.com/docs/en/build-with-claude/structured-out
|
|||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from litellm.llms.anthropic.chat.transformation import AnthropicConfig
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import asyncio
|
||||
import base64
|
||||
import copy
|
||||
import importlib
|
||||
import json
|
||||
import uuid
|
||||
from types import SimpleNamespace
|
||||
from collections.abc import Mapping
|
||||
from types import SimpleNamespace
|
||||
from typing import Final
|
||||
from unittest.mock import patch
|
||||
|
||||
|
|
@ -674,7 +675,6 @@ def test_shared_defs_reach_native_output_once_instead_of_inlined_per_use(set_nat
|
|||
assert "tools" not in result
|
||||
|
||||
|
||||
|
||||
class Relabel(BaseModel):
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
|
|
@ -711,6 +711,7 @@ def test_nested_pydantic_response_format_reaches_native_output_with_refs_that_re
|
|||
assert item_ref.removeprefix("#/$defs/") in sent["$defs"]
|
||||
assert "tools" not in result
|
||||
|
||||
|
||||
def test_output_config_format_forwarded_for_bedrock_chat_invoke_request(local_model_cost_map):
|
||||
"""Bedrock Invoke chat path forwards ``output_config.format`` alongside effort
|
||||
for models with native structured-output support (Claude Sonnet 4.6)."""
|
||||
|
|
@ -1266,6 +1267,9 @@ def test_chat_flagged_model_replays_a_byte_identical_prefix_around_a_mid_convers
|
|||
assert [m["role"] for m in requests[2]["messages"]] == ["user", "assistant", "user", "system", "assistant", "user"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
_CITY_SCHEMA: Final = {
|
||||
"type": "object",
|
||||
"properties": {"city": {"type": "string"}},
|
||||
|
|
@ -1326,4 +1330,3 @@ def test_invoke_uses_opt_in_native_output_is_false_when_native_output_did_not_co
|
|||
assert not AmazonAnthropicClaudeConfig().uses_opt_in_native_output(
|
||||
model, optional_params, {"model_info": model_info}
|
||||
)
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -34,9 +34,7 @@ class TestBedrockFilesTransformation:
|
|||
transformation = BedrockJsonlFilesTransformation()
|
||||
|
||||
# Load input JSONL file
|
||||
input_file_path = os.path.join(
|
||||
os.path.dirname(__file__), "input_batch_completions.jsonl"
|
||||
)
|
||||
input_file_path = os.path.join(os.path.dirname(__file__), "input_batch_completions.jsonl")
|
||||
|
||||
# Read and parse the JSONL content
|
||||
openai_jsonl_content = []
|
||||
|
|
@ -124,28 +122,20 @@ class TestBedrockFilesTransformation:
|
|||
model_input = record["modelInput"]
|
||||
|
||||
# Must have inferenceConfig with maxTokens, NOT top-level max_tokens
|
||||
assert (
|
||||
"inferenceConfig" in model_input
|
||||
), "Nova modelInput must contain inferenceConfig"
|
||||
assert "inferenceConfig" in model_input, "Nova modelInput must contain inferenceConfig"
|
||||
assert model_input["inferenceConfig"]["maxTokens"] == 50
|
||||
assert model_input["inferenceConfig"]["temperature"] == 0.7
|
||||
assert (
|
||||
"max_tokens" not in model_input
|
||||
), "max_tokens must NOT be at the top level for Nova"
|
||||
assert (
|
||||
"temperature" not in model_input
|
||||
), "temperature must NOT be at the top level for Nova"
|
||||
assert "max_tokens" not in model_input, "max_tokens must NOT be at the top level for Nova"
|
||||
assert "temperature" not in model_input, "temperature must NOT be at the top level for Nova"
|
||||
|
||||
# Must have messages
|
||||
assert "messages" in model_input
|
||||
|
||||
# Nova Pro rejects empty additionalModelRequestFields / system — they must be absent
|
||||
assert (
|
||||
"additionalModelRequestFields" not in model_input
|
||||
), "Nova: empty additionalModelRequestFields must be omitted, not serialized as {}"
|
||||
assert (
|
||||
"system" not in model_input
|
||||
), "Nova: empty system must be omitted, not serialized as []"
|
||||
assert "additionalModelRequestFields" not in model_input, (
|
||||
"Nova: empty additionalModelRequestFields must be omitted, not serialized as {}"
|
||||
)
|
||||
assert "system" not in model_input, "Nova: empty system must be omitted, not serialized as []"
|
||||
|
||||
def test_nova_batch_jsonl_omits_empty_converse_fields(self):
|
||||
"""
|
||||
|
|
@ -218,10 +208,7 @@ class TestBedrockFilesTransformation:
|
|||
config = BedrockFilesConfig()
|
||||
|
||||
# 1x1 transparent PNG
|
||||
img_b64 = (
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4"
|
||||
"2mP8z8BQDwADhQGAWjR9awAAAABJRU5ErkJggg=="
|
||||
)
|
||||
img_b64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwADhQGAWjR9awAAAABJRU5ErkJggg=="
|
||||
|
||||
openai_jsonl_content = [
|
||||
{
|
||||
|
|
@ -237,9 +224,7 @@ class TestBedrockFilesTransformation:
|
|||
{"type": "text", "text": "Describe this image."},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": "data:image/png;base64," + img_b64
|
||||
},
|
||||
"image_url": {"url": "data:image/png;base64," + img_b64},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
@ -277,16 +262,10 @@ class TestBedrockFilesTransformation:
|
|||
# Verify Converse image format
|
||||
assert "format" in block["image"], "Image block must have format field"
|
||||
assert "source" in block["image"], "Image block must have source field"
|
||||
assert (
|
||||
"bytes" in block["image"]["source"]
|
||||
), "Image source must have bytes field"
|
||||
assert "bytes" in block["image"]["source"], "Image source must have bytes field"
|
||||
# Must NOT have OpenAI-style image_url
|
||||
assert (
|
||||
"image_url" not in block
|
||||
), "image_url must not appear in Converse format"
|
||||
assert (
|
||||
block.get("type") != "image_url"
|
||||
), "type=image_url must not appear in Converse format"
|
||||
assert "image_url" not in block, "image_url must not appear in Converse format"
|
||||
assert block.get("type") != "image_url", "type=image_url must not appear in Converse format"
|
||||
|
||||
assert has_text, "Should have a text content block"
|
||||
assert has_image, "Should have an image content block"
|
||||
|
|
@ -371,9 +350,7 @@ class TestBedrockFilesTransformation:
|
|||
)
|
||||
|
||||
assert "us-gov-west-1" in url, f"Expected us-gov-west-1 in URL but got: {url}"
|
||||
assert (
|
||||
"us-west-2" not in url
|
||||
), f"us-west-2 must not appear when s3_region_name is set, got: {url}"
|
||||
assert "us-west-2" not in url, f"us-west-2 must not appear when s3_region_name is set, got: {url}"
|
||||
assert "litellm-batch-352026" in url
|
||||
|
||||
def test_get_complete_file_url_sanitizes_untrusted_filename(self):
|
||||
|
|
@ -405,9 +382,7 @@ class TestBedrockFilesTransformation:
|
|||
from litellm.llms.bedrock.files.transformation import BedrockFilesConfig
|
||||
|
||||
config = BedrockFilesConfig()
|
||||
object_name = config._get_s3_object_name_from_batch_jsonl(
|
||||
[{"body": {"model": "bedrock/../../secret:model"}}]
|
||||
)
|
||||
object_name = config._get_s3_object_name_from_batch_jsonl([{"body": {"model": "bedrock/../../secret:model"}}])
|
||||
|
||||
assert object_name.startswith("litellm-bedrock-files-")
|
||||
assert object_name.endswith(".jsonl")
|
||||
|
|
@ -462,9 +437,9 @@ class TestBedrockFilesTransformation:
|
|||
litellm_params=litellm_params,
|
||||
)
|
||||
|
||||
assert (
|
||||
captured_optional_params.get("aws_region_name") == "us-gov-west-1"
|
||||
), "s3_region_name must be forwarded as aws_region_name for SigV4 signing"
|
||||
assert captured_optional_params.get("aws_region_name") == "us-gov-west-1", (
|
||||
"s3_region_name must be forwarded as aws_region_name for SigV4 signing"
|
||||
)
|
||||
|
||||
def test_s3_region_name_wins_over_aws_region_name_for_signing(self):
|
||||
"""
|
||||
|
|
@ -518,9 +493,9 @@ class TestBedrockFilesTransformation:
|
|||
litellm_params=litellm_params,
|
||||
)
|
||||
|
||||
assert (
|
||||
captured_optional_params.get("aws_region_name") == "us-gov-west-1"
|
||||
), "s3_region_name must override aws_region_name for SigV4 signing"
|
||||
assert captured_optional_params.get("aws_region_name") == "us-gov-west-1", (
|
||||
"s3_region_name must override aws_region_name for SigV4 signing"
|
||||
)
|
||||
|
||||
def _signed_upload_request(self, litellm_params: dict) -> dict:
|
||||
from litellm.llms.bedrock.files.transformation import BedrockFilesConfig
|
||||
|
|
@ -1115,9 +1090,9 @@ class TestBedrockFilesEmbeddingTransformation:
|
|||
}
|
||||
]
|
||||
)
|
||||
assert result[0]["modelInput"] == {
|
||||
"inputText": "Hi"
|
||||
}, f"model id {model_id} did not route to Titan v2 embedding path"
|
||||
assert result[0]["modelInput"] == {"inputText": "Hi"}, (
|
||||
f"model id {model_id} did not route to Titan v2 embedding path"
|
||||
)
|
||||
|
||||
def test_pretokenized_input_list_of_ints_raises(self):
|
||||
"""`input: List[int]` (pre-tokenized) is rejected, not silently mis-shaped."""
|
||||
|
|
@ -1187,7 +1162,6 @@ class TestBedrockFilesEmbeddingTransformation:
|
|||
assert "messages" in result[0]["modelInput"]
|
||||
assert "inputText" not in result[0]["modelInput"]
|
||||
|
||||
|
||||
def test_titan_v2_marker_boundary_rejects_lookalikes(self):
|
||||
"""The marker must end at `:`, `/`, or end-of-string to avoid false positives."""
|
||||
from litellm.llms.bedrock.files.transformation import BedrockFilesConfig
|
||||
|
|
@ -1198,9 +1172,9 @@ class TestBedrockFilesEmbeddingTransformation:
|
|||
"bedrock/amazon.titan-embed-text-v2-experimental:0",
|
||||
"bedrock/amazon.titan-embed-text-v2foo",
|
||||
):
|
||||
assert not BedrockFilesConfig._is_titan_v2_embed_model(
|
||||
model
|
||||
), f"{model} unexpectedly matched the Titan v2 marker"
|
||||
assert not BedrockFilesConfig._is_titan_v2_embed_model(model), (
|
||||
f"{model} unexpectedly matched the Titan v2 marker"
|
||||
)
|
||||
|
||||
# Real Titan v2 ids that MUST match
|
||||
for model in (
|
||||
|
|
@ -1209,9 +1183,9 @@ class TestBedrockFilesEmbeddingTransformation:
|
|||
"us.amazon.titan-embed-text-v2:0",
|
||||
"arn:aws:bedrock:us-east-1:123:foundation-model/amazon.titan-embed-text-v2:0",
|
||||
):
|
||||
assert BedrockFilesConfig._is_titan_v2_embed_model(
|
||||
model
|
||||
), f"{model} unexpectedly missed the Titan v2 marker"
|
||||
assert BedrockFilesConfig._is_titan_v2_embed_model(model), (
|
||||
f"{model} unexpectedly missed the Titan v2 marker"
|
||||
)
|
||||
|
||||
def test_titan_v2_accepted_when_registry_schema_field_matches(self, mocker):
|
||||
"""Registry-driven happy path: nested
|
||||
|
|
@ -1221,13 +1195,9 @@ class TestBedrockFilesEmbeddingTransformation:
|
|||
|
||||
mocker.patch(
|
||||
"litellm.get_model_info",
|
||||
return_value={
|
||||
"provider_specific_entry": {"bedrock_invocation_schema": "titan_v2"}
|
||||
},
|
||||
)
|
||||
assert BedrockFilesConfig._is_titan_v2_embed_model(
|
||||
"amazon.titan-embed-text-v2:0"
|
||||
return_value={"provider_specific_entry": {"bedrock_invocation_schema": "titan_v2"}},
|
||||
)
|
||||
assert BedrockFilesConfig._is_titan_v2_embed_model("amazon.titan-embed-text-v2:0")
|
||||
|
||||
def test_titan_v2_rejected_when_registry_schema_field_differs(self, mocker):
|
||||
"""Registry resolves with a different schema value (e.g. a hypothetical
|
||||
|
|
@ -1237,19 +1207,13 @@ class TestBedrockFilesEmbeddingTransformation:
|
|||
|
||||
mocker.patch(
|
||||
"litellm.get_model_info",
|
||||
return_value={
|
||||
"provider_specific_entry": {"bedrock_invocation_schema": "cohere_v3"}
|
||||
},
|
||||
return_value={"provider_specific_entry": {"bedrock_invocation_schema": "cohere_v3"}},
|
||||
)
|
||||
# Even though the model id looks like Titan v2, the registry says
|
||||
# otherwise and we trust it.
|
||||
assert not BedrockFilesConfig._is_titan_v2_embed_model(
|
||||
"amazon.titan-embed-text-v2:0"
|
||||
)
|
||||
assert not BedrockFilesConfig._is_titan_v2_embed_model("amazon.titan-embed-text-v2:0")
|
||||
|
||||
def test_titan_v2_falls_back_to_marker_when_registry_lacks_schema_field(
|
||||
self, mocker
|
||||
):
|
||||
def test_titan_v2_falls_back_to_marker_when_registry_lacks_schema_field(self, mocker):
|
||||
"""Registry resolves but the entry has no
|
||||
`provider_specific_entry.bedrock_invocation_schema` field yet (e.g.
|
||||
a stale local registry) -> fall through to substring."""
|
||||
|
|
@ -1260,9 +1224,7 @@ class TestBedrockFilesEmbeddingTransformation:
|
|||
"litellm.get_model_info",
|
||||
return_value={"mode": "embedding"},
|
||||
)
|
||||
assert BedrockFilesConfig._is_titan_v2_embed_model(
|
||||
"amazon.titan-embed-text-v2:0"
|
||||
)
|
||||
assert BedrockFilesConfig._is_titan_v2_embed_model("amazon.titan-embed-text-v2:0")
|
||||
|
||||
# provider_specific_entry present but missing the schema key
|
||||
mocker.patch(
|
||||
|
|
@ -1272,9 +1234,7 @@ class TestBedrockFilesEmbeddingTransformation:
|
|||
"provider_specific_entry": {"unrelated": "value"},
|
||||
},
|
||||
)
|
||||
assert BedrockFilesConfig._is_titan_v2_embed_model(
|
||||
"amazon.titan-embed-text-v2:0"
|
||||
)
|
||||
assert BedrockFilesConfig._is_titan_v2_embed_model("amazon.titan-embed-text-v2:0")
|
||||
|
||||
def test_titan_v2_accepted_when_registry_silent(self, mocker):
|
||||
"""Marker-only match is fine for ids the registry can't resolve
|
||||
|
|
@ -1282,9 +1242,7 @@ class TestBedrockFilesEmbeddingTransformation:
|
|||
from litellm.llms.bedrock.files.transformation import BedrockFilesConfig
|
||||
|
||||
mocker.patch("litellm.get_model_info", side_effect=Exception("not mapped"))
|
||||
assert BedrockFilesConfig._is_titan_v2_embed_model(
|
||||
"us.amazon.titan-embed-text-v2:0"
|
||||
)
|
||||
assert BedrockFilesConfig._is_titan_v2_embed_model("us.amazon.titan-embed-text-v2:0")
|
||||
assert BedrockFilesConfig._is_titan_v2_embed_model(
|
||||
"arn:aws:bedrock:us-east-1:123:foundation-model/amazon.titan-embed-text-v2:0"
|
||||
)
|
||||
|
|
@ -1296,87 +1254,49 @@ class TestBedrockFilesEmbeddingTransformation:
|
|||
# Happy path: returns the nested field's string value
|
||||
mocker.patch(
|
||||
"litellm.get_model_info",
|
||||
return_value={
|
||||
"provider_specific_entry": {"bedrock_invocation_schema": "titan_v2"}
|
||||
},
|
||||
)
|
||||
assert (
|
||||
BedrockFilesConfig._lookup_provider_specific_field(
|
||||
"anything", "bedrock_invocation_schema"
|
||||
)
|
||||
== "titan_v2"
|
||||
return_value={"provider_specific_entry": {"bedrock_invocation_schema": "titan_v2"}},
|
||||
)
|
||||
assert BedrockFilesConfig._lookup_provider_specific_field("anything", "bedrock_invocation_schema") == "titan_v2"
|
||||
|
||||
# Registry raises -> None
|
||||
mocker.patch("litellm.get_model_info", side_effect=Exception("not mapped"))
|
||||
assert (
|
||||
BedrockFilesConfig._lookup_provider_specific_field("anything", "any")
|
||||
is None
|
||||
)
|
||||
assert BedrockFilesConfig._lookup_provider_specific_field("anything", "any") is None
|
||||
|
||||
# Registry returns non-dict -> None
|
||||
mocker.patch("litellm.get_model_info", return_value="not a dict")
|
||||
assert (
|
||||
BedrockFilesConfig._lookup_provider_specific_field("anything", "any")
|
||||
is None
|
||||
)
|
||||
assert BedrockFilesConfig._lookup_provider_specific_field("anything", "any") is None
|
||||
|
||||
# Registry returns dict without provider_specific_entry -> None
|
||||
mocker.patch("litellm.get_model_info", return_value={"mode": "embedding"})
|
||||
assert (
|
||||
BedrockFilesConfig._lookup_provider_specific_field(
|
||||
"anything", "bedrock_invocation_schema"
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert BedrockFilesConfig._lookup_provider_specific_field("anything", "bedrock_invocation_schema") is None
|
||||
|
||||
# provider_specific_entry exists but isn't a dict -> None
|
||||
mocker.patch(
|
||||
"litellm.get_model_info",
|
||||
return_value={"provider_specific_entry": "not a dict"},
|
||||
)
|
||||
assert (
|
||||
BedrockFilesConfig._lookup_provider_specific_field(
|
||||
"anything", "bedrock_invocation_schema"
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert BedrockFilesConfig._lookup_provider_specific_field("anything", "bedrock_invocation_schema") is None
|
||||
|
||||
# provider_specific_entry dict missing the requested field -> None
|
||||
mocker.patch(
|
||||
"litellm.get_model_info",
|
||||
return_value={"provider_specific_entry": {"unrelated": "x"}},
|
||||
)
|
||||
assert (
|
||||
BedrockFilesConfig._lookup_provider_specific_field(
|
||||
"anything", "bedrock_invocation_schema"
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert BedrockFilesConfig._lookup_provider_specific_field("anything", "bedrock_invocation_schema") is None
|
||||
|
||||
# Non-string nested value -> None
|
||||
mocker.patch(
|
||||
"litellm.get_model_info",
|
||||
return_value={"provider_specific_entry": {"bedrock_invocation_schema": 42}},
|
||||
)
|
||||
assert (
|
||||
BedrockFilesConfig._lookup_provider_specific_field(
|
||||
"anything", "bedrock_invocation_schema"
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert BedrockFilesConfig._lookup_provider_specific_field("anything", "bedrock_invocation_schema") is None
|
||||
|
||||
# Empty-string nested value -> None
|
||||
mocker.patch(
|
||||
"litellm.get_model_info",
|
||||
return_value={"provider_specific_entry": {"bedrock_invocation_schema": ""}},
|
||||
)
|
||||
assert (
|
||||
BedrockFilesConfig._lookup_provider_specific_field(
|
||||
"anything", "bedrock_invocation_schema"
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert BedrockFilesConfig._lookup_provider_specific_field("anything", "bedrock_invocation_schema") is None
|
||||
|
||||
def test_classify_batch_record_helper(self):
|
||||
"""Helper classifies by `url` first, then by body shape."""
|
||||
|
|
@ -1384,28 +1304,18 @@ class TestBedrockFilesEmbeddingTransformation:
|
|||
from litellm.types.llms.bedrock import BedrockBatchRecordKind
|
||||
|
||||
assert (
|
||||
BedrockFilesConfig._classify_batch_record(
|
||||
{"url": "/v1/embeddings", "body": {"input": "x"}}
|
||||
)
|
||||
BedrockFilesConfig._classify_batch_record({"url": "/v1/embeddings", "body": {"input": "x"}})
|
||||
is BedrockBatchRecordKind.EMBEDDING
|
||||
)
|
||||
# body-only fallback
|
||||
assert (
|
||||
BedrockFilesConfig._classify_batch_record({"body": {"input": "x"}})
|
||||
is BedrockBatchRecordKind.EMBEDDING
|
||||
)
|
||||
assert BedrockFilesConfig._classify_batch_record({"body": {"input": "x"}}) is BedrockBatchRecordKind.EMBEDDING
|
||||
# chat shape
|
||||
assert (
|
||||
BedrockFilesConfig._classify_batch_record(
|
||||
{"url": "/v1/chat/completions", "body": {"messages": []}}
|
||||
)
|
||||
BedrockFilesConfig._classify_batch_record({"url": "/v1/chat/completions", "body": {"messages": []}})
|
||||
is BedrockBatchRecordKind.CHAT
|
||||
)
|
||||
# ambiguous body without any recognized key is treated as chat
|
||||
assert (
|
||||
BedrockFilesConfig._classify_batch_record({"body": {}})
|
||||
is BedrockBatchRecordKind.CHAT
|
||||
)
|
||||
assert BedrockFilesConfig._classify_batch_record({"body": {}}) is BedrockBatchRecordKind.CHAT
|
||||
|
||||
@pytest.mark.parametrize("body", ["not a mapping", ["messages"], 7, None], ids=["str", "list", "int", "missing"])
|
||||
def test_classify_batch_record_falls_back_to_chat_for_non_mapping_body(self, body):
|
||||
|
|
@ -1445,7 +1355,6 @@ class TestBedrockFilesEmbeddingTransformation:
|
|||
transformer, corrupting the modelInput.
|
||||
"""
|
||||
from litellm.llms.bedrock.files.transformation import BedrockFilesConfig
|
||||
|
||||
from litellm.types.llms.bedrock import BedrockBatchRecordKind
|
||||
|
||||
# Direct helper assertion
|
||||
|
|
@ -1483,9 +1392,7 @@ class TestBedrockFilesEmbeddingTransformation:
|
|||
)
|
||||
|
||||
model_input = result[0]["modelInput"]
|
||||
assert (
|
||||
"inputText" not in model_input
|
||||
), "explicit chat URL must not produce an embedding-shaped modelInput"
|
||||
assert "inputText" not in model_input, "explicit chat URL must not produce an embedding-shaped modelInput"
|
||||
|
||||
def test_coerce_embedding_input_helper_isolated(self):
|
||||
"""Direct coverage of the extracted input-normalization helper."""
|
||||
|
|
@ -1495,9 +1402,7 @@ class TestBedrockFilesEmbeddingTransformation:
|
|||
|
||||
# Happy paths
|
||||
assert BedrockFilesConfig._coerce_embedding_input_to_string("hello") == "hello"
|
||||
assert (
|
||||
BedrockFilesConfig._coerce_embedding_input_to_string(["hello"]) == "hello"
|
||||
)
|
||||
assert BedrockFilesConfig._coerce_embedding_input_to_string(["hello"]) == "hello"
|
||||
|
||||
# Error paths
|
||||
with pytest.raises(ValueError, match="missing required `input`"):
|
||||
|
|
@ -1525,22 +1430,16 @@ class TestBedrockFilesEmbeddingTransformation:
|
|||
|
||||
# /v1/completions (legacy completions endpoint)
|
||||
assert (
|
||||
BedrockFilesConfig._classify_batch_record(
|
||||
{"url": "/v1/completions", "body": {"input": "x"}}
|
||||
)
|
||||
BedrockFilesConfig._classify_batch_record({"url": "/v1/completions", "body": {"input": "x"}})
|
||||
is BedrockBatchRecordKind.TEXT_COMPLETION
|
||||
)
|
||||
assert (
|
||||
BedrockFilesConfig._classify_batch_record(
|
||||
{"url": "/v1/responses", "body": {"input": "x"}}
|
||||
)
|
||||
BedrockFilesConfig._classify_batch_record({"url": "/v1/responses", "body": {"input": "x"}})
|
||||
is BedrockBatchRecordKind.RESPONSES
|
||||
)
|
||||
# Arbitrary unknown url - caller's explicit signal still wins
|
||||
assert (
|
||||
BedrockFilesConfig._classify_batch_record(
|
||||
{"url": "/v1/moderations", "body": {"input": "x"}}
|
||||
)
|
||||
BedrockFilesConfig._classify_batch_record({"url": "/v1/moderations", "body": {"input": "x"}})
|
||||
is BedrockBatchRecordKind.CHAT
|
||||
)
|
||||
|
||||
|
|
@ -1692,7 +1591,10 @@ class TestBedrockBatchNonChatEndpointRecords:
|
|||
assert model_input["messages"][0]["content"] == [{"type": "text", "text": "Weather in Paris?"}]
|
||||
tool = model_input["tools"][0]
|
||||
function = tool.get("function", tool)
|
||||
assert (function["name"], function.get("parameters", function.get("input_schema"))) == ("get_weather", parameters)
|
||||
assert (function["name"], function.get("parameters", function.get("input_schema"))) == (
|
||||
"get_weather",
|
||||
parameters,
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("url", "body"),
|
||||
|
|
@ -1850,9 +1752,7 @@ class TestBedrockBatchNonChatEndpointRecords:
|
|||
"embedding",
|
||||
]
|
||||
for record in result[:3]:
|
||||
assert record["modelInput"]["messages"] == [
|
||||
{"role": "user", "content": [{"type": "text", "text": "hi"}]}
|
||||
]
|
||||
assert record["modelInput"]["messages"] == [{"role": "user", "content": [{"type": "text", "text": "hi"}]}]
|
||||
assert result[3]["modelInput"] == {"inputText": "hi"}
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -1897,9 +1797,7 @@ class TestBedrockBatchNonChatEndpointRecords:
|
|||
}
|
||||
)
|
||||
|
||||
assert model_input["messages"] == [
|
||||
{"role": "user", "content": [{"type": "text", "text": "from messages"}]}
|
||||
]
|
||||
assert model_input["messages"] == [{"role": "user", "content": [{"type": "text", "text": "from messages"}]}]
|
||||
|
||||
|
||||
class TestBedrockBatchAnthropicRowParams:
|
||||
|
|
@ -1980,7 +1878,10 @@ class TestBedrockBatchAnthropicRowParams:
|
|||
@pytest.mark.parametrize(
|
||||
("url", "body"),
|
||||
[
|
||||
("/v1/chat/completions", {"messages": [{"role": "user", "content": "17 * 23?"}], "reasoning_effort": "low"}),
|
||||
(
|
||||
"/v1/chat/completions",
|
||||
{"messages": [{"role": "user", "content": "17 * 23?"}], "reasoning_effort": "low"},
|
||||
),
|
||||
("/v1/responses", {"input": "17 * 23?", "reasoning": {"effort": "low"}}),
|
||||
],
|
||||
ids=["chat", "responses"],
|
||||
|
|
@ -2140,16 +2041,24 @@ class TestBedrockFileDeletion:
|
|||
with respx.mock, closing(HTTPHandler()) as client:
|
||||
route: Final = respx.delete(self.URL).mock(return_value=httpx.Response(204))
|
||||
deleted: Final = litellm.file_delete(
|
||||
file_id=self.S3_URI, custom_llm_provider="bedrock", client=client,
|
||||
aws_access_key_id="AKIAEXAMPLE", aws_secret_access_key="test-secret", aws_region_name="us-west-2",
|
||||
file_id=self.S3_URI,
|
||||
custom_llm_provider="bedrock",
|
||||
client=client,
|
||||
aws_access_key_id="AKIAEXAMPLE",
|
||||
aws_secret_access_key="test-secret",
|
||||
aws_region_name="us-west-2",
|
||||
)
|
||||
assert route.call_count == 1
|
||||
request: Final = route.calls[0].request
|
||||
assert request.content == b""
|
||||
signed: Final = AWSRequest(method="DELETE", url=self.URL, headers={
|
||||
"X-Amz-Date": request.headers["X-Amz-Date"],
|
||||
"X-Amz-Content-SHA256": request.headers["X-Amz-Content-SHA256"],
|
||||
})
|
||||
signed: Final = AWSRequest(
|
||||
method="DELETE",
|
||||
url=self.URL,
|
||||
headers={
|
||||
"X-Amz-Date": request.headers["X-Amz-Date"],
|
||||
"X-Amz-Content-SHA256": request.headers["X-Amz-Content-SHA256"],
|
||||
},
|
||||
)
|
||||
signed.context["timestamp"] = request.headers["X-Amz-Date"]
|
||||
auth: Final = S3SigV4Auth(Credentials("AKIAEXAMPLE", "test-secret"), "s3", "us-west-2")
|
||||
signature: Final = auth.signature(auth.string_to_sign(signed, auth.canonical_request(signed)), signed)
|
||||
|
|
@ -2177,15 +2086,22 @@ class TestBedrockFileDeletion:
|
|||
|
||||
with pytest.raises(BedrockError, match="AccessDenied"):
|
||||
await litellm.afile_delete(
|
||||
file_id=self.S3_URI, custom_llm_provider="bedrock", client=client,
|
||||
aws_access_key_id="AKIAEXAMPLE", aws_secret_access_key="test-secret", aws_region_name="us-west-2",
|
||||
file_id=self.S3_URI,
|
||||
custom_llm_provider="bedrock",
|
||||
client=client,
|
||||
aws_access_key_id="AKIAEXAMPLE",
|
||||
aws_secret_access_key="test-secret",
|
||||
aws_region_name="us-west-2",
|
||||
)
|
||||
assert route.call_count == 1
|
||||
|
||||
@pytest.mark.parametrize("file_id, message", [
|
||||
("s3://other-bucket/litellm-bedrock-files-model-abc.jsonl", "configured storage bucket"),
|
||||
("s3://my-bucket/private/data.jsonl", "LiteLLM-managed"),
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"file_id, message",
|
||||
[
|
||||
("s3://other-bucket/litellm-bedrock-files-model-abc.jsonl", "configured storage bucket"),
|
||||
("s3://my-bucket/private/data.jsonl", "LiteLLM-managed"),
|
||||
],
|
||||
)
|
||||
def test_delete_rejects_untrusted_objects_before_signing(
|
||||
self, file_id: str, message: str, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
|
|
@ -2233,11 +2149,7 @@ class TestBedrockFileContentTransformation:
|
|||
assert params == {}
|
||||
|
||||
signed_headers = litellm_params[S3_SIGNED_REQUEST_HEADERS_PARAM]
|
||||
content_hashes = {
|
||||
value
|
||||
for name, value in signed_headers.items()
|
||||
if name.lower() == "x-amz-content-sha256"
|
||||
}
|
||||
content_hashes = {value for name, value in signed_headers.items() if name.lower() == "x-amz-content-sha256"}
|
||||
assert content_hashes == {hashlib.sha256(b"").hexdigest()}, (
|
||||
"GET has no payload, so the content hash must be the empty-body hash."
|
||||
" The header name is matched case-insensitively because botocore picks"
|
||||
|
|
@ -2260,9 +2172,7 @@ class TestBedrockFileContentTransformation:
|
|||
unified_file_id = SpecialEnums.LITELLM_MANAGED_FILE_COMPLETE_STR.value.format(
|
||||
"application/json", "unified-id", "", self.S3_URI, "model-id"
|
||||
)
|
||||
encoded_file_id = (
|
||||
base64.urlsafe_b64encode(unified_file_id.encode()).decode().rstrip("=")
|
||||
)
|
||||
encoded_file_id = base64.urlsafe_b64encode(unified_file_id.encode()).decode().rstrip("=")
|
||||
|
||||
url, _ = BedrockFilesConfig().transform_file_content_request(
|
||||
file_content_request={"file_id": encoded_file_id},
|
||||
|
|
@ -2280,16 +2190,13 @@ class TestBedrockFileContentTransformation:
|
|||
|
||||
with pytest.raises(BedrockError, match="configured storage bucket") as rejection:
|
||||
BedrockFilesConfig().transform_file_content_request(
|
||||
file_content_request={
|
||||
"file_id": "s3://other-bucket/litellm-batch-outputs/job/x.jsonl.out"
|
||||
},
|
||||
file_content_request={"file_id": "s3://other-bucket/litellm-batch-outputs/job/x.jsonl.out"},
|
||||
optional_params={},
|
||||
litellm_params=self._litellm_params(),
|
||||
)
|
||||
|
||||
assert rejection.value.status_code == 400
|
||||
|
||||
|
||||
def test_transform_file_content_request_rejects_unmanaged_key(self, monkeypatch):
|
||||
from litellm.llms.bedrock.common_utils import BedrockError
|
||||
from litellm.llms.bedrock.files.transformation import BedrockFilesConfig
|
||||
|
|
@ -2305,7 +2212,6 @@ class TestBedrockFileContentTransformation:
|
|||
|
||||
assert rejection.value.status_code == 400
|
||||
|
||||
|
||||
def test_extract_s3_uri_rejects_non_managed_file_id(self):
|
||||
"""A file id that is neither an s3:// URI nor a unified id must be rejected."""
|
||||
from litellm.llms.bedrock.files.transformation import (
|
||||
|
|
@ -2315,9 +2221,7 @@ class TestBedrockFileContentTransformation:
|
|||
with pytest.raises(ValueError, match="managed LiteLLM S3 file id"):
|
||||
extract_s3_uri_from_file_id("file-1234567890")
|
||||
|
||||
def test_transform_file_content_request_requires_configured_bucket(
|
||||
self, monkeypatch
|
||||
):
|
||||
def test_transform_file_content_request_requires_configured_bucket(self, monkeypatch):
|
||||
"""Without a server-configured bucket (env or snapshot), the request must fail
|
||||
before any S3 call rather than guessing a bucket from the file id."""
|
||||
from litellm.llms.bedrock.files.transformation import BedrockFilesConfig
|
||||
|
|
@ -2354,9 +2258,7 @@ class TestBedrockFileContentTransformation:
|
|||
|
||||
from litellm.types.router import CredentialLiteLLMParams
|
||||
|
||||
snapshot = CredentialLiteLLMParams(**deployment_litellm_params).model_dump(
|
||||
exclude_none=True
|
||||
)
|
||||
snapshot = CredentialLiteLLMParams(**deployment_litellm_params).model_dump(exclude_none=True)
|
||||
params = self._litellm_params()
|
||||
params["_litellm_internal_model_credentials"] = MappingProxyType(snapshot)
|
||||
return params
|
||||
|
|
@ -2373,19 +2275,12 @@ class TestBedrockFileContentTransformation:
|
|||
monkeypatch.delenv("AWS_S3_OUTPUT_BUCKET_NAME", raising=False)
|
||||
|
||||
url, _ = BedrockFilesConfig().transform_file_content_request(
|
||||
file_content_request={
|
||||
"file_id": "s3://out-bucket/litellm-batch-outputs/job/in.jsonl.out"
|
||||
},
|
||||
file_content_request={"file_id": "s3://out-bucket/litellm-batch-outputs/job/in.jsonl.out"},
|
||||
optional_params={},
|
||||
litellm_params=self._trusted(
|
||||
s3_bucket_name="in-bucket", s3_output_bucket_name="out-bucket"
|
||||
),
|
||||
litellm_params=self._trusted(s3_bucket_name="in-bucket", s3_output_bucket_name="out-bucket"),
|
||||
)
|
||||
|
||||
assert (
|
||||
url
|
||||
== "https://s3.us-west-2.amazonaws.com/out-bucket/litellm-batch-outputs/job/in.jsonl.out"
|
||||
)
|
||||
assert url == "https://s3.us-west-2.amazonaws.com/out-bucket/litellm-batch-outputs/job/in.jsonl.out"
|
||||
|
||||
def test_output_bucket_falls_back_to_env(self, monkeypatch):
|
||||
"""The output bucket resolves from AWS_S3_OUTPUT_BUCKET_NAME when not in
|
||||
|
|
@ -2396,17 +2291,12 @@ class TestBedrockFileContentTransformation:
|
|||
monkeypatch.setenv("AWS_S3_OUTPUT_BUCKET_NAME", "env-out-bucket")
|
||||
|
||||
url, _ = BedrockFilesConfig().transform_file_content_request(
|
||||
file_content_request={
|
||||
"file_id": "s3://env-out-bucket/litellm-batch-outputs/job/in.jsonl.out"
|
||||
},
|
||||
file_content_request={"file_id": "s3://env-out-bucket/litellm-batch-outputs/job/in.jsonl.out"},
|
||||
optional_params={},
|
||||
litellm_params=self._litellm_params(),
|
||||
)
|
||||
|
||||
assert (
|
||||
url
|
||||
== "https://s3.us-west-2.amazonaws.com/env-out-bucket/litellm-batch-outputs/job/in.jsonl.out"
|
||||
)
|
||||
assert url == "https://s3.us-west-2.amazonaws.com/env-out-bucket/litellm-batch-outputs/job/in.jsonl.out"
|
||||
|
||||
def test_input_bucket_still_validates_when_output_bucket_set(self, monkeypatch):
|
||||
"""Adding output-bucket support must not break retrieval of input-bucket
|
||||
|
|
@ -2417,19 +2307,12 @@ class TestBedrockFileContentTransformation:
|
|||
monkeypatch.delenv("AWS_S3_OUTPUT_BUCKET_NAME", raising=False)
|
||||
|
||||
url, _ = BedrockFilesConfig().transform_file_content_request(
|
||||
file_content_request={
|
||||
"file_id": "s3://in-bucket/litellm-batch-outputs/job/in.jsonl.out"
|
||||
},
|
||||
file_content_request={"file_id": "s3://in-bucket/litellm-batch-outputs/job/in.jsonl.out"},
|
||||
optional_params={},
|
||||
litellm_params=self._trusted(
|
||||
s3_bucket_name="in-bucket", s3_output_bucket_name="out-bucket"
|
||||
),
|
||||
litellm_params=self._trusted(s3_bucket_name="in-bucket", s3_output_bucket_name="out-bucket"),
|
||||
)
|
||||
|
||||
assert (
|
||||
url
|
||||
== "https://s3.us-west-2.amazonaws.com/in-bucket/litellm-batch-outputs/job/in.jsonl.out"
|
||||
)
|
||||
assert url == "https://s3.us-west-2.amazonaws.com/in-bucket/litellm-batch-outputs/job/in.jsonl.out"
|
||||
|
||||
def test_rejects_bucket_outside_input_and_output(self, monkeypatch):
|
||||
"""A file id whose bucket is neither the input nor the output bucket is
|
||||
|
|
@ -2442,18 +2325,13 @@ class TestBedrockFileContentTransformation:
|
|||
|
||||
with pytest.raises(BedrockError, match="configured storage bucket") as rejection:
|
||||
BedrockFilesConfig().transform_file_content_request(
|
||||
file_content_request={
|
||||
"file_id": "s3://other-bucket/litellm-batch-outputs/job/x.jsonl.out"
|
||||
},
|
||||
file_content_request={"file_id": "s3://other-bucket/litellm-batch-outputs/job/x.jsonl.out"},
|
||||
optional_params={},
|
||||
litellm_params=self._trusted(
|
||||
s3_bucket_name="in-bucket", s3_output_bucket_name="out-bucket"
|
||||
),
|
||||
litellm_params=self._trusted(s3_bucket_name="in-bucket", s3_output_bucket_name="out-bucket"),
|
||||
)
|
||||
|
||||
assert rejection.value.status_code == 400
|
||||
|
||||
|
||||
def test_sign_request_without_botocore_raises_helpful_error(self, monkeypatch):
|
||||
"""A missing botocore must surface an actionable 'install boto3' error
|
||||
rather than a raw import failure."""
|
||||
|
|
@ -2479,9 +2357,7 @@ class TestBedrockFileContentTransformation:
|
|||
|
||||
monkeypatch.delenv("AWS_S3_BUCKET_NAME", raising=False)
|
||||
litellm_params = self._litellm_params()
|
||||
litellm_params["_litellm_internal_model_credentials"] = MappingProxyType(
|
||||
{"s3_bucket_name": "my-bucket"}
|
||||
)
|
||||
litellm_params["_litellm_internal_model_credentials"] = MappingProxyType({"s3_bucket_name": "my-bucket"})
|
||||
|
||||
url, _ = BedrockFilesConfig().transform_file_content_request(
|
||||
file_content_request={"file_id": self.S3_URI},
|
||||
|
|
@ -2521,9 +2397,10 @@ class TestBedrockFileContentTransformation:
|
|||
s3_endpoint_url="https://bucket.vpce-abc.s3.us-east-1.vpce.amazonaws.com",
|
||||
)
|
||||
|
||||
assert BedrockFilesConfig()._s3_request_target(
|
||||
optional_params={}, litellm_params=lp
|
||||
).endpoint_url == "https://bucket.vpce-abc.s3.us-east-1.vpce.amazonaws.com"
|
||||
assert (
|
||||
BedrockFilesConfig()._s3_request_target(optional_params={}, litellm_params=lp).endpoint_url
|
||||
== "https://bucket.vpce-abc.s3.us-east-1.vpce.amazonaws.com"
|
||||
)
|
||||
|
||||
def test_validate_environment_merges_and_pops_signed_get_headers(self):
|
||||
from litellm.llms.bedrock.files.transformation import (
|
||||
|
|
@ -2531,9 +2408,7 @@ class TestBedrockFileContentTransformation:
|
|||
BedrockFilesConfig,
|
||||
)
|
||||
|
||||
litellm_params = {
|
||||
S3_SIGNED_REQUEST_HEADERS_PARAM: {"Authorization": "AWS4-HMAC-SHA256 test"}
|
||||
}
|
||||
litellm_params = {S3_SIGNED_REQUEST_HEADERS_PARAM: {"Authorization": "AWS4-HMAC-SHA256 test"}}
|
||||
|
||||
headers = BedrockFilesConfig().validate_environment(
|
||||
headers={"x-custom": "kept"},
|
||||
|
|
@ -2599,9 +2474,7 @@ class TestBedrockFileContentTransformation:
|
|||
monkeypatch.setenv("AWS_S3_BUCKET_NAME", "my-bucket")
|
||||
|
||||
with respx.mock:
|
||||
route = respx.get(self.EXPECTED_URL).mock(
|
||||
return_value=httpx.Response(200, content=b'{"recordId": "x"}')
|
||||
)
|
||||
route = respx.get(self.EXPECTED_URL).mock(return_value=httpx.Response(200, content=b'{"recordId": "x"}'))
|
||||
|
||||
response = litellm.file_content(
|
||||
file_id=self.S3_URI,
|
||||
|
|
@ -2629,9 +2502,7 @@ class TestBedrockFileContentTransformation:
|
|||
litellm.in_memory_llm_clients_cache.flush_cache()
|
||||
|
||||
with respx.mock:
|
||||
route = respx.get(self.EXPECTED_URL).mock(
|
||||
return_value=httpx.Response(200, content=b'{"recordId": "x"}')
|
||||
)
|
||||
route = respx.get(self.EXPECTED_URL).mock(return_value=httpx.Response(200, content=b'{"recordId": "x"}'))
|
||||
|
||||
response = await litellm.afile_content(
|
||||
file_id=self.S3_URI,
|
||||
|
|
@ -2640,11 +2511,7 @@ class TestBedrockFileContentTransformation:
|
|||
)
|
||||
|
||||
assert route.called
|
||||
assert (
|
||||
route.calls[0]
|
||||
.request.headers["Authorization"]
|
||||
.startswith("AWS4-HMAC-SHA256")
|
||||
)
|
||||
assert route.calls[0].request.headers["Authorization"].startswith("AWS4-HMAC-SHA256")
|
||||
assert response.content == b'{"recordId": "x"}'
|
||||
|
||||
|
||||
|
|
@ -2678,9 +2545,7 @@ class TestBedrockFilesS3SignatureEncoding:
|
|||
headers: Mapping[str, str],
|
||||
) -> str:
|
||||
sent = {name.lower(): value for name, value in headers.items()}
|
||||
signed_names = (
|
||||
sent["authorization"].split("SignedHeaders=")[1].split(",")[0].split(";")
|
||||
)
|
||||
signed_names = sent["authorization"].split("SignedHeaders=")[1].split(",")[0].split(";")
|
||||
request = AWSRequest(
|
||||
method=method,
|
||||
url=url,
|
||||
|
|
@ -2688,12 +2553,8 @@ class TestBedrockFilesS3SignatureEncoding:
|
|||
headers={name: sent[name] for name in signed_names if name in sent},
|
||||
)
|
||||
request.context["timestamp"] = sent["x-amz-date"]
|
||||
signer = signer_cls(
|
||||
Credentials(self.ACCESS_KEY, self.SECRET_KEY), "s3", self.REGION
|
||||
)
|
||||
return signer.signature(
|
||||
signer.string_to_sign(request, signer.canonical_request(request)), request
|
||||
)
|
||||
signer = signer_cls(Credentials(self.ACCESS_KEY, self.SECRET_KEY), "s3", self.REGION)
|
||||
return signer.signature(signer.string_to_sign(request, signer.canonical_request(request)), request)
|
||||
|
||||
def _assert_signed_the_way_s3_reads_it(
|
||||
self,
|
||||
|
|
@ -2704,12 +2565,8 @@ class TestBedrockFilesS3SignatureEncoding:
|
|||
) -> None:
|
||||
assert "%20" in url, "the object key must reach the wire percent-encoded"
|
||||
sent_signature = headers["Authorization"].split("Signature=")[1].strip()
|
||||
assert sent_signature == self._signature_under(
|
||||
S3SigV4Auth, method, url, body, headers
|
||||
)
|
||||
assert sent_signature != self._signature_under(
|
||||
SigV4Auth, method, url, body, headers
|
||||
)
|
||||
assert sent_signature == self._signature_under(S3SigV4Auth, method, url, body, headers)
|
||||
assert sent_signature != self._signature_under(SigV4Auth, method, url, body, headers)
|
||||
|
||||
def test_create_file_signs_spaced_object_key_the_way_s3_does(self) -> None:
|
||||
from litellm.llms.bedrock.files.transformation import BedrockFilesConfig
|
||||
|
|
@ -2743,9 +2600,7 @@ class TestBedrockFilesS3SignatureEncoding:
|
|||
headers=signed["headers"],
|
||||
)
|
||||
|
||||
def test_file_content_signs_spaced_object_key_the_way_s3_does(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
def test_file_content_signs_spaced_object_key_the_way_s3_does(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from litellm.llms.bedrock.files.transformation import (
|
||||
S3_SIGNED_REQUEST_HEADERS_PARAM,
|
||||
BedrockFilesConfig,
|
||||
|
|
@ -2759,9 +2614,7 @@ class TestBedrockFilesS3SignatureEncoding:
|
|||
}
|
||||
|
||||
url, _ = BedrockFilesConfig().transform_file_content_request(
|
||||
file_content_request={
|
||||
"file_id": "s3://my-bucket/LLM AI Projects/litellm-bedrock-files-model-abc.jsonl"
|
||||
},
|
||||
file_content_request={"file_id": "s3://my-bucket/LLM AI Projects/litellm-bedrock-files-model-abc.jsonl"},
|
||||
optional_params={},
|
||||
litellm_params=litellm_params,
|
||||
)
|
||||
|
|
@ -3092,7 +2945,6 @@ class TestBedrockFileDeletionTransformation:
|
|||
|
||||
assert rejection.value.status_code == 400
|
||||
|
||||
|
||||
def test_transform_delete_file_request_rejects_unmanaged_key(self, monkeypatch):
|
||||
from litellm.llms.bedrock.common_utils import BedrockError
|
||||
from litellm.llms.bedrock.files.transformation import BedrockFilesConfig
|
||||
|
|
@ -3108,7 +2960,6 @@ class TestBedrockFileDeletionTransformation:
|
|||
|
||||
assert rejection.value.status_code == 400
|
||||
|
||||
|
||||
def test_transform_delete_file_response_echoes_the_deleted_id(self):
|
||||
import httpx
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from unittest.mock import Mock
|
|||
|
||||
import httpx
|
||||
import pytest
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
# Ensure the project root is on the import path so `litellm` can be imported when
|
||||
# tests are executed from any working directory.
|
||||
|
|
@ -25,6 +26,7 @@ from litellm.llms.bedrock.common_utils import (
|
|||
normalize_custom_field_on_tools,
|
||||
normalize_tool_input_schema_types_for_bedrock_invoke,
|
||||
)
|
||||
import litellm
|
||||
from litellm.constants import (
|
||||
BEDROCK_MIN_THINKING_BUDGET_TOKENS,
|
||||
DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET,
|
||||
|
|
@ -3781,3 +3783,5 @@ async def test_bedrock_messages_400_is_not_retried_unless_it_rejects_opt_in_nati
|
|||
assert isinstance(raised, litellm.BadRequestError)
|
||||
assert len(bedrock.bodies) == 1
|
||||
|
||||
|
||||
REASONING_BODY_ADAPTER: Final = TypeAdapter(dict[str, object])
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
|
||||
import asyncio, importlib, litellm, litellm.litellm_core_utils.get_model_cost_map as bedrock_govcloud_model_cost_map, pytest
|
||||
import asyncio
|
||||
import importlib
|
||||
from collections.abc import Iterator
|
||||
from typing import Final
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from litellm.llms.bedrock.common_utils import(
|
||||
import litellm
|
||||
import litellm.litellm_core_utils.get_model_cost_map as bedrock_govcloud_model_cost_map
|
||||
from litellm import completion
|
||||
from litellm.llms.bedrock.common_utils import (
|
||||
AmazonBedrockGlobalConfig,
|
||||
BedrockModelInfo,
|
||||
bedrock_native_output_is_opt_in,
|
||||
|
|
@ -16,11 +22,8 @@ from litellm.llms.bedrock.common_utils import(
|
|||
strip_bedrock_throughput_suffix,
|
||||
without_bedrock_native_output_opt_in,
|
||||
)
|
||||
from collections.abc import Iterator
|
||||
from litellm import completion
|
||||
from litellm.llms.bedrock.count_tokens.bedrock_token_counter import BedrockTokenCounter
|
||||
from tests._vcr_conftest_common import install_live_call_probe, record_vcr_outcome
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# get_bedrock_response_stream_shape lazy-load tests #
|
||||
|
|
@ -48,9 +51,7 @@ def test_bedrock_response_stream_shape_lazy_loads_once():
|
|||
import litellm.llms.bedrock.common_utils as mod
|
||||
|
||||
sentinel = MagicMock()
|
||||
with patch.object(
|
||||
mod, "_load_bedrock_response_stream_shape", return_value=sentinel
|
||||
) as mock_load:
|
||||
with patch.object(mod, "_load_bedrock_response_stream_shape", return_value=sentinel) as mock_load:
|
||||
assert mod.get_bedrock_response_stream_shape() is sentinel
|
||||
assert mod.get_bedrock_response_stream_shape() is sentinel
|
||||
mock_load.assert_called_once()
|
||||
|
|
@ -97,9 +98,7 @@ def test_bedrock_response_stream_shape_is_structure_shape():
|
|||
from litellm.llms.bedrock.common_utils import get_bedrock_response_stream_shape
|
||||
|
||||
loaded_shape = get_bedrock_response_stream_shape()
|
||||
assert (
|
||||
loaded_shape is not None
|
||||
), "get_bedrock_response_stream_shape() is None — botocore may not be installed"
|
||||
assert loaded_shape is not None, "get_bedrock_response_stream_shape() is None — botocore may not be installed"
|
||||
shape: StructureShape = loaded_shape
|
||||
assert isinstance(shape, StructureShape)
|
||||
assert shape.name == "ResponseStream"
|
||||
|
|
@ -164,9 +163,7 @@ def test_deepseek_cris():
|
|||
Test that DeepSeek models with cross-region inference prefix use converse route
|
||||
"""
|
||||
bedrock_model_info = BedrockModelInfo
|
||||
bedrock_route = bedrock_model_info.get_bedrock_route(
|
||||
model="bedrock/us.deepseek.r1-v1:0"
|
||||
)
|
||||
bedrock_route = bedrock_model_info.get_bedrock_route(model="bedrock/us.deepseek.r1-v1:0")
|
||||
assert bedrock_route == "converse"
|
||||
|
||||
|
||||
|
|
@ -239,27 +236,19 @@ def test_govcloud_cross_region_inference_prefix():
|
|||
bedrock_model_info = BedrockModelInfo
|
||||
|
||||
# Test us-gov prefix is stripped correctly for Claude models
|
||||
base_model = bedrock_model_info.get_base_model(
|
||||
model="bedrock/us-gov.anthropic.claude-haiku-4-5-20251001-v1:0"
|
||||
)
|
||||
base_model = bedrock_model_info.get_base_model(model="bedrock/us-gov.anthropic.claude-haiku-4-5-20251001-v1:0")
|
||||
assert base_model == "anthropic.claude-haiku-4-5-20251001-v1:0"
|
||||
|
||||
# Test us-gov prefix is stripped correctly for different Claude versions
|
||||
base_model = bedrock_model_info.get_base_model(
|
||||
model="bedrock/us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
)
|
||||
base_model = bedrock_model_info.get_base_model(model="bedrock/us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0")
|
||||
assert base_model == "anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
|
||||
# Test us-gov prefix is stripped correctly for Haiku models
|
||||
base_model = bedrock_model_info.get_base_model(
|
||||
model="bedrock/us-gov.anthropic.claude-3-haiku-20240307-v1:0"
|
||||
)
|
||||
base_model = bedrock_model_info.get_base_model(model="bedrock/us-gov.anthropic.claude-3-haiku-20240307-v1:0")
|
||||
assert base_model == "anthropic.claude-3-haiku-20240307-v1:0"
|
||||
|
||||
# Test us-gov prefix is stripped correctly for Meta models
|
||||
base_model = bedrock_model_info.get_base_model(
|
||||
model="bedrock/us-gov.meta.llama3-8b-instruct-v1:0"
|
||||
)
|
||||
base_model = bedrock_model_info.get_base_model(model="bedrock/us-gov.meta.llama3-8b-instruct-v1:0")
|
||||
assert base_model == "meta.llama3-8b-instruct-v1:0"
|
||||
|
||||
|
||||
|
|
@ -273,23 +262,14 @@ def test_context_window_suffix_stripped_for_cost_lookup():
|
|||
"""
|
||||
from litellm.llms.bedrock.common_utils import get_bedrock_base_model
|
||||
|
||||
assert (
|
||||
get_bedrock_base_model("us.anthropic.claude-opus-4-6-v1[1m]")
|
||||
== "anthropic.claude-opus-4-6-v1"
|
||||
)
|
||||
assert (
|
||||
get_bedrock_base_model("us.anthropic.claude-sonnet-4-6[1m]")
|
||||
== "anthropic.claude-sonnet-4-6"
|
||||
)
|
||||
assert get_bedrock_base_model("us.anthropic.claude-opus-4-6-v1[1m]") == "anthropic.claude-opus-4-6-v1"
|
||||
assert get_bedrock_base_model("us.anthropic.claude-sonnet-4-6[1m]") == "anthropic.claude-sonnet-4-6"
|
||||
assert (
|
||||
get_bedrock_base_model("global.anthropic.claude-opus-4-5-20251101-v1:0[1m]")
|
||||
== "anthropic.claude-opus-4-5-20251101-v1:0"
|
||||
)
|
||||
# Ensure models without suffix are unaffected
|
||||
assert (
|
||||
get_bedrock_base_model("us.anthropic.claude-opus-4-6-v1")
|
||||
== "anthropic.claude-opus-4-6-v1"
|
||||
)
|
||||
assert get_bedrock_base_model("us.anthropic.claude-opus-4-6-v1") == "anthropic.claude-opus-4-6-v1"
|
||||
# Ensure :51k throughput suffix still works
|
||||
assert (
|
||||
get_bedrock_base_model("anthropic.claude-3-5-sonnet-20241022-v2:0:51k")
|
||||
|
|
@ -339,9 +319,7 @@ def test_output_config_effort_normalization_uses_model_info_ceiling(monkeypatch)
|
|||
("us.anthropic.claude-opus-4-7", "xhigh"),
|
||||
],
|
||||
)
|
||||
def test_bundled_bedrock_opus_model_info_declares_output_config_effort_ceiling(
|
||||
model, expected_ceiling
|
||||
):
|
||||
def test_bundled_bedrock_opus_model_info_declares_output_config_effort_ceiling(model, expected_ceiling):
|
||||
from litellm.litellm_core_utils.get_model_cost_map import GetModelCostMap
|
||||
|
||||
model_info = GetModelCostMap.load_local_model_cost_map()[model]
|
||||
|
|
@ -360,54 +338,24 @@ def test_route_prefix_matched_as_path_segment_not_substring():
|
|||
or a ``/`` boundary.
|
||||
"""
|
||||
# The bedrock_mantle/ provider prefix must NOT be read as the mantle/ route.
|
||||
assert (
|
||||
BedrockModelInfo.get_bedrock_route("bedrock_mantle/openai.gpt-5.5") != "mantle"
|
||||
)
|
||||
assert (
|
||||
BedrockModelInfo.get_bedrock_route("bedrock_mantle/openai.gpt-5.4") == "converse"
|
||||
)
|
||||
assert (
|
||||
BedrockModelInfo._explicit_mantle_route("bedrock_mantle/openai.gpt-5.5")
|
||||
is False
|
||||
)
|
||||
assert BedrockModelInfo.get_bedrock_route("bedrock_mantle/openai.gpt-5.5") != "mantle"
|
||||
assert BedrockModelInfo.get_bedrock_route("bedrock_mantle/openai.gpt-5.4") == "converse"
|
||||
assert BedrockModelInfo._explicit_mantle_route("bedrock_mantle/openai.gpt-5.5") is False
|
||||
|
||||
# A genuine mantle route still resolves, via the startswith branch...
|
||||
assert (
|
||||
BedrockModelInfo.get_bedrock_route("mantle/anthropic.claude-mythos-preview")
|
||||
== "mantle"
|
||||
)
|
||||
assert BedrockModelInfo.get_bedrock_route("mantle/anthropic.claude-mythos-preview") == "mantle"
|
||||
# ...and via the mid-path "/mantle/" branch (after the bedrock/ provider prefix).
|
||||
assert (
|
||||
BedrockModelInfo.get_bedrock_route(
|
||||
"bedrock/mantle/anthropic.claude-mythos-preview"
|
||||
)
|
||||
== "mantle"
|
||||
)
|
||||
assert BedrockModelInfo.get_bedrock_route("bedrock/mantle/anthropic.claude-mythos-preview") == "mantle"
|
||||
|
||||
|
||||
def test_model_has_route_prefix_exercises_both_branches():
|
||||
"""``_model_has_route_prefix`` matches on ``startswith`` or a ``/`` boundary only."""
|
||||
# startswith branch
|
||||
assert (
|
||||
BedrockModelInfo._model_has_route_prefix(
|
||||
"mantle/anthropic.claude-mythos-preview", "mantle/"
|
||||
)
|
||||
is True
|
||||
)
|
||||
assert BedrockModelInfo._model_has_route_prefix("mantle/anthropic.claude-mythos-preview", "mantle/") is True
|
||||
# f"/{prefix}" boundary branch
|
||||
assert (
|
||||
BedrockModelInfo._model_has_route_prefix(
|
||||
"bedrock/mantle/anthropic.claude-mythos-preview", "mantle/"
|
||||
)
|
||||
is True
|
||||
)
|
||||
assert BedrockModelInfo._model_has_route_prefix("bedrock/mantle/anthropic.claude-mythos-preview", "mantle/") is True
|
||||
# neither branch: the token only appears glued to another segment
|
||||
assert (
|
||||
BedrockModelInfo._model_has_route_prefix(
|
||||
"bedrock_mantle/openai.gpt-5.5", "mantle/"
|
||||
)
|
||||
is False
|
||||
)
|
||||
assert BedrockModelInfo._model_has_route_prefix("bedrock_mantle/openai.gpt-5.5", "mantle/") is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -457,16 +405,10 @@ def test_explicit_invoke_route_does_not_match_async_invoke():
|
|||
"""
|
||||
async_invoke_model = "async_invoke/twelvelabs.marengo-embed-2-7-v1:0"
|
||||
assert BedrockModelInfo._explicit_invoke_route(async_invoke_model) is False
|
||||
assert (
|
||||
BedrockModelInfo._explicit_invoke_route(f"bedrock/{async_invoke_model}")
|
||||
is False
|
||||
)
|
||||
assert BedrockModelInfo._explicit_invoke_route(f"bedrock/{async_invoke_model}") is False
|
||||
# ...while async_invoke/ is still detected as its own route.
|
||||
assert BedrockModelInfo._explicit_async_invoke_route(async_invoke_model) is True
|
||||
assert (
|
||||
BedrockModelInfo._explicit_async_invoke_route(f"bedrock/{async_invoke_model}")
|
||||
is True
|
||||
)
|
||||
assert BedrockModelInfo._explicit_async_invoke_route(f"bedrock/{async_invoke_model}") is True
|
||||
|
||||
|
||||
def test_capability_lookups_fall_back_to_base_model_when_regional_entry_lacks_field(monkeypatch):
|
||||
|
|
@ -1010,7 +952,9 @@ def test_unmapped_openai_family_model_routes_to_converse():
|
|||
_FORMAT_REJECTION: Final = "The model returned the following errors: output_config.format: Extra inputs are not permitted"
|
||||
|
||||
|
||||
_COMPILE_REJECTION: Final = "The model returned the following errors: compilation exceeded limits: Schema is too complex."
|
||||
_COMPILE_REJECTION: Final = (
|
||||
"The model returned the following errors: compilation exceeded limits: Schema is too complex."
|
||||
)
|
||||
|
||||
|
||||
_SCHEMA_PATTERN_REJECTION: Final = (
|
||||
|
|
@ -1076,4 +1020,3 @@ def test_without_bedrock_native_output_opt_in_turns_the_opt_in_off_and_keeps_the
|
|||
assert turned_off["api_base"] == "https://bedrock.example"
|
||||
assert deployment_model_info(turned_off)["id"] == "deployment-1"
|
||||
assert litellm_params["model_info"] == {"id": "deployment-1"}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ import asyncio
|
|||
import base64
|
||||
import contextlib
|
||||
import contextvars
|
||||
import copy
|
||||
import importlib
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import queue
|
||||
import re
|
||||
import threading
|
||||
from collections.abc import Callable, Iterator, Mapping
|
||||
from concurrent.futures import Future, ThreadPoolExecutor
|
||||
|
|
@ -36,6 +39,7 @@ from litellm.integrations.custom_guardrail import CustomGuardrail
|
|||
from litellm.integrations.custom_logger import CustomLogger
|
||||
from litellm.litellm_core_utils.get_litellm_params import get_litellm_params
|
||||
from litellm.litellm_core_utils.thread_pool_executor import executor as logging_executor
|
||||
from litellm.llms.base_llm.audio_transcription.transformation import BaseAudioTranscriptionConfig
|
||||
from litellm.llms.base_llm.base_model_iterator import MockResponseIterator
|
||||
from litellm.proxy.utils import is_valid_api_key
|
||||
from litellm.types.integrations.custom_logger import HEADROOM_CONVERTED_STREAM_KEY
|
||||
|
|
@ -70,12 +74,19 @@ from litellm.utils import (
|
|||
_is_streaming_request,
|
||||
_run_success_deployment_hook_on_converted_chat_stream,
|
||||
_snapshot_exception_for_hook,
|
||||
add_custom_logger_callback_to_specific_event,
|
||||
async_post_call_failure_deployment_hook,
|
||||
async_post_call_success_deployment_hook,
|
||||
calculate_max_parallel_requests,
|
||||
check_valid_key,
|
||||
client,
|
||||
filter_out_litellm_params,
|
||||
get_applied_guardrails,
|
||||
get_llm_provider,
|
||||
get_non_default_completion_params,
|
||||
get_optional_params_embeddings,
|
||||
get_optional_params_image_gen,
|
||||
get_potential_model_names,
|
||||
get_prompt_cache_min_tokens,
|
||||
is_cached_message,
|
||||
is_prompt_caching_valid_prompt,
|
||||
|
|
@ -3135,9 +3146,7 @@ class TestExtraBodyCannotOverrideModel:
|
|||
}
|
||||
]
|
||||
|
||||
untouched = litellm.get_optional_params(
|
||||
model="my-vllm-model", custom_llm_provider="hosted_vllm", tools=tools()
|
||||
)
|
||||
untouched = litellm.get_optional_params(model="my-vllm-model", custom_llm_provider="hosted_vllm", tools=tools())
|
||||
assert untouched["tools"][0]["function"]["custom_marker"] == "LEAK", untouched
|
||||
|
||||
result = litellm.get_optional_params(
|
||||
|
|
@ -4106,6 +4115,9 @@ def test_is_prompt_caching_valid_prompt_explicit_min_token_count_overrides_model
|
|||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def test_custom_logger_guards_ignore_subclass_instances(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Regression LIT-4392: the success/failure existence guards used isinstance, so a user
|
||||
subclass of a built-in logger already promoted into the callback lists made the guard
|
||||
|
|
@ -4517,7 +4529,13 @@ _SUCCESS_RESPONSES_BY_CALL_TYPE: Final = (
|
|||
pytest.param(EmbeddingResponse(model="text-embedding-3-small"), CallTypes.aembedding, id="embedding"),
|
||||
pytest.param(
|
||||
ResponsesAPIResponse(
|
||||
id="resp_abc", created_at=1, output=[], parallel_tool_calls=False, tool_choice="auto", tools=[], model="gpt-5.6"
|
||||
id="resp_abc",
|
||||
created_at=1,
|
||||
output=[],
|
||||
parallel_tool_calls=False,
|
||||
tool_choice="auto",
|
||||
tools=[],
|
||||
model="gpt-5.6",
|
||||
),
|
||||
CallTypes.aresponses,
|
||||
id="responses",
|
||||
|
|
@ -4545,7 +4563,9 @@ async def test_success_deployment_hook_raising_keeps_response_and_runs_later_hoo
|
|||
|
||||
assert result is response
|
||||
assert second_hook.seen_responses == (response,)
|
||||
failure_logs: Final = tuple(r for r in caplog.records if "async_post_call_success_deployment_hook error" in r.message)
|
||||
failure_logs: Final = tuple(
|
||||
r for r in caplog.records if "async_post_call_success_deployment_hook error" in r.message
|
||||
)
|
||||
assert len(failure_logs) == 1
|
||||
assert "_ChatShapedSuccessDeploymentHook" in failure_logs[0].message
|
||||
assert str(call_type) in failure_logs[0].message
|
||||
|
|
@ -4669,6 +4689,9 @@ async def test_wrapper_async_logs_converted_responses_stream_with_standard_loggi
|
|||
assert success_kwargs["stream"] is True
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wrapper_async_replays_cached_converted_chat_stream_as_stream(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import json
|
||||
import os
|
||||
import re
|
||||
from collections.abc import Collection, Iterator, Mapping
|
||||
from pathlib import Path
|
||||
from typing import Final, Literal, cast
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
|
@ -13,8 +15,6 @@ from litellm.llms.bedrock.common_utils import BedrockModelInfo
|
|||
from litellm.llms.custom_httpx.http_handler import HTTPHandler
|
||||
from litellm.types.utils import ModelInfoBase
|
||||
from litellm.utils import _invalidate_model_cost_lowercase_map
|
||||
import os
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
|
@ -169,9 +169,7 @@ def test_model_info_bedrock_converse(monkeypatch):
|
|||
except FileNotFoundError:
|
||||
pytest.skip("whitelisted_bedrock_models.txt not found")
|
||||
|
||||
_enforce_bedrock_converse_models(
|
||||
model_cost=litellm.model_cost, whitelist_models=whitelist_models
|
||||
)
|
||||
_enforce_bedrock_converse_models(model_cost=litellm.model_cost, whitelist_models=whitelist_models)
|
||||
|
||||
|
||||
@pytest.mark.flaky(retries=6, delay=2)
|
||||
|
|
@ -195,9 +193,7 @@ def test_model_info_bedrock_converse_enforcement(monkeypatch):
|
|||
|
||||
# Check for unwhitelisted models
|
||||
with pytest.raises(AssertionError, match=r"fake\.bedrock-chat-model"):
|
||||
_enforce_bedrock_converse_models(
|
||||
model_cost=litellm.model_cost, whitelist_models=whitelist_models
|
||||
)
|
||||
_enforce_bedrock_converse_models(model_cost=litellm.model_cost, whitelist_models=whitelist_models)
|
||||
except FileNotFoundError as e:
|
||||
pytest.skip("whitelisted_bedrock_models.txt not found")
|
||||
|
||||
|
|
@ -256,11 +252,7 @@ def test_get_model_info_bedrock_models():
|
|||
k = k.replace(f"{commitment}/", "")
|
||||
base_model = BedrockModelInfo.get_base_model(k)
|
||||
# get_base_model() returns model id without "bedrock/" prefix; cost map keys use "bedrock/<model>"
|
||||
base_model_key = (
|
||||
base_model
|
||||
if base_model in litellm.model_cost
|
||||
else f"bedrock/{base_model}"
|
||||
)
|
||||
base_model_key = base_model if base_model in litellm.model_cost else f"bedrock/{base_model}"
|
||||
if base_model_key not in litellm.model_cost:
|
||||
continue
|
||||
base_model_info = litellm.model_cost[base_model_key]
|
||||
|
|
@ -268,15 +260,13 @@ def test_get_model_info_bedrock_models():
|
|||
if "invoke/" in k:
|
||||
continue
|
||||
if base_model_key.startswith("supports_"):
|
||||
assert (
|
||||
base_model_key in v
|
||||
), f"{base_model_key} is not in model cost map for {k}"
|
||||
assert base_model_key in v, f"{base_model_key} is not in model cost map for {k}"
|
||||
if _govcloud_may_lag(k, base_model_key):
|
||||
assert base_model_value or not v[base_model_key], f"{base_model_key} is on {k} but not its base"
|
||||
continue
|
||||
assert (
|
||||
v[base_model_key] == base_model_value
|
||||
), f"{base_model_key} is not equal to {base_model_value} for model {k}"
|
||||
assert v[base_model_key] == base_model_value, (
|
||||
f"{base_model_key} is not equal to {base_model_value} for model {k}"
|
||||
)
|
||||
|
||||
|
||||
def _cross_region_base_model_key(
|
||||
|
|
@ -299,10 +289,7 @@ def _cross_region_profiles(
|
|||
(model_key, model_info, base_model_key)
|
||||
for model_key, model_info in model_cost.items()
|
||||
if str(model_info.get("litellm_provider", "")).startswith("bedrock")
|
||||
and (
|
||||
base_model_key := _cross_region_base_model_key(model_key, prefixes, model_cost)
|
||||
)
|
||||
is not None
|
||||
and (base_model_key := _cross_region_base_model_key(model_key, prefixes, model_cost)) is not None
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -336,9 +323,7 @@ def test_get_model_info_bedrock_cross_region_capability_parity():
|
|||
if _govcloud_may_lag(k, cap):
|
||||
assert base_value or not v[cap], f"{cap} is on {k} but not on {base_model_key}"
|
||||
continue
|
||||
assert (
|
||||
v[cap] == base_value
|
||||
), f"{cap} is {v[cap]} on {k} but {base_value} on {base_model_key}"
|
||||
assert v[cap] == base_value, f"{cap} is {v[cap]} on {k} but {base_value} on {base_model_key}"
|
||||
|
||||
assert checked > 0, "no cross-region bedrock profiles found - the filter is inert"
|
||||
|
||||
|
|
@ -367,9 +352,7 @@ def test_get_model_info_bedrock_priced_cross_region_profile_has_priced_base():
|
|||
base = litellm.model_cost[base_model_key]
|
||||
for cost_key in ("input_cost_per_token", "output_cost_per_token"):
|
||||
if (v.get(cost_key) or 0) > 0:
|
||||
assert (
|
||||
base.get(cost_key) or 0
|
||||
) > 0, f"{k} charges {cost_key} but its base {base_model_key} is free"
|
||||
assert (base.get(cost_key) or 0) > 0, f"{k} charges {cost_key} but its base {base_model_key} is free"
|
||||
|
||||
assert checked > 0, "no cross-region bedrock profiles found - the filter is inert"
|
||||
|
||||
|
|
@ -401,23 +384,17 @@ def test_get_model_info_case_insensitive_lookup(monkeypatch):
|
|||
)
|
||||
|
||||
# Test 1: Exact case should work
|
||||
info = litellm.get_model_info(
|
||||
model="Qwen/Qwen3-Next-80B-A3B-Thinking", custom_llm_provider="together_ai"
|
||||
)
|
||||
info = litellm.get_model_info(model="Qwen/Qwen3-Next-80B-A3B-Thinking", custom_llm_provider="together_ai")
|
||||
assert info is not None
|
||||
assert info["supports_function_calling"] is True
|
||||
|
||||
# Test 2: Lowercase should also work (case-insensitive lookup)
|
||||
info_lower = litellm.get_model_info(
|
||||
model="qwen/qwen3-next-80b-a3b-thinking", custom_llm_provider="together_ai"
|
||||
)
|
||||
info_lower = litellm.get_model_info(model="qwen/qwen3-next-80b-a3b-thinking", custom_llm_provider="together_ai")
|
||||
assert info_lower is not None
|
||||
assert info_lower["supports_function_calling"] is True
|
||||
|
||||
# Test 3: Mixed case should also work
|
||||
info_mixed = litellm.get_model_info(
|
||||
model="QWEN/qwen3-NEXT-80b-a3b-thinking", custom_llm_provider="together_ai"
|
||||
)
|
||||
info_mixed = litellm.get_model_info(model="QWEN/qwen3-NEXT-80b-a3b-thinking", custom_llm_provider="together_ai")
|
||||
assert info_mixed is not None
|
||||
assert info_mixed["supports_function_calling"] is True
|
||||
|
||||
|
|
@ -445,21 +422,14 @@ def test_get_model_info_case_insensitive_supports_function_calling(monkeypatch):
|
|||
from litellm.utils import supports_function_calling
|
||||
|
||||
# Exact case
|
||||
assert (
|
||||
supports_function_calling("TestModel-ABC", custom_llm_provider="test_provider")
|
||||
is True
|
||||
)
|
||||
assert supports_function_calling("TestModel-ABC", custom_llm_provider="test_provider") is True
|
||||
|
||||
# Lowercase (should now work with case-insensitive lookup)
|
||||
assert (
|
||||
supports_function_calling("testmodel-abc", custom_llm_provider="test_provider")
|
||||
is True
|
||||
)
|
||||
assert supports_function_calling("testmodel-abc", custom_llm_provider="test_provider") is True
|
||||
|
||||
|
||||
def test_get_model_info_custom_model_router():
|
||||
from litellm import Router
|
||||
from litellm import get_model_info
|
||||
from litellm import Router, get_model_info
|
||||
|
||||
litellm.turn_on_debug()
|
||||
|
||||
|
|
@ -517,6 +487,4 @@ def test_get_model_info_custom_provider():
|
|||
# Get registered model info
|
||||
from litellm import get_model_info
|
||||
|
||||
get_model_info(
|
||||
model="my-custom-llm/my-fake-model"
|
||||
) # 💥 "Exception: This model isn't mapped yet." in v1.56.10
|
||||
get_model_info(model="my-custom-llm/my-fake-model") # 💥 "Exception: This model isn't mapped yet." in v1.56.10
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
Tests for litellm.types.completion module
|
||||
|
||||
This test suite validates the CompletionRequest model and its compatibility with
|
||||
This test suite validates the CompletionRequest model and its compatibility with
|
||||
OpenAI ChatCompletion API message formats.
|
||||
|
||||
Usage:
|
||||
|
|
@ -102,9 +102,7 @@ def test_completion_request_multimodal_content():
|
|||
{"type": "text", "text": "What's in this image?"},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD..."
|
||||
},
|
||||
"image_url": {"url": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD..."},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue