mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-02 02:11:58 +00:00
fix(oci): drop max_retries instead of hard-failing on OCI
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.
This commit is contained in:
parent
3bd89f209e
commit
33c467fad0
3 changed files with 88 additions and 29 deletions
|
|
@ -317,6 +317,12 @@ class OCIChatConfig(BaseConfig):
|
|||
for key, value in {**non_default_params, **optional_params}.items():
|
||||
alias = param_map.get(key)
|
||||
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
|
||||
if drop_params or litellm.drop_params:
|
||||
continue
|
||||
raise OCIError(
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ from typing import Iterator
|
|||
import httpx
|
||||
import pytest
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Skip gate
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -79,7 +78,9 @@ def _wait_for_health(base_url: str, proc: subprocess.Popen, deadline: float) ->
|
|||
except httpx.HTTPError:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
raise RuntimeError(f"litellm proxy did not become ready within {STARTUP_TIMEOUT_S}s")
|
||||
raise RuntimeError(
|
||||
f"litellm proxy did not become ready within {STARTUP_TIMEOUT_S}s"
|
||||
)
|
||||
|
||||
|
||||
def _oci_env_from_profile() -> dict[str, str]:
|
||||
|
|
@ -106,38 +107,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,
|
||||
|
|
@ -155,6 +153,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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -206,9 +225,7 @@ def test_chat_completion_via_proxy(proxy_url: str, model: str) -> None:
|
|||
# Reasoning models may return empty content if their budget covers only
|
||||
# the thinking turn — accept either text or a non-empty reasoning field.
|
||||
has_content = bool(msg.get("content"))
|
||||
has_reasoning = bool(msg.get("reasoning_content")) or bool(
|
||||
msg.get("reasoning")
|
||||
)
|
||||
has_reasoning = bool(msg.get("reasoning_content")) or bool(msg.get("reasoning"))
|
||||
assert has_content or has_reasoning, f"empty assistant message for {model}: {msg}"
|
||||
usage = body.get("usage") or {}
|
||||
assert usage.get("total_tokens", 0) > 0
|
||||
|
|
@ -232,7 +249,7 @@ def test_chat_completion_streaming_via_proxy(proxy_url: str, model: str) -> None
|
|||
continue
|
||||
if not line.startswith("data:"):
|
||||
continue
|
||||
payload = line[len("data:"):].strip()
|
||||
payload = line[len("data:") :].strip()
|
||||
if payload == "[DONE]":
|
||||
saw_done = True
|
||||
break
|
||||
|
|
@ -271,4 +288,25 @@ def test_model_list_advertises_oci_models(proxy_url: str) -> None:
|
|||
assert r.status_code == 200, r.text
|
||||
advertised = {row["id"] for row in r.json()["data"]}
|
||||
for expected in CHAT_MODELS + ["oci-embed"]:
|
||||
assert expected in advertised, f"{expected} missing from /v1/models: {advertised}"
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1133,8 +1133,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}'
|
||||
|
|
@ -1293,6 +1292,22 @@ 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_transform_request_tool_choice_string_mapped(self):
|
||||
config = OCIChatConfig()
|
||||
result = config.transform_request(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue