From 87da890e6c66bba1c25006e68d93a5a2c7998332 Mon Sep 17 00:00:00 2001 From: Chenglun Hu Date: Tue, 9 Jun 2026 21:32:07 +0800 Subject: [PATCH] fix(responses): promote OpenAI-SDK base_url to internal api_base Fixes #30026. litellm.completion() converts the OpenAI-SDK-style 'base_url' kwarg into the internal 'api_base' before building LitellmParams (main.py:1384). litellm.responses() didn't, so YAML model_list entries that set base_url: https://eu.api.openai.com/v1 silently hit api.openai.com on the /v1/responses path while /v1/chat/completions on the same deployment worked fine. Add the same one-line promotion in responses() just before constructing GenericLiteLLMParams. api_base passed explicitly still wins; absence of both leaves the kwargs untouched. --- litellm/responses/main.py | 8 +++ .../test_responses_base_url_passthrough.py | 68 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 tests/test_litellm/responses/test_responses_base_url_passthrough.py diff --git a/litellm/responses/main.py b/litellm/responses/main.py index e4c713f67c0..6b4617e94ca 100644 --- a/litellm/responses/main.py +++ b/litellm/responses/main.py @@ -954,6 +954,14 @@ def responses( # Update local_vars to include the converted text parameter local_vars["text"] = text + # Mirror completion()'s OpenAI-SDK-style base_url -> internal api_base promotion + # (litellm/main.py:1384). YAML model_list / OpenAI client kwargs use 'base_url'; + # CredentialLiteLLMParams stores 'api_base'. Without this, GenericLiteLLMParams + # below leaves api_base=None and the request goes to api.openai.com regardless of + # the configured base_url (issue #30026). + if kwargs.get("base_url") is not None and kwargs.get("api_base") is None: + kwargs["api_base"] = kwargs.pop("base_url") + # get llm provider logic litellm_params = GenericLiteLLMParams(**kwargs) diff --git a/tests/test_litellm/responses/test_responses_base_url_passthrough.py b/tests/test_litellm/responses/test_responses_base_url_passthrough.py new file mode 100644 index 00000000000..69191d0d8a4 --- /dev/null +++ b/tests/test_litellm/responses/test_responses_base_url_passthrough.py @@ -0,0 +1,68 @@ +"""Regression for #30026 — proxy YAML's ``base_url`` (OpenAI-SDK convention) must reach +the OpenAI client as ``api_base``. Before the fix, ``litellm.responses()`` constructed +``GenericLiteLLMParams(**kwargs)`` without first mapping ``base_url`` to ``api_base``, +so requests went to ``api.openai.com`` regardless of the configured base_url.""" + +import importlib +from unittest.mock import patch + +import pytest + +import litellm +from litellm.types.router import GenericLiteLLMParams + +# Import the responses package's main module directly; `litellm.responses` is a +# callable in the public namespace, so we can't dotted-import through it. +responses_main = importlib.import_module("litellm.responses.main") + + +def _patched_params_capture(monkeypatch): + """Return a (seen-dict, patched-class) pair that records the kwargs handed to + GenericLiteLLMParams inside responses().""" + seen = {} + + class _Spy(GenericLiteLLMParams): + def __init__(self, **data): + seen.update(data) + super().__init__(**data) + + monkeypatch.setattr(responses_main, "GenericLiteLLMParams", _Spy) + return seen + + +def test_base_url_in_kwargs_is_promoted_to_api_base(monkeypatch): + seen = _patched_params_capture(monkeypatch) + # mock_response short-circuits in responses() right after litellm_params is built, + # so we reach the promotion logic without making an HTTP call. + litellm.responses( + input="hello", + model="openai/gpt-4o-mini", + base_url="https://eu.api.openai.com/v1", + mock_response="ok", + ) + assert seen.get("api_base") == "https://eu.api.openai.com/v1" + assert "base_url" not in seen + + +def test_existing_api_base_takes_precedence(monkeypatch): + seen = _patched_params_capture(monkeypatch) + litellm.responses( + input="hi", + model="openai/gpt-4o-mini", + base_url="https://wrong.example.com/v1", + api_base="https://right.example.com/v1", + mock_response="ok", + ) + assert seen.get("api_base") == "https://right.example.com/v1" + + +def test_no_base_url_leaves_api_base_unset(monkeypatch): + seen = _patched_params_capture(monkeypatch) + litellm.responses( + input="hi", + model="openai/gpt-4o-mini", + mock_response="ok", + ) + # Neither key present → no promotion, api_base stays absent from the kwargs. + assert "api_base" not in seen + assert "base_url" not in seen