fix(oci): drop max_retries instead of hard-failing on OCI (#29727)

max_retries is a litellm-level control param (litellm applies retries itself),
not a generation param OCI accepts. The provider mapped it to False and raised
"param `max_retries` is not supported on OCI" whenever it was present. The
litellm proxy injects max_retries on every request, so any OCI call through the
proxy 500'd unless drop_params was set.

Drop max_retries silently in map_openai_params. Adds a unit test (Cohere and
generic) and a gateway integration test that a plain request succeeds through a
proxy without drop_params.

Co-authored-by: Sameer Kankute <sameer@berri.ai>
This commit is contained in:
Fede Kamelhar 2026-06-11 06:33:45 -04:00 • committed by GitHub
parent 41924f62e0
commit e1f3a963fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 80 additions and 23 deletions

View file

@ -357,7 +357,13 @@ class OCIChatConfig(BaseConfig):
for key, value in {**non_default_params, **optional_params}.items():
alias = param_map.get(key)
if alias is False:
if alias is False:
# max_retries is a litellm-level control param (litellm applies
# retries itself); it is never a generation param OCI accepts, so
# drop it silently. The litellm proxy injects it on every request,
# which otherwise 500s OCI calls unless drop_params is set.
if key == "max_retries":
continue
# n=1 (or None) is the OpenAI default: a single generation, which
# every OCI model produces anyway. Drop it silently so standard
# clients that always send n=1 (e.g. the MLflow gateway) are not

View file

@ -108,38 +108,35 @@ def _oci_env_from_profile() -> dict[str, str]:
}
@pytest.fixture(scope="module")
def proxy_url() -> Iterator[str]:
oci_env = _oci_env_from_profile()
port = _free_port()
base_url = f"http://127.0.0.1:{port}"
def _serve(config_path: str) -> Iterator[str]:
"""Boot the litellm proxy with the given config and yield its base URL."""
env = os.environ.copy()
env.update(oci_env)
env.update(_oci_env_from_profile())
# Avoid pulling in DB-backed features for this lightweight smoke run.
env.pop("DATABASE_URL", None)
env["STORE_MODEL_IN_DB"] = "False"
port = _free_port()
base_url = f"http://127.0.0.1:{port}"
# Prefer the `litellm` console script that lives next to the active
# Python so we inherit the test virtualenv. Fall back to PATH.
cli = Path(sys.executable).parent / "litellm"
if not cli.exists():
cli = "litellm"
cmd = [
str(cli),
"--config",
str(CONFIG_PATH),
"--port",
str(port),
"--host",
"127.0.0.1",
"--num_workers",
"1",
]
proc = subprocess.Popen(
cmd,
[
str(cli),
"--config",
config_path,
"--port",
str(port),
"--host",
"127.0.0.1",
"--num_workers",
"1",
],
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
@ -157,6 +154,27 @@ def proxy_url() -> Iterator[str]:
proc.wait(timeout=5)
@pytest.fixture(scope="module")
def proxy_url() -> Iterator[str]:
yield from _serve(str(CONFIG_PATH))
@pytest.fixture(scope="module")
def proxy_url_no_drop_params(tmp_path_factory) -> Iterator[str]:
"""A proxy WITHOUT drop_params, to prove benign params the proxy injects
(e.g. max_retries) don't break OCI calls."""
cfg = tmp_path_factory.mktemp("oci_nodrop") / "config.yaml"
cfg.write_text(
"model_list:\n"
" - model_name: oci-cohere-command\n"
" litellm_params:\n"
" model: oci/cohere.command-latest\n"
"general_settings:\n"
f" master_key: {MASTER_KEY}\n"
)
yield from _serve(str(cfg))
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
@ -273,6 +291,25 @@ def test_model_list_advertises_oci_models(proxy_url: str) -> None:
assert expected in advertised, f"{expected} missing from /v1/models: {advertised}"
def test_chat_completion_no_drop_params(proxy_url_no_drop_params: str) -> None:
"""A plain chat completion succeeds through a proxy without drop_params.
Regression for the HTTP 500 ``param `max_retries` is not supported on OCI``:
the proxy injects max_retries on every request, so without this fix any OCI
call through the proxy failed unless drop_params was set.
"""
r = httpx.post(
f"{proxy_url_no_drop_params}/v1/chat/completions",
headers=_auth_headers(),
json=_chat_payload("oci-cohere-command"),
timeout=REQUEST_TIMEOUT_S,
)
assert r.status_code == 200, f"no-drop_params -> {r.status_code}: {r.text}"
body = r.json()
assert body["object"] == "chat.completion"
assert body["choices"][0]["message"].get("content") is not None
def test_cohere_default_n_via_proxy(proxy_url: str) -> None:
"""A Cohere request carrying the default n=1 succeeds through the gateway.

View file

@ -1304,8 +1304,7 @@ class TestOCIStreamingSignedBody:
When signed_json_body is provided, the POST must use that exact bytes object,
not json.dumps(data) — otherwise the RSA-SHA256 signature is invalid.
"""
import httpx
from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock
config = OCIChatConfig()
signed_bytes = b'{"signed": true}'
@ -1464,6 +1463,21 @@ class TestOCIChatConfigErrorPaths:
)
assert "audio" not in result
@pytest.mark.parametrize("model", ["cohere.command-latest", "xai.grok-4"])
def test_map_openai_params_max_retries_dropped_without_drop_params(self, model):
"""max_retries is a litellm control param, not a generation param. It
must be dropped silently (no raise) even when drop_params is False, so
the litellm proxy (which injects max_retries on every request) does not
500 every OCI call.
"""
config = OCIChatConfig()
result = config.map_openai_params(
non_default_params={"max_retries": 3},
optional_params={},
model=model,
drop_params=False,
)
assert "max_retries" not in result
def test_map_openai_params_cohere_n_default_dropped(self):
"""Cohere has no numGenerations field, but n=1 (and None) is the OpenAI
default single-generation request. It must be dropped silently rather