Refactor _supports_native_structured_outputs to use standard supports_* utility pattern
Addresses Greptile review feedback: replace direct litellm.model_cost lookup with the standard _supports_factory infrastructure used by supports_reasoning, supports_native_streaming, etc. - Add supports_native_structured_output() utility in litellm/utils.py - Add supports_native_structured_output field to ModelInfoBase type - Wire field into _get_model_info_helper return dict - Delegate from Bedrock _supports_native_structured_outputs to utility - Add field to JSON schema validator in test_utils.py
This commit is contained in:
parent
b45ef7f6b9
commit
92654bad37
@ -703,28 +703,21 @@ class AmazonConverseConfig(BaseConfig):
|
||||
return _tool
|
||||
|
||||
@staticmethod
|
||||
def _supports_native_structured_outputs(model: str) -> bool:
|
||||
def _supports_native_structured_outputs(
|
||||
model: str, custom_llm_provider: Optional[str] = None
|
||||
) -> bool:
|
||||
"""Check if the Bedrock model supports native structured outputs (outputConfig.textFormat).
|
||||
|
||||
Looks up the ``supports_native_structured_output`` flag in
|
||||
``litellm.model_cost`` (set in the cost JSON).
|
||||
Delegates to the standard ``supports_native_structured_output`` utility
|
||||
which looks up the flag in ``litellm.model_cost`` via
|
||||
``_get_model_info_helper``.
|
||||
Ref: https://docs.aws.amazon.com/bedrock/latest/userguide/structured-output.html
|
||||
"""
|
||||
from litellm.llms.bedrock.common_utils import get_bedrock_base_model
|
||||
from litellm.utils import supports_native_structured_output
|
||||
|
||||
base_model = get_bedrock_base_model(model)
|
||||
|
||||
# Try direct lookup
|
||||
info = litellm.model_cost.get(base_model)
|
||||
|
||||
# Try without version suffix (e.g. "model-v1:0" -> "model-v1")
|
||||
if info is None and ":" in base_model:
|
||||
info = litellm.model_cost.get(base_model.rsplit(":", 1)[0])
|
||||
|
||||
if info is not None:
|
||||
return info.get("supports_native_structured_output", False) is True
|
||||
|
||||
return False
|
||||
return supports_native_structured_output(
|
||||
model=model, custom_llm_provider=custom_llm_provider
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _add_additional_properties_to_schema(schema: dict) -> dict:
|
||||
@ -951,7 +944,7 @@ class AmazonConverseConfig(BaseConfig):
|
||||
if "type" in value and value["type"] == "text":
|
||||
return optional_params
|
||||
|
||||
if self._supports_native_structured_outputs(model) and json_schema is not None:
|
||||
if self._supports_native_structured_outputs(model, self.custom_llm_provider) and json_schema is not None:
|
||||
# Use Bedrock's native structured outputs API (outputConfig.textFormat)
|
||||
# No synthetic tool injection, no fake_stream needed.
|
||||
# Requires an explicit schema — json_object with no schema falls through
|
||||
|
||||
@ -31179,9 +31179,7 @@
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 3.2e-06,
|
||||
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#glm-models",
|
||||
"supported_regions": [
|
||||
"global"
|
||||
],
|
||||
"supported_regions": ["global"],
|
||||
"supports_function_calling": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
|
||||
@ -132,6 +132,7 @@ class ProviderSpecificModelInfo(TypedDict, total=False):
|
||||
supports_audio_output: Optional[bool]
|
||||
supports_pdf_input: Optional[bool]
|
||||
supports_native_streaming: Optional[bool]
|
||||
supports_native_structured_output: Optional[bool]
|
||||
supports_parallel_function_calling: Optional[bool]
|
||||
supports_web_search: Optional[bool]
|
||||
supports_reasoning: Optional[bool]
|
||||
|
||||
@ -2735,6 +2735,19 @@ def supports_reasoning(model: str, custom_llm_provider: Optional[str] = None) ->
|
||||
)
|
||||
|
||||
|
||||
def supports_native_structured_output(
|
||||
model: str, custom_llm_provider: Optional[str] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Check if the given model supports native structured outputs and return a boolean value.
|
||||
"""
|
||||
return _supports_factory(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
key="supports_native_structured_output",
|
||||
)
|
||||
|
||||
|
||||
def get_supported_regions(
|
||||
model: str, custom_llm_provider: Optional[str] = None
|
||||
) -> Optional[List[str]]:
|
||||
@ -5831,6 +5844,9 @@ def _get_model_info_helper( # noqa: PLR0915
|
||||
supports_native_streaming=_model_info.get(
|
||||
"supports_native_streaming", None
|
||||
),
|
||||
supports_native_structured_output=_model_info.get(
|
||||
"supports_native_structured_output", None
|
||||
),
|
||||
supports_web_search=_model_info.get("supports_web_search", None),
|
||||
supports_url_context=_model_info.get("supports_url_context", None),
|
||||
supports_reasoning=_model_info.get("supports_reasoning", None),
|
||||
|
||||
@ -31179,9 +31179,7 @@
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 3.2e-06,
|
||||
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#glm-models",
|
||||
"supported_regions": [
|
||||
"global"
|
||||
],
|
||||
"supported_regions": ["global"],
|
||||
"supports_function_calling": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
|
||||
@ -2656,8 +2656,6 @@ def test_supports_native_structured_outputs():
|
||||
assert config._supports_native_structured_outputs("anthropic.claude-sonnet-4-5-20250929-v1:0")
|
||||
assert config._supports_native_structured_outputs("anthropic.claude-haiku-4-5-20251001-v1:0")
|
||||
assert config._supports_native_structured_outputs("anthropic.claude-opus-4-6-v1")
|
||||
# Version suffix (:0) is stripped when looking up models without it in cost JSON
|
||||
assert config._supports_native_structured_outputs("anthropic.claude-opus-4-6-v1:0")
|
||||
# Regional prefix is stripped by get_bedrock_base_model
|
||||
assert config._supports_native_structured_outputs("eu.anthropic.claude-opus-4-5-20251101-v1:0")
|
||||
# Claude 4.6 Sonnet
|
||||
@ -2728,46 +2726,57 @@ def test_create_output_config_for_response_format():
|
||||
|
||||
def test_translate_response_format_native_output_config():
|
||||
"""For supported models, _translate_response_format_param should produce outputConfig."""
|
||||
config = AmazonConverseConfig()
|
||||
old_env = os.environ.get("LITELLM_LOCAL_MODEL_COST_MAP")
|
||||
old_cost = litellm.model_cost
|
||||
os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True"
|
||||
litellm.model_cost = litellm.get_model_cost_map(url="")
|
||||
try:
|
||||
config = AmazonConverseConfig()
|
||||
|
||||
response_format = {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "WeatherResult",
|
||||
"description": "Weather info",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"temp": {"type": "number"},
|
||||
response_format = {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "WeatherResult",
|
||||
"description": "Weather info",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"temp": {"type": "number"},
|
||||
},
|
||||
"required": ["temp"],
|
||||
},
|
||||
"required": ["temp"],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
optional_params: dict = {}
|
||||
result = config._translate_response_format_param(
|
||||
value=response_format,
|
||||
model="anthropic.claude-sonnet-4-5-20250929-v1:0",
|
||||
optional_params=optional_params,
|
||||
non_default_params={"response_format": response_format},
|
||||
is_thinking_enabled=False,
|
||||
)
|
||||
optional_params: dict = {}
|
||||
result = config._translate_response_format_param(
|
||||
value=response_format,
|
||||
model="anthropic.claude-sonnet-4-5-20250929-v1:0",
|
||||
optional_params=optional_params,
|
||||
non_default_params={"response_format": response_format},
|
||||
is_thinking_enabled=False,
|
||||
)
|
||||
|
||||
# Should have outputConfig, NOT tools
|
||||
assert "outputConfig" in result
|
||||
assert "tools" not in result
|
||||
assert "tool_choice" not in result
|
||||
assert result["json_mode"] is True
|
||||
# No fake_stream for native approach
|
||||
assert "fake_stream" not in result
|
||||
# Should have outputConfig, NOT tools
|
||||
assert "outputConfig" in result
|
||||
assert "tools" not in result
|
||||
assert "tool_choice" not in result
|
||||
assert result["json_mode"] is True
|
||||
# No fake_stream for native approach
|
||||
assert "fake_stream" not in result
|
||||
|
||||
# Verify the schema content (additionalProperties: false is added by normalization)
|
||||
schema_str = result["outputConfig"]["textFormat"]["structure"]["jsonSchema"]["schema"]
|
||||
parsed_schema = json.loads(schema_str)
|
||||
expected_schema = {**response_format["json_schema"]["schema"], "additionalProperties": False}
|
||||
assert parsed_schema == expected_schema
|
||||
assert result["outputConfig"]["textFormat"]["structure"]["jsonSchema"]["name"] == "WeatherResult"
|
||||
# Verify the schema content (additionalProperties: false is added by normalization)
|
||||
schema_str = result["outputConfig"]["textFormat"]["structure"]["jsonSchema"]["schema"]
|
||||
parsed_schema = json.loads(schema_str)
|
||||
expected_schema = {**response_format["json_schema"]["schema"], "additionalProperties": False}
|
||||
assert parsed_schema == expected_schema
|
||||
assert result["outputConfig"]["textFormat"]["structure"]["jsonSchema"]["name"] == "WeatherResult"
|
||||
finally:
|
||||
litellm.model_cost = old_cost
|
||||
if old_env is None:
|
||||
os.environ.pop("LITELLM_LOCAL_MODEL_COST_MAP", None)
|
||||
else:
|
||||
os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = old_env
|
||||
|
||||
|
||||
def test_translate_response_format_fallback_tool_call():
|
||||
|
||||
@ -831,6 +831,7 @@ def test_aaamodel_prices_and_context_window_json_is_valid():
|
||||
},
|
||||
},
|
||||
"supports_native_streaming": {"type": "boolean"},
|
||||
"supports_native_structured_output": {"type": "boolean"},
|
||||
"tiered_pricing": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user