diff --git a/litellm/responses/bridge_check.py b/litellm/responses/bridge_check.py index 60318d6b184..c274eb82666 100644 --- a/litellm/responses/bridge_check.py +++ b/litellm/responses/bridge_check.py @@ -1,3 +1,4 @@ +import re from collections.abc import Mapping, Sequence from typing import Final, cast # noqa: TID251 # moved unchanged from litellm/main.py from urllib.parse import urlsplit @@ -61,6 +62,22 @@ def keep_registered_mode(backend_key: str, shared_model_info: Mapping[str, objec _OPENAI_API_HOST: Final = "api.openai.com" +# Databricks serves OpenAI GPT models as e.g. ``databricks-gpt-5-6-sol``: the minor version is +# hyphenated, so the OpenAI version parser does not recognise it. The minor version is optional +# so major-only names (``databricks-gpt-5``) map as well. +_DATABRICKS_GPT_NAME: Final = re.compile(r"^databricks-gpt-(\d+)(?:-(\d+))?(?=-|$)") + + +def _openai_gpt_name(match: "re.Match[str]") -> str: + major: Final = match.group(1) + minor: Final = match.group(2) + return f"gpt-{major}" if minor is None else f"gpt-{major}.{minor}" + + +def _openai_name_for_databricks_model(model: str) -> str: + """``databricks-gpt-5-6-sol`` -> ``gpt-5.6-sol``; other names are returned unchanged.""" + model_name: Final = model.split("/")[-1] + return _DATABRICKS_GPT_NAME.sub(_openai_gpt_name, model_name) def _is_openai_backed_api_base(api_base: str) -> bool: @@ -243,6 +260,13 @@ def responses_api_bridge_check( on_foundry_openai_endpoint: Final = custom_llm_provider == "azure_ai" and azure_ai_supports_native_responses( model, api_base ) + # Databricks proxies OpenAI GPT models to the real OpenAI backend, which enforces the + # reasoning+tools constraint, and serves them on its own /responses route as well. + on_databricks_gpt_endpoint: Final = ( + custom_llm_provider == "databricks" + and OpenAIGPT5Config.is_model_gpt_5_model(model) + and not OpenAIGPT5Config.is_model_gpt_5_search_model(model) + ) mantle_series_name: Final = ( _mantle_responses_series_name(model) if custom_llm_provider == "bedrock_mantle" else None ) @@ -254,7 +278,7 @@ def responses_api_bridge_check( ) gpt_series_name: Final = mantle_series_name or model on_constraint_enforcing_endpoint: Final = ( - custom_llm_provider == "azure" + custom_llm_provider in ("azure", "databricks") or on_mantle_responses_endpoint or resolved_api_base == "" or _is_openai_backed_api_base(resolved_api_base) @@ -266,7 +290,9 @@ def responses_api_bridge_check( foundry_chat_rejects_function_tools_while_reasoning(model, reasoning_effort) if on_foundry_openai_endpoint else ( - OpenAIGPT5Config.is_model_gpt_5_4_plus_model(gpt_series_name) + OpenAIGPT5Config.is_model_gpt_5_4_plus_model( + _openai_name_for_databricks_model(model) if on_databricks_gpt_endpoint else gpt_series_name + ) and (reasoning_effort is not None or on_constraint_enforcing_endpoint) ) ) @@ -282,7 +308,7 @@ def responses_api_bridge_check( or (carries_tool_search and on_constraint_enforcing_endpoint) or (web_search_options is not None and on_mantle_responses_endpoint) ) - ): + ) or (on_databricks_gpt_endpoint and model_info.get("mode") != "responses" and chat_rejects_function_tools): model_info["mode"] = "responses" model = model.replace("responses/", "") diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index c3d3575b8b3..778ec072ab5 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -1810,6 +1810,79 @@ def test_responses_api_bridge_check_azure_ai_without_foundry_responses_route_sta assert model_info.get("mode") != "responses" +_DATABRICKS_FUNCTION_TOOL: Final = ({"type": "function", "function": {"name": "get_capital"}},) + + +@pytest.mark.parametrize( + "model_name, reasoning_effort", + [ + pytest.param("databricks-gpt-5-6-sol", None, id="gpt-5.6-unset-effort"), + pytest.param("databricks-gpt-5-6-luna", "high", id="gpt-5.6-explicit-effort"), + pytest.param("databricks-gpt-5-4", None, id="gpt-5.4-unset-effort"), + pytest.param("databricks-gpt-5-4-mini", "low", id="gpt-5.4-mini-explicit-effort"), + pytest.param("databricks/databricks-gpt-5-6-terra", None, id="provider-prefixed"), + ], +) +def test_responses_api_bridge_check_databricks_gpt_5_4_plus_tools_route_to_responses(model_name, reasoning_effort): + """Databricks forwards GPT models to OpenAI, whose Chat Completions rejects function tools while reasoning.""" + from litellm.main import responses_api_bridge_check + + model_info, model = responses_api_bridge_check( + model=model_name, + custom_llm_provider="databricks", + tools=_DATABRICKS_FUNCTION_TOOL, + reasoning_effort=reasoning_effort, + ) + + assert model == model_name + assert model_info.get("mode") == "responses" + + +@pytest.mark.parametrize( + "model_name, tools, reasoning_effort", + [ + pytest.param("databricks-gpt-5-6-sol", _DATABRICKS_FUNCTION_TOOL, "none", id="explicit-none"), + pytest.param("databricks-gpt-5-6-sol", None, None, id="no-tools"), + pytest.param("databricks-gpt-5-2", _DATABRICKS_FUNCTION_TOOL, None, id="pre-5.4"), + pytest.param("databricks-gpt-5", _DATABRICKS_FUNCTION_TOOL, None, id="major-only-pre-5.4"), + pytest.param("databricks-gpt-5-6-search-api", _DATABRICKS_FUNCTION_TOOL, None, id="search-model"), + pytest.param("databricks-gpt-oss-120b", _DATABRICKS_FUNCTION_TOOL, None, id="gpt-oss"), + pytest.param("databricks-claude-sonnet-4-5", _DATABRICKS_FUNCTION_TOOL, None, id="non-gpt"), + ], +) +def test_responses_api_bridge_check_databricks_without_rejected_tools_stays_chat(model_name, tools, reasoning_effort): + from litellm.main import responses_api_bridge_check + + model_info, model = responses_api_bridge_check( + model=model_name, + custom_llm_provider="databricks", + tools=tools, + reasoning_effort=reasoning_effort, + ) + + assert model == model_name + assert model_info.get("mode") != "responses" + + +@pytest.mark.parametrize( + "model_name, expected", + [ + ("databricks-gpt-5-6-sol", "gpt-5.6-sol"), + ("databricks/databricks-gpt-5-4", "gpt-5.4"), + ("databricks-gpt-5-4-mini", "gpt-5.4-mini"), + ("databricks-gpt-5", "gpt-5"), + ("databricks-gpt-5-mini", "gpt-5-mini"), + ("databricks-gpt-6", "gpt-6"), + ("databricks-gpt-oss-120b", "databricks-gpt-oss-120b"), + ("databricks-claude-sonnet-4-5", "databricks-claude-sonnet-4-5"), + ], +) +def test_openai_name_for_databricks_model(model_name, expected): + from litellm.responses.bridge_check import _openai_name_for_databricks_model + + assert _openai_name_for_databricks_model(model_name) == expected + + def test_responses_api_bridge_check_older_gpt_5_tools_without_reasoning_stays_chat(): """Pre-5.4 GPT-5 names keep the old boundary: tools alone never bridge.""" from litellm.main import responses_api_bridge_check