mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-13 08:52:02 +00:00
test(bedrock): guard the line's structured output path and sync stream errors
The integration tests that expected an open-object schema to move to the JSON tool on a native model now expect it to stay native, as the line does. A Bedrock /v1/messages test checks a row-native profile forwards the caller's schema unchanged, and a Converse handler test checks a sync stream error keeps its status class and the line's raw body.
This commit is contained in:
parent
040c8ab5bf
commit
9daff6de4c
6 changed files with 68 additions and 41 deletions
|
|
@ -755,9 +755,7 @@ def test_native_schema_reaches_bedrock_with_limits_in_descriptions_and_every_obj
|
|||
|
||||
|
||||
@pytest.mark.parametrize("route", ["converse", "invoke"])
|
||||
def test_open_object_schema_takes_the_json_tool_on_a_native_model_and_keeps_the_dict_open(
|
||||
gateway: Gateway, route: str
|
||||
) -> None:
|
||||
def test_open_object_schema_stays_native_on_a_model_native_on_every_request(gateway: Gateway, route: str) -> None:
|
||||
marker, marker_hex = _marker()
|
||||
with wire_server(_bedrock) as wire, gateway.scenario() as scenario:
|
||||
model: Final = _deployment(scenario, wire, route, _SONNET)
|
||||
|
|
@ -767,9 +765,10 @@ def test_open_object_schema_takes_the_json_tool_on_a_native_model_and_keeps_the_
|
|||
assert _content_json(_chat_message(response)) == _payload(marker_hex), response.text
|
||||
_, body = _one_call(wire, marker_hex)
|
||||
invoke: Final = route == "invoke"
|
||||
assert _native_schema(body, invoke) is None, body
|
||||
tool_schema: Final = _tools(body, invoke)[_JSON_TOOL]
|
||||
assert object_value(tool_schema["properties"])["counts"] == _OPEN_COUNTS, tool_schema
|
||||
schema: Final = _native_schema(body, invoke)
|
||||
assert schema is not None, body
|
||||
assert object_value(schema["properties"]).keys() == {"store", "counts"}, schema
|
||||
assert _JSON_TOOL not in _tools(body, invoke), body
|
||||
|
||||
|
||||
def test_thinking_with_a_caller_tool_on_a_bedrock_native_model_sends_only_the_callers_tool(gateway: Gateway) -> None:
|
||||
|
|
|
|||
|
|
@ -1451,27 +1451,13 @@ def _native_deployment(gateway: Gateway, scenario: Scenario, api_base: str, prov
|
|||
raise AssertionError(provider)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provider", ["anthropic", "azure_ai"])
|
||||
def test_native_model_json_schema_with_an_open_object_takes_the_json_tool_and_keeps_the_object_open(
|
||||
gateway: Gateway, provider: str
|
||||
) -> None:
|
||||
marker, marker_hex = _marker("values")
|
||||
with wire_server(_peer) as wire, gateway.scenario() as scenario:
|
||||
model: Final = _native_deployment(gateway, scenario, wire.url, provider, _FORCED_TOOL_NATIVE_MODEL)
|
||||
response: Final = gateway.request(
|
||||
"POST", "/v1/chat/completions", _chat_body(model, marker, response_format=_VALUES_RESPONSE_FORMAT)
|
||||
)
|
||||
assert _chat_content(response) == _tool_input("values", marker_hex), response.text
|
||||
sent: Final = _one_delivery(wire, marker_hex)
|
||||
assert not _structured(sent.body), sent.body
|
||||
assert _json_tool_schema(sent)["properties"] == _VALUES_SCHEMA["properties"], sent.body
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("provider", "backend", "extra"),
|
||||
[
|
||||
("anthropic", _NATIVE_MODEL, {}),
|
||||
("vertex_ai", _NATIVE_MODEL, {}),
|
||||
("anthropic", _FORCED_TOOL_NATIVE_MODEL, {}),
|
||||
("azure_ai", _FORCED_TOOL_NATIVE_MODEL, {}),
|
||||
("anthropic", _FORCED_TOOL_NATIVE_MODEL, {"thinking": {"type": "adaptive"}}),
|
||||
("anthropic", _FORCED_TOOL_NATIVE_MODEL, {"tools": [_USER_TOOL_SPEC]}),
|
||||
("anthropic", _FORCED_TOOL_NATIVE_MODEL, {"reasoning_effort": "none"}),
|
||||
|
|
@ -1480,13 +1466,15 @@ def test_native_model_json_schema_with_an_open_object_takes_the_json_tool_and_ke
|
|||
ids=[
|
||||
"forced_tool_refused-anthropic",
|
||||
"forced_tool_refused-vertex_ai",
|
||||
"plain-anthropic",
|
||||
"plain-azure_ai",
|
||||
"thinking",
|
||||
"caller_tools",
|
||||
"effort_none-anthropic",
|
||||
"effort_none-azure_ai",
|
||||
],
|
||||
)
|
||||
def test_open_object_schema_keeps_native_output_when_the_json_tool_cannot_be_forced(
|
||||
def test_open_object_schema_keeps_native_output_on_a_native_model(
|
||||
gateway: Gateway, provider: str, backend: str, extra: dict[str, JsonValue]
|
||||
) -> None:
|
||||
marker, marker_hex = _marker("values")
|
||||
|
|
|
|||
|
|
@ -633,22 +633,22 @@ DEFINITIONS_REF_SCHEMA: Final = {
|
|||
def test_model_native_on_every_request_sends_every_schema_natively_without_the_json_tool(
|
||||
set_native_structured_output, schema, request_params
|
||||
):
|
||||
model = "us.anthropic.claude-opus-4-8"
|
||||
model: Final = "us.anthropic.claude-opus-4-8"
|
||||
set_native_structured_output(model, "legacy")
|
||||
|
||||
result = _invoke_params_for_schema(model, schema, request_params)
|
||||
result: Final = _invoke_params_for_schema(model, schema, request_params)
|
||||
|
||||
assert result["output_format"]["type"] == "json_schema"
|
||||
assert "json_tool_call" not in [tool["name"] for tool in result.get("tools", [])]
|
||||
|
||||
|
||||
def test_definitions_ref_reaches_a_model_native_on_every_request_inlined(set_native_structured_output):
|
||||
model = "us.anthropic.claude-opus-4-8"
|
||||
model: Final = "us.anthropic.claude-opus-4-8"
|
||||
set_native_structured_output(model, "legacy")
|
||||
|
||||
result = _invoke_params_for_schema(model, DEFINITIONS_REF_SCHEMA, {})
|
||||
result: Final = _invoke_params_for_schema(model, DEFINITIONS_REF_SCHEMA, {})
|
||||
|
||||
sent = result["output_format"]["schema"]
|
||||
sent: Final = result["output_format"]["schema"]
|
||||
assert sent["properties"]["home"]["properties"]["city"] == {"type": "string"}
|
||||
assert "$ref" not in json.dumps(sent)
|
||||
|
||||
|
|
@ -657,10 +657,10 @@ def test_definitions_ref_reaches_a_model_native_on_every_request_inlined(set_nat
|
|||
def test_open_object_schema_stays_native_beside_the_callers_tools_on_a_model_native_on_every_request(
|
||||
set_native_structured_output, thinking_params
|
||||
):
|
||||
model = "us.anthropic.claude-opus-4-8"
|
||||
model: Final = "us.anthropic.claude-opus-4-8"
|
||||
set_native_structured_output(model, "legacy")
|
||||
|
||||
result = _invoke_params_for_schema(model, OPEN_OBJECT_SCHEMA, {**thinking_params, "tools": [LOOKUP_TOOL]})
|
||||
result: Final = _invoke_params_for_schema(model, OPEN_OBJECT_SCHEMA, {**thinking_params, "tools": [LOOKUP_TOOL]})
|
||||
|
||||
assert "tool_choice" not in result
|
||||
assert [tool["name"] for tool in result["tools"]] == ["lookup"]
|
||||
|
|
@ -670,10 +670,10 @@ def test_open_object_schema_stays_native_beside_the_callers_tools_on_a_model_nat
|
|||
def test_open_object_schema_keeps_the_json_tool_beside_the_callers_tools_on_an_opt_in_model(
|
||||
set_native_structured_output,
|
||||
):
|
||||
model = "us.anthropic.claude-opus-4-8"
|
||||
model: Final = "us.anthropic.claude-opus-4-8"
|
||||
set_native_structured_output(model, "bedrock")
|
||||
|
||||
result = _invoke_params_for_schema(
|
||||
result: Final = _invoke_params_for_schema(
|
||||
model, OPEN_OBJECT_SCHEMA, {"thinking": {"type": "adaptive"}, "tools": [LOOKUP_TOOL]}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -346,3 +346,32 @@ def test_converse_profile_override_records_cost_without_changing_response_model(
|
|||
+ CONVERSE_RESPONSE["usage"]["outputTokens"] * prices["output_cost_per_token"]
|
||||
)
|
||||
|
||||
|
||||
_INVALID_REQUEST_BODY: Final = b'{"message":"The request is invalid."}'
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("status_code", "error_class"),
|
||||
[(400, litellm.BadRequestError), (429, litellm.RateLimitError), (503, litellm.ServiceUnavailableError)],
|
||||
)
|
||||
def test_sync_converse_stream_error_keeps_the_status_class_and_raw_body(
|
||||
status_code: int, error_class: type[Exception]
|
||||
) -> None:
|
||||
def respond(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(status_code, content=_INVALID_REQUEST_BODY)
|
||||
|
||||
with httpx.Client(transport=httpx.MockTransport(respond)) as transport:
|
||||
with pytest.raises(error_class) as raised:
|
||||
litellm.completion(
|
||||
model="bedrock/converse/us.anthropic.claude-sonnet-4-5-20250929-v1:0",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
stream=True,
|
||||
num_retries=0,
|
||||
aws_access_key_id="test-key",
|
||||
aws_secret_access_key="test-secret",
|
||||
aws_region_name="us-east-1",
|
||||
client=HTTPHandler(client=transport),
|
||||
)
|
||||
|
||||
assert raised.value.status_code == status_code
|
||||
assert repr(_INVALID_REQUEST_BODY) in raised.value.message
|
||||
|
|
|
|||
|
|
@ -3270,6 +3270,14 @@ MESSAGES_OPEN_OBJECT_SCHEMA: Final = {
|
|||
"type": "object",
|
||||
"properties": {"counts": {"type": "object", "additionalProperties": {"type": "integer"}}},
|
||||
}
|
||||
MESSAGES_KEYWORDS_SCHEMA: Final = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"count": {"type": "integer", "minimum": 1},
|
||||
"ids": {"type": "array", "items": {"type": "string"}, "uniqueItems": True},
|
||||
},
|
||||
"required": ["count", "ids"],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -3288,9 +3296,11 @@ def test_bedrock_messages_schema_native_output_cannot_hold_is_inlined_on_a_bedro
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"schema", [MESSAGES_TREE_SCHEMA, MESSAGES_OPEN_OBJECT_SCHEMA], ids=["recursive", "open-object"]
|
||||
"schema",
|
||||
[MESSAGES_TREE_SCHEMA, MESSAGES_OPEN_OBJECT_SCHEMA, MESSAGES_KEYWORDS_SCHEMA],
|
||||
ids=["recursive", "open-object", "keywords"],
|
||||
)
|
||||
def test_bedrock_messages_schema_native_output_cannot_hold_is_forwarded_on_a_legacy_native_profile(
|
||||
def test_bedrock_messages_forwards_the_callers_schema_unchanged_on_a_legacy_native_profile(
|
||||
set_native_structured_output, schema
|
||||
):
|
||||
model: Final = "us.anthropic.claude-sonnet-4-6"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ association via `aws_bedrock_project_id`.
|
|||
"""
|
||||
|
||||
import json
|
||||
from typing import Final
|
||||
from unittest.mock import patch
|
||||
|
||||
import httpx
|
||||
|
|
@ -391,25 +392,25 @@ def test_mantle_messages_validate_environment_without_project_id():
|
|||
assert _MANTLE_WORKSPACE_HEADER not in headers
|
||||
|
||||
|
||||
_MANTLE_CONFIGS = pytest.mark.parametrize(
|
||||
_MANTLE_CONFIGS: Final = pytest.mark.parametrize(
|
||||
"config", [AmazonMantleConfig(), BedrockMantleClaudeChatConfig()], ids=["bedrock_mantle_route", "mantle_provider"]
|
||||
)
|
||||
_MANTLE_MODEL = "anthropic.claude-opus-4-8"
|
||||
_CLOSED_RESPONSE_FORMAT = {
|
||||
_MANTLE_MODEL: Final = "anthropic.claude-opus-4-8"
|
||||
_CLOSED_RESPONSE_FORMAT: Final = {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "out",
|
||||
"schema": {"type": "object", "properties": {"a": {"type": "string"}}, "required": ["a"]},
|
||||
},
|
||||
}
|
||||
_OPEN_RESPONSE_FORMAT = {
|
||||
_OPEN_RESPONSE_FORMAT: Final = {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "out",
|
||||
"schema": {"type": "object", "properties": {"counts": {"type": "object", "additionalProperties": True}}},
|
||||
},
|
||||
}
|
||||
_LOOKUP_TOOL = {
|
||||
_LOOKUP_TOOL: Final = {
|
||||
"type": "function",
|
||||
"function": {"name": "lookup", "parameters": {"type": "object", "properties": {"id": {"type": "string"}}}},
|
||||
}
|
||||
|
|
@ -419,7 +420,7 @@ _LOOKUP_TOOL = {
|
|||
def test_mantle_opt_in_model_keeps_the_json_tool_when_thinking_is_requested(set_native_structured_output, config):
|
||||
set_native_structured_output(_MANTLE_MODEL, "bedrock")
|
||||
|
||||
result = config.map_openai_params(
|
||||
result: Final = config.map_openai_params(
|
||||
non_default_params={"response_format": _CLOSED_RESPONSE_FORMAT, "thinking": {"type": "adaptive"}},
|
||||
optional_params={},
|
||||
model=_MANTLE_MODEL,
|
||||
|
|
@ -436,7 +437,7 @@ def test_mantle_model_native_on_every_request_stays_native_beside_the_callers_to
|
|||
):
|
||||
set_native_structured_output(_MANTLE_MODEL, "legacy")
|
||||
|
||||
result = config.map_openai_params(
|
||||
result: Final = config.map_openai_params(
|
||||
non_default_params={"response_format": _OPEN_RESPONSE_FORMAT, "tools": [_LOOKUP_TOOL]},
|
||||
optional_params={},
|
||||
model=_MANTLE_MODEL,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue