mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(bridge): resolve the effective OpenAI base once, shared by gate and chat handler
The gpt-5.4+ responses-bridge gate classified the endpoint from the call-level api_base alone, while the OpenAI chat handler resolves arg > global > env > default. A custom base configured via litellm.api_base or OPENAI_BASE_URL/ OPENAI_API_BASE was therefore invisible to the gate: it read blank as the default OpenAI endpoint and bridged a request the custom backend has no /responses route for. Extract that resolution into one _resolve_openai_api_base() and have both the gate and _complete_custom_openai() call it, so the gate can never classify an endpoint the request won't hit. The gate compares the resolved base against the default (import litellm seeds OPENAI_BASE_URL to the default, so "override is non-None" is not a safe custom-endpoint signal); whitespace collapses to the default as before. reasoning_effort="none" remains the escape hatch.
This commit is contained in:
parent
c7c656e8a9
commit
4139f548da
2 changed files with 80 additions and 11 deletions
|
|
@ -979,6 +979,23 @@ def mock_completion(
|
|||
raise Exception("Mock completion response failed - {}".format(e))
|
||||
|
||||
|
||||
_OPENAI_DEFAULT_API_BASE = "https://api.openai.com/v1"
|
||||
|
||||
|
||||
def _resolve_openai_api_base(api_base: str | None) -> str:
|
||||
"""Effective OpenAI base a chat request will hit: arg > global > env > default. The bridge gate
|
||||
and the ``_complete_custom_openai`` chat handler MUST resolve this identically, or a custom base
|
||||
set via ``litellm.api_base`` or ``OPENAI_BASE_URL``/``OPENAI_API_BASE`` is invisible to the gate,
|
||||
which then misreads it as the default OpenAI endpoint and bridges a request the backend can't serve."""
|
||||
return (
|
||||
api_base
|
||||
or litellm.api_base
|
||||
or get_secret_str("OPENAI_BASE_URL")
|
||||
or get_secret_str("OPENAI_API_BASE")
|
||||
or _OPENAI_DEFAULT_API_BASE
|
||||
)
|
||||
|
||||
|
||||
def responses_api_bridge_check(
|
||||
model: str,
|
||||
custom_llm_provider: str,
|
||||
|
|
@ -1047,10 +1064,15 @@ def responses_api_bridge_check(
|
|||
reasoning_active = reasoning_effort.get("effort") != "none" or reasoning_effort.get("summary") is not None
|
||||
else:
|
||||
reasoning_active = reasoning_effort != "none"
|
||||
# A blank api_base (None, "", or whitespace) is not a custom endpoint: it resolves
|
||||
# to the default OpenAI base downstream, which does enforce the reasoning+tools
|
||||
# constraint. Azure always targets an OpenAI-constraint endpoint regardless.
|
||||
on_constraint_enforcing_endpoint = custom_llm_provider == "azure" or not (api_base and api_base.strip())
|
||||
# The reasoning+tools constraint is enforced only by the real OpenAI endpoint (and Azure OpenAI).
|
||||
# Resolve the effective base arg>global>env>default exactly as the chat handler does, so a custom
|
||||
# base set via litellm.api_base or OPENAI_BASE_URL/OPENAI_API_BASE isn't misread as the default and
|
||||
# bridged to a /responses route it lacks. A whitespace-only base collapses to the default too.
|
||||
resolved_api_base = _resolve_openai_api_base(api_base)
|
||||
on_constraint_enforcing_endpoint = custom_llm_provider == "azure" or resolved_api_base.strip() in (
|
||||
"",
|
||||
_OPENAI_DEFAULT_API_BASE,
|
||||
)
|
||||
if (
|
||||
custom_llm_provider in ("openai", "azure")
|
||||
and model_info.get("mode") != "responses"
|
||||
|
|
@ -2396,13 +2418,8 @@ def _complete_custom_openai(
|
|||
stream = ctx.stream
|
||||
timeout = ctx.timeout
|
||||
|
||||
api_base = (
|
||||
api_base # for deepinfra/perplexity/anyscale/groq/friendliai we check in get_llm_provider and pass in the api base from there
|
||||
or litellm.api_base
|
||||
or get_secret("OPENAI_BASE_URL")
|
||||
or get_secret("OPENAI_API_BASE")
|
||||
or "https://api.openai.com/v1"
|
||||
)
|
||||
# for deepinfra/perplexity/anyscale/groq/friendliai we check in get_llm_provider and pass in the api base from there
|
||||
api_base = _resolve_openai_api_base(api_base)
|
||||
organization = (
|
||||
organization
|
||||
or litellm.organization
|
||||
|
|
|
|||
|
|
@ -1043,6 +1043,58 @@ def test_responses_api_bridge_check_custom_api_base_with_unset_effort_stays_chat
|
|||
assert model_info.get("mode") != "responses"
|
||||
|
||||
|
||||
def test_responses_api_bridge_check_custom_api_base_via_global_with_unset_effort_stays_chat(monkeypatch):
|
||||
"""
|
||||
A custom base set through the litellm.api_base global (not the call arg) is resolved the
|
||||
same way the chat handler resolves it, so the unset-effort arm must not reroute a chat-only
|
||||
backend to a /responses route it lacks. Regression guard: the gate previously inspected only
|
||||
the call-level api_base and bridged these requests.
|
||||
"""
|
||||
import litellm
|
||||
from litellm.main import responses_api_bridge_check
|
||||
|
||||
monkeypatch.setattr(litellm, "api_base", "http://vllm.internal:8000/v1")
|
||||
with patch("litellm.main._get_model_info_helper") as mock_get_model_info:
|
||||
mock_get_model_info.return_value = {"max_tokens": 128000}
|
||||
model_info, model = responses_api_bridge_check(
|
||||
model="gpt-5.6",
|
||||
custom_llm_provider="openai",
|
||||
tools=[{"type": "function", "function": {"name": "get_capital"}}],
|
||||
reasoning_effort=None,
|
||||
api_base=None,
|
||||
)
|
||||
|
||||
assert model == "gpt-5.6"
|
||||
assert model_info.get("mode") != "responses"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("env_var", ["OPENAI_BASE_URL", "OPENAI_API_BASE"])
|
||||
def test_responses_api_bridge_check_custom_api_base_via_env_with_unset_effort_stays_chat(monkeypatch, env_var):
|
||||
"""
|
||||
A custom base set via OPENAI_BASE_URL/OPENAI_API_BASE env is resolved identically to the chat
|
||||
handler, so the unset-effort arm leaves the request on chat instead of bridging it.
|
||||
"""
|
||||
import litellm
|
||||
from litellm.main import responses_api_bridge_check
|
||||
|
||||
monkeypatch.setattr(litellm, "api_base", None)
|
||||
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
|
||||
monkeypatch.delenv("OPENAI_API_BASE", raising=False)
|
||||
monkeypatch.setenv(env_var, "http://vllm.internal:8000/v1")
|
||||
with patch("litellm.main._get_model_info_helper") as mock_get_model_info:
|
||||
mock_get_model_info.return_value = {"max_tokens": 128000}
|
||||
model_info, model = responses_api_bridge_check(
|
||||
model="gpt-5.6",
|
||||
custom_llm_provider="openai",
|
||||
tools=[{"type": "function", "function": {"name": "get_capital"}}],
|
||||
reasoning_effort=None,
|
||||
api_base=None,
|
||||
)
|
||||
|
||||
assert model == "gpt-5.6"
|
||||
assert model_info.get("mode") != "responses"
|
||||
|
||||
|
||||
def test_responses_api_bridge_check_custom_api_base_with_explicit_effort_still_routes():
|
||||
"""Explicit reasoning_effort keeps its pre-existing bridging behavior on any api_base."""
|
||||
from litellm.main import responses_api_bridge_check
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue