From 34c93645e9aaed8546032ef52ff71b54b264e454 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:30:06 -0700 Subject: [PATCH 1/4] fix(openai): gpt-5.5 does not support reasoning_effort=minimal Verified against OpenAI's live Chat Completions API on 2026-04-24: POST /v1/chat/completions {"model": "gpt-5.5", "reasoning_effort": "minimal", ...} -> 400 Unsupported value: 'reasoning_effort' does not support 'minimal' with this model. Supported values are: 'none', 'low', 'medium', 'high', and 'xhigh'. POST /v1/chat/completions {"model": "gpt-5.5-pro", "reasoning_effort": "minimal", ...} -> 400 Unsupported value: 'minimal' is not supported with the 'gpt-5.5-pro' model. Supported values are: 'medium', 'high', and 'xhigh'. Set supports_minimal_reasoning_effort=false on all four entries (gpt-5.5, gpt-5.5-2026-04-23, gpt-5.5-pro, gpt-5.5-pro-2026-04-23) so OpenAIGPT5Config._is_reasoning_effort_level_explicitly_disabled fires and LiteLLM either drops the param (drop_params=True) or raises a local UnsupportedParamsError, instead of round-tripping to OpenAI for a 400. Adds a parametrized test_gpt55_reasoning_effort_flags_match_live_openai_api test that pins supports_{none,minimal,xhigh}_reasoning_effort on each entry to OpenAI's actual API contract. Note: gpt-5.5-pro additionally rejects 'none' and 'low'. 'none' is already handled (supports_none_reasoning_effort=false). 'low' is not representable in the current JSON schema (no supports_low flag); filing separately. --- ...odel_prices_and_context_window_backup.json | 8 ++-- model_prices_and_context_window.json | 8 ++-- .../llm_cost_calc/test_llm_cost_calc_utils.py | 40 +++++++++++++++++++ 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 49ce5022c56..ffe5b47bac7 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -19319,7 +19319,7 @@ "supports_web_search": true, "supports_none_reasoning_effort": true, "supports_xhigh_reasoning_effort": true, - "supports_minimal_reasoning_effort": true + "supports_minimal_reasoning_effort": false }, "gpt-5.5-2026-04-23": { "cache_read_input_token_cost": 5e-07, @@ -19367,7 +19367,7 @@ "supports_web_search": true, "supports_none_reasoning_effort": true, "supports_xhigh_reasoning_effort": true, - "supports_minimal_reasoning_effort": true + "supports_minimal_reasoning_effort": false }, "gpt-5.5-pro": { "cache_read_input_token_cost": 6e-06, @@ -19410,7 +19410,7 @@ "supports_web_search": true, "supports_none_reasoning_effort": false, "supports_xhigh_reasoning_effort": true, - "supports_minimal_reasoning_effort": true + "supports_minimal_reasoning_effort": false }, "gpt-5.5-pro-2026-04-23": { "cache_read_input_token_cost": 6e-06, @@ -19453,7 +19453,7 @@ "supports_web_search": true, "supports_none_reasoning_effort": false, "supports_xhigh_reasoning_effort": true, - "supports_minimal_reasoning_effort": true + "supports_minimal_reasoning_effort": false }, "gpt-5.4": { "cache_read_input_token_cost": 2.5e-07, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 3733f07a30d..39f85650d1f 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -19333,7 +19333,7 @@ "supports_web_search": true, "supports_none_reasoning_effort": true, "supports_xhigh_reasoning_effort": true, - "supports_minimal_reasoning_effort": true + "supports_minimal_reasoning_effort": false }, "gpt-5.5-2026-04-23": { "cache_read_input_token_cost": 5e-07, @@ -19381,7 +19381,7 @@ "supports_web_search": true, "supports_none_reasoning_effort": true, "supports_xhigh_reasoning_effort": true, - "supports_minimal_reasoning_effort": true + "supports_minimal_reasoning_effort": false }, "gpt-5.5-pro": { "cache_read_input_token_cost": 6e-06, @@ -19424,7 +19424,7 @@ "supports_web_search": true, "supports_none_reasoning_effort": false, "supports_xhigh_reasoning_effort": true, - "supports_minimal_reasoning_effort": true + "supports_minimal_reasoning_effort": false }, "gpt-5.5-pro-2026-04-23": { "cache_read_input_token_cost": 6e-06, @@ -19467,7 +19467,7 @@ "supports_web_search": true, "supports_none_reasoning_effort": false, "supports_xhigh_reasoning_effort": true, - "supports_minimal_reasoning_effort": true + "supports_minimal_reasoning_effort": false }, "gpt-5.4": { "cache_read_input_token_cost": 2.5e-07, diff --git a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py index 5e37e2a3424..8b1b39848ea 100644 --- a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py +++ b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py @@ -411,6 +411,46 @@ def test_generic_cost_per_token_gpt55_pro(): ) +@pytest.mark.parametrize( + "model,expected_none,expected_xhigh,expected_minimal", + [ + # Verified against OpenAI's live API on 2026-04-24: + # gpt-5.5 -> supports: none, low, medium, high, xhigh + # gpt-5.5-pro -> supports: medium, high, xhigh + # Neither supports "minimal"; gpt-5.5-pro additionally does not support "none". + # The JSON must reflect this so LiteLLM rejects unsupported values locally + # (or drops them with drop_params=True) instead of round-tripping to OpenAI + # for a 400. + ("gpt-5.5", True, True, False), + ("gpt-5.5-2026-04-23", True, True, False), + ("gpt-5.5-pro", False, True, False), + ("gpt-5.5-pro-2026-04-23", False, True, False), + ], +) +def test_gpt55_reasoning_effort_flags_match_live_openai_api( + model, expected_none, expected_xhigh, expected_minimal +): + """Pin reasoning_effort capability flags to OpenAI's actual API contract. + + Observed via `POST /v1/chat/completions` with reasoning_effort=minimal: + ``Unsupported value: 'reasoning_effort' does not support 'minimal' with + this model``. gpt-5.5-pro additionally rejects 'none' and 'low'. + """ + os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + litellm.model_cost = litellm.get_model_cost_map(url="") + + m = litellm.model_cost[model] + assert ( + m.get("supports_none_reasoning_effort") is expected_none + ), f"{model}: supports_none_reasoning_effort expected {expected_none}" + assert ( + m.get("supports_xhigh_reasoning_effort") is expected_xhigh + ), f"{model}: supports_xhigh_reasoning_effort expected {expected_xhigh}" + assert ( + m.get("supports_minimal_reasoning_effort") is expected_minimal + ), f"{model}: supports_minimal_reasoning_effort expected {expected_minimal}" + + @pytest.mark.parametrize( "base_model,dated_model", [ From 94f8f12a00d63253a78187699e585c74c49c08c7 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 24 Apr 2026 15:05:43 -0700 Subject: [PATCH 2/4] feat(openai): add supports_low_reasoning_effort flag; reject low on gpt-5.5-pro MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gpt-5.5-pro only accepts reasoning_effort in {medium, high, xhigh} (verified live against OpenAI's API on 2026-04-24). LiteLLM previously had no way to express this constraint — the existing JSON schema covered none/minimal/xhigh but not low. Result: drop_params=true users saw an avoidable 400 from OpenAI. Add supports_low_reasoning_effort following the existing opt-out pattern (default-allow, explicit false to block). Mirror the minimal branch in OpenAIGPT5Config.map_openai_params so 'low' goes through the same _is_reasoning_effort_level_explicitly_disabled gate. Set the flag to false on gpt-5.5-pro and gpt-5.5-pro-2026-04-23 in both model_prices JSON files (kept in sync). Other models leave the key absent so behavior is unchanged. Tests cover: rejection on pro variants (no drop_params), drop on pro with drop_params=True, passthrough on gpt-5.5 chat, passthrough on unknown models, and the helper-level _is_reasoning_effort_level_explicitly_disabled contract. --- .../llms/openai/chat/gpt_5_transformation.py | 8 +- ...odel_prices_and_context_window_backup.json | 6 +- litellm/types/utils.py | 1 + litellm/utils.py | 3 + model_prices_and_context_window.json | 6 +- .../llms/openai/test_gpt5_transformation.py | 75 +++++++++++++++++++ 6 files changed, 92 insertions(+), 7 deletions(-) diff --git a/litellm/llms/openai/chat/gpt_5_transformation.py b/litellm/llms/openai/chat/gpt_5_transformation.py index 34941a545eb..4e34d10b187 100644 --- a/litellm/llms/openai/chat/gpt_5_transformation.py +++ b/litellm/llms/openai/chat/gpt_5_transformation.py @@ -244,9 +244,11 @@ class OpenAIGPT5Config(OpenAIGPTConfig): ), status_code=400, ) - elif effective_effort == "minimal": - # minimal is opt-out: unknown models pass through; only block when - # the model map explicitly sets supports_minimal_reasoning_effort=false. + elif effective_effort in ("minimal", "low"): + # minimal/low are opt-out: unknown models pass through; only block when + # the model map explicitly sets supports_{level}_reasoning_effort=false. + # Example: gpt-5.5-pro only accepts {medium, high, xhigh}, so it sets + # supports_low_reasoning_effort=false (and supports_minimal=false). if self._is_reasoning_effort_level_explicitly_disabled( model, effective_effort ): diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index ffe5b47bac7..82aa4a6a0d7 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -19410,7 +19410,8 @@ "supports_web_search": true, "supports_none_reasoning_effort": false, "supports_xhigh_reasoning_effort": true, - "supports_minimal_reasoning_effort": false + "supports_minimal_reasoning_effort": false, + "supports_low_reasoning_effort": false }, "gpt-5.5-pro-2026-04-23": { "cache_read_input_token_cost": 6e-06, @@ -19453,7 +19454,8 @@ "supports_web_search": true, "supports_none_reasoning_effort": false, "supports_xhigh_reasoning_effort": true, - "supports_minimal_reasoning_effort": false + "supports_minimal_reasoning_effort": false, + "supports_low_reasoning_effort": false }, "gpt-5.4": { "cache_read_input_token_cost": 2.5e-07, diff --git a/litellm/types/utils.py b/litellm/types/utils.py index c347956cba7..c81bbca19e8 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -140,6 +140,7 @@ class ProviderSpecificModelInfo(TypedDict, total=False): supports_url_context: Optional[bool] supports_none_reasoning_effort: Optional[bool] supports_minimal_reasoning_effort: Optional[bool] + supports_low_reasoning_effort: Optional[bool] supports_xhigh_reasoning_effort: Optional[bool] supports_max_reasoning_effort: Optional[bool] diff --git a/litellm/utils.py b/litellm/utils.py index e1ad1db63ef..b60b5aca546 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -5896,6 +5896,9 @@ def _get_model_info_helper( # noqa: PLR0915 supports_minimal_reasoning_effort=_model_info.get( "supports_minimal_reasoning_effort", None ), + supports_low_reasoning_effort=_model_info.get( + "supports_low_reasoning_effort", None + ), supports_xhigh_reasoning_effort=_model_info.get( "supports_xhigh_reasoning_effort", None ), diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 39f85650d1f..830988b7a2e 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -19424,7 +19424,8 @@ "supports_web_search": true, "supports_none_reasoning_effort": false, "supports_xhigh_reasoning_effort": true, - "supports_minimal_reasoning_effort": false + "supports_minimal_reasoning_effort": false, + "supports_low_reasoning_effort": false }, "gpt-5.5-pro-2026-04-23": { "cache_read_input_token_cost": 6e-06, @@ -19467,7 +19468,8 @@ "supports_web_search": true, "supports_none_reasoning_effort": false, "supports_xhigh_reasoning_effort": true, - "supports_minimal_reasoning_effort": false + "supports_minimal_reasoning_effort": false, + "supports_low_reasoning_effort": false }, "gpt-5.4": { "cache_read_input_token_cost": 2.5e-07, diff --git a/tests/test_litellm/llms/openai/test_gpt5_transformation.py b/tests/test_litellm/llms/openai/test_gpt5_transformation.py index aebab33e808..a0584f89855 100644 --- a/tests/test_litellm/llms/openai/test_gpt5_transformation.py +++ b/tests/test_litellm/llms/openai/test_gpt5_transformation.py @@ -536,6 +536,81 @@ def test_gpt5_unknown_model_passes_through_minimal(config: OpenAIConfig): assert params["reasoning_effort"] == "minimal" +def test_gpt5_5_pro_rejects_reasoning_effort_low(config: OpenAIConfig): + """gpt-5.5-pro only accepts {medium, high, xhigh} — 'low' must raise. + + Verified against OpenAI's live API: /v1/chat/completions with + reasoning_effort='low' on gpt-5.5-pro returns HTTP 400. + """ + with pytest.raises(litellm.utils.UnsupportedParamsError): + config.map_openai_params( + non_default_params={"reasoning_effort": "low"}, + optional_params={}, + model="gpt-5.5-pro", + drop_params=False, + ) + + +def test_gpt5_5_pro_dated_rejects_reasoning_effort_low(config: OpenAIConfig): + """Dated snapshot must inherit the base alias's low-rejection behavior.""" + with pytest.raises(litellm.utils.UnsupportedParamsError): + config.map_openai_params( + non_default_params={"reasoning_effort": "low"}, + optional_params={}, + model="gpt-5.5-pro-2026-04-23", + drop_params=False, + ) + + +def test_gpt5_5_pro_drops_reasoning_effort_low_when_requested(config: OpenAIConfig): + """drop_params=True silently strips 'low' instead of round-tripping a 400.""" + params = config.map_openai_params( + non_default_params={"reasoning_effort": "low"}, + optional_params={}, + model="gpt-5.5-pro", + drop_params=True, + ) + assert "reasoning_effort" not in params + + +def test_gpt5_5_chat_allows_reasoning_effort_low(config: OpenAIConfig): + """gpt-5.5 (chat) supports 'low'; flag absent → opt-out check passes.""" + params = config.map_openai_params( + non_default_params={"reasoning_effort": "low"}, + optional_params={}, + model="gpt-5.5", + drop_params=False, + ) + assert params["reasoning_effort"] == "low" + + +def test_gpt5_unknown_model_passes_through_low(config: OpenAIConfig): + """Unknown gpt-5 models pass 'low' through (opt-out, not opt-in).""" + params = config.map_openai_params( + non_default_params={"reasoning_effort": "low"}, + optional_params={}, + model="gpt-5.4-turbo-preview", + drop_params=False, + ) + assert params["reasoning_effort"] == "low" + + +def test_gpt5_low_explicitly_disabled_check(gpt5_config: OpenAIGPT5Config): + """supports_low_reasoning_effort=false → disabled; missing/true → not disabled.""" + assert gpt5_config._is_reasoning_effort_level_explicitly_disabled( + "gpt-5.5-pro", "low" + ) + assert gpt5_config._is_reasoning_effort_level_explicitly_disabled( + "gpt-5.5-pro-2026-04-23", "low" + ) + assert not gpt5_config._is_reasoning_effort_level_explicitly_disabled( + "gpt-5.5", "low" + ) + assert not gpt5_config._is_reasoning_effort_level_explicitly_disabled( + "gpt-5.4", "low" + ) + + def test_gpt5_normalizes_reasoning_effort_dict_with_summary(config: OpenAIConfig): """Dict with summary/generate_summary is normalized for chat completions.""" params = config.map_openai_params( From c3338384c93c467ebf447a52b0d5bba740a19395 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 24 Apr 2026 15:33:30 -0700 Subject: [PATCH 3/4] test: register supports_low_reasoning_effort in cost-map JSON schema The strict 'additionalProperties: false' schema in test_aaamodel_prices_and_context_window_json_is_valid rejected the new flag added in this PR's earlier commit. Register it alongside the other supports_*_reasoning_effort entries so the schema validation passes. --- tests/test_litellm/test_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index 67b62696196..c6b0f49ff61 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -769,6 +769,7 @@ def test_aaamodel_prices_and_context_window_json_is_valid(): "uses_embed_content": {"type": "boolean"}, "supports_reasoning": {"type": "boolean"}, "supports_minimal_reasoning_effort": {"type": "boolean"}, + "supports_low_reasoning_effort": {"type": "boolean"}, "supports_none_reasoning_effort": {"type": "boolean"}, "supports_xhigh_reasoning_effort": {"type": "boolean"}, "supports_max_reasoning_effort": {"type": "boolean"}, From 860843953daa59300d58ebf9766485eb97ad0331 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 30 Apr 2026 19:14:02 +0000 Subject: [PATCH 4/4] fix(vertex_ai): default missing items on array schemas Vertex rejects array schemas without an items field (GenerateContentRequest.tools[*].function_declarations[*].parameters...items: missing field). This happened for tool params containing anyOf branches like {"type": "array"} with no items, including the case where convert_anyof_null_to_nullable strips an empty items entry. Default missing items to {"type": "object"} in process_items so the same default applies to bare arrays and arrays nested inside anyOf. --- litellm/llms/vertex_ai/common_utils.py | 2 ++ .../vertex_ai/test_vertex_ai_common_utils.py | 23 ++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/litellm/llms/vertex_ai/common_utils.py b/litellm/llms/vertex_ai/common_utils.py index ccd4d4f2934..8a6b5ddd70b 100644 --- a/litellm/llms/vertex_ai/common_utils.py +++ b/litellm/llms/vertex_ai/common_utils.py @@ -599,6 +599,8 @@ def process_items(schema, depth=0): if isinstance(schema, dict): if "items" in schema and schema["items"] == {}: schema["items"] = {"type": "object"} + elif schema.get("type") == "array" and "items" not in schema: + schema["items"] = {"type": "object"} for key, value in schema.items(): if isinstance(value, dict): process_items(value, depth + 1) diff --git a/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py b/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py index ef93375c3cd..5d8dec75456 100644 --- a/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py +++ b/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py @@ -225,7 +225,11 @@ def test_build_vertex_schema(): "metadata": {"type": "object"}, "callbacks": { "anyOf": [ - {"type": "array", "nullable": True}, + { + "type": "array", + "items": {"type": "object"}, + "nullable": True, + }, {"type": "object", "nullable": True}, ] }, @@ -288,6 +292,23 @@ def test_process_items_basic(): process_items(schema) assert schema["properties"]["nested"]["items"] == {"type": "object"} + # Test array with no items field at all - Vertex requires items to be present + schema = {"type": "array"} + process_items(schema) + assert schema["items"] == {"type": "object"} + + # Test array missing items inside anyOf branch + schema = { + "type": "object", + "properties": { + "callbacks": { + "anyOf": [{"type": "array"}, {"type": "object"}], + } + }, + } + process_items(schema) + assert schema["properties"]["callbacks"]["anyOf"][0]["items"] == {"type": "object"} + def test_vertex_ai_complex_response_schema(): import json