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.
This commit is contained in:
Chenglun Hu 2026-06-09 21:32:07 +08:00
parent 51ba6e39cd
commit 87da890e6c
2 changed files with 76 additions and 0 deletions

View file

@ -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)

View file

@ -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