mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-07 07:58:09 +00:00
fix(openai): never send OPENAI_ADMIN_KEY as the bearer token for a deployment without an api_key (#50218)
* fix(openai): authenticate raw chat requests with the deployment api_key only, never OPENAI_ADMIN_KEY
openai 3.x request options default to bearer_auth plus admin_api_key_auth, so a deployment whose api_key is empty sent Authorization: Bearer <OPENAI_ADMIN_KEY> to its api_base. Pass security={"bearer_auth": True} when the installed SDK's make_request_options accepts it (openai 2.x has no admin key fallback and no security kwarg)
* test(openai): assert the admin key never reaches the wire whether the SDK sends unsigned or refuses
---------
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
88ea40e69c
commit
f25fff42a6
2 changed files with 55 additions and 0 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import inspect
|
||||
import time
|
||||
import types
|
||||
from collections.abc import AsyncIterator, Callable, Coroutine, Iterable, Iterator, Mapping, Sequence
|
||||
|
|
@ -379,6 +380,11 @@ _EXTRA_HEADERS_ADAPTER: Final = TypeAdapter(dict[str, str] | None)
|
|||
_EXTRA_QUERY_ADAPTER: Final = TypeAdapter(dict[str, object] | None)
|
||||
_NO_EXTRA_HEADERS: Final[Mapping[str, str]] = types.MappingProxyType({})
|
||||
_SDK_OPTION_KEYS: Final = frozenset(("extra_headers", "extra_query", "extra_body"))
|
||||
_CLIENT_API_KEY_ONLY_AUTH: Final[Mapping[str, Mapping[str, bool]]] = (
|
||||
types.MappingProxyType({"security": types.MappingProxyType({"bearer_auth": True})})
|
||||
if "security" in inspect.signature(make_request_options).parameters
|
||||
else types.MappingProxyType({})
|
||||
)
|
||||
|
||||
|
||||
def _raw_chat_response(
|
||||
|
|
@ -398,6 +404,7 @@ def _request_without_sdk_transform(
|
|||
extra_query=_EXTRA_QUERY_ADAPTER.validate_python(data.get("extra_query")),
|
||||
extra_body=data.get("extra_body"),
|
||||
timeout=timeout,
|
||||
**_CLIENT_API_KEY_ONLY_AUTH, # pyright: ignore[reportArgumentType] # openai<3 make_request_options has no security kwarg
|
||||
)
|
||||
return body, options
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import asyncio
|
||||
import contextlib
|
||||
import gzip
|
||||
import importlib
|
||||
import inspect
|
||||
import json
|
||||
import os
|
||||
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
||||
|
|
@ -227,6 +229,52 @@ async def test_acompletion_returns_prompt_cache_details_over_injected_transport(
|
|||
assert prompt_tokens_details.cached_tokens == 7
|
||||
|
||||
|
||||
_SDK_READS_OPENAI_ADMIN_KEY: Final = "admin_api_key" in inspect.signature(AsyncOpenAI.__init__).parameters
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _SDK_READS_OPENAI_ADMIN_KEY, reason="openai<3 has no OPENAI_ADMIN_KEY fallback to leak")
|
||||
@pytest.mark.asyncio
|
||||
async def test_acompletion_never_sends_the_openai_admin_key_for_a_deployment_without_a_key(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
||||
monkeypatch.setenv("OPENAI_ADMIN_KEY", "sk-admin-only")
|
||||
monkeypatch.setattr(litellm, "api_key", None)
|
||||
monkeypatch.setattr(litellm, "openai_key", None)
|
||||
outbound: Final = asyncio.Queue()
|
||||
|
||||
def respond(request: httpx.Request) -> httpx.Response:
|
||||
outbound.put_nowait(request.headers.get("authorization"))
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"id": "chatcmpl-no-admin-key",
|
||||
"object": "chat.completion",
|
||||
"created": 0,
|
||||
"model": "gpt-5.4-mini",
|
||||
"choices": [{"index": 0, "message": {"role": "assistant", "content": "ok"}, "finish_reason": "stop"}],
|
||||
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
|
||||
},
|
||||
)
|
||||
|
||||
async with httpx.AsyncClient(transport=httpx.MockTransport(respond)) as http_client:
|
||||
monkeypatch.setattr(litellm, "aclient_session", http_client)
|
||||
with contextlib.suppress(litellm.AuthenticationError, litellm.InternalServerError):
|
||||
await asyncio.wait_for(
|
||||
litellm.acompletion(
|
||||
model="openai/gpt-5.4-mini",
|
||||
api_key="",
|
||||
messages=[{"role": "user", "content": "who signs this request"}],
|
||||
num_retries=0,
|
||||
max_retries=0,
|
||||
),
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
sent: Final = tuple(outbound.get_nowait() for _ in range(outbound.qsize()))
|
||||
assert all(authorization is None for authorization in sent), sent
|
||||
|
||||
|
||||
|
||||
class _SearchingChatGate(CustomLogger):
|
||||
def __init__(self) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue