From 96b7647e5eafe1f32e1df50a6f1ec2554869afc8 Mon Sep 17 00:00:00 2001 From: Tao Date: Thu, 5 Nov 2026 00:23:38 +0800 Subject: [PATCH] fix: isolate IteraCompute credentials before fallback --- litellm/main.py | 16 ++++--- .../openai_like/test_iteracompute_provider.py | 43 +++++++++++++++++++ 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/litellm/main.py b/litellm/main.py index d0b34614353..7c1e675d284 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -2788,14 +2788,18 @@ def _complete_custom_openai( # 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) - if custom_llm_provider == "bigmodel" and not api_key: + if custom_llm_provider in ("bigmodel", "iteracompute") and not api_key: raise litellm.AuthenticationError( - message="Set BIGMODEL_API_KEY or pass api_key", llm_provider="bigmodel", model=model + message=f"Set {custom_llm_provider.upper()}_API_KEY or pass api_key", + llm_provider=custom_llm_provider, + model=model, ) - organization = ( - organization - or (None if custom_llm_provider == "bigmodel" else litellm.organization or get_secret("OPENAI_ORGANIZATION")) - or None # default - https://github.com/openai/openai-python/blob/284c1799070c723c6a553337134148a7ab088dd8/openai/util.py#L105 + organization = organization or ( + "" + if custom_llm_provider == "iteracompute" + else None + if custom_llm_provider == "bigmodel" + else litellm.organization or get_secret("OPENAI_ORGANIZATION") or None ) openai.organization = organization # set API KEY diff --git a/tests/unit/llms/openai_like/test_iteracompute_provider.py b/tests/unit/llms/openai_like/test_iteracompute_provider.py index 039a432d628..6f101b6a275 100644 --- a/tests/unit/llms/openai_like/test_iteracompute_provider.py +++ b/tests/unit/llms/openai_like/test_iteracompute_provider.py @@ -147,6 +147,49 @@ def test_invalid_key_surfaces_authentication_error() -> None: ) +@pytest.mark.parametrize("global_openai_key", (None, "unrelated-global-openai-key")) +@pytest.mark.parametrize("stream", (False, True)) +def test_missing_provider_key_never_sends_openai_credentials( + monkeypatch: pytest.MonkeyPatch, global_openai_key: str | None, stream: bool +) -> None: + monkeypatch.delenv("ITERACOMPUTE_API_KEY", raising=False) + monkeypatch.setenv("OPENAI_API_KEY", "unrelated-env-openai-key") + monkeypatch.setattr(litellm, "openai_key", global_openai_key) + monkeypatch.setattr(litellm, "api_key", None) + with respx.mock(assert_all_called=False) as upstream: + route: Final = upstream.post("https://api.iteracompute.com/v1/chat/completions").respond(200, json=_COMPLETION) + with pytest.raises(litellm.AuthenticationError, match="ITERACOMPUTE_API_KEY"): + litellm.completion( + model="iteracompute/qwen/qwen3.8-27b", + messages=[{"role": "user", "content": "Hello"}], + stream=stream, + ) + assert not route.called + + +@pytest.mark.parametrize("organization", (None, "provider-organization")) +def test_provider_key_wins_over_openai_credentials( + monkeypatch: pytest.MonkeyPatch, organization: str | None +) -> None: + monkeypatch.setenv("ITERACOMPUTE_API_KEY", "provider-key") + monkeypatch.setenv("OPENAI_API_KEY", "unrelated-openai-key") + monkeypatch.setattr(litellm, "openai_key", "unrelated-global-openai-key") + monkeypatch.setenv("OPENAI_ORGANIZATION", "unrelated-organization") + monkeypatch.setattr(litellm, "organization", "unrelated-global-organization") + with respx.mock() as upstream: + route: Final = upstream.post("https://api.iteracompute.com/v1/chat/completions").respond(200, json=_COMPLETION) + litellm.completion( + model="iteracompute/qwen/qwen3.8-27b", + messages=[{"role": "user", "content": "Hello"}], + organization=organization, + ) + assert route.calls.last.request.headers["authorization"] == "Bearer provider-key" + if organization is None: + assert not route.calls.last.request.headers.get("openai-organization") + else: + assert route.calls.last.request.headers["openai-organization"] == organization + + def test_responses_bridge_keeps_model_namespace() -> None: with respx.mock() as upstream: route: Final = upstream.post("https://api.iteracompute.com/v1/chat/completions").respond(200, json=_COMPLETION)