merge: origin/main into litellm_bedrock-local-token-count-marker

This commit is contained in:
ryan-crabbe-berri 2026-11-02 16:36:44 -08:00
commit a6e10cdcdd
2 changed files with 36 additions and 1 deletions

View file

@ -21,7 +21,7 @@ import httpx
from fastapi import FastAPI, HTTPException
from pydantic import ConfigDict, TypeAdapter, ValidationError
from starlette.requests import Request as StarletteRequest
from starlette.responses import JSONResponse
from starlette.responses import JSONResponse, Response
from starlette.routing import Route
from starlette.types import Message, Receive, Scope, Send
@ -2489,6 +2489,9 @@ if MCP_AVAILABLE:
},
)(scope, receive, send)
return
if scope.get("method") == "OPTIONS":
await Response(status_code=204, headers={"Allow": "GET, POST, DELETE, OPTIONS"})(scope, receive, send)
return
path: Final = _scope_path(scope)
peek: Final = _LazyPeekedBody(receive) if scope.get("method") == "POST" else None
if peek is not None:

View file

@ -3319,3 +3319,35 @@ async def test_mcp_routing_truncated_json_body_is_not_stashed_for_auth():
assert await scope[MCP_PEEKED_BODY_SCOPE_KEY]() == b"{}"
assert await _read_request_body(_admission_request(scope)) == {}
@pytest.mark.asyncio
@pytest.mark.parametrize("path", ["/mcp", "/atlassian/mcp"])
async def test_bare_options_skips_litellm_auth(path: str):
"""A bare OPTIONS probe must not run LiteLLM auth, so it never triggers the 401 OAuth challenge."""
from litellm.proxy.mcp_server import server as mcp_server
scope: Scope = {
"type": "http",
"method": "OPTIONS",
"path": path,
"headers": [],
"query_string": b"",
"server": ("localhost", 8000),
"scheme": "http",
}
receive = AsyncMock(return_value={"type": "http.request", "body": b"", "more_body": False})
sent: list[dict] = []
async def send(message: dict) -> None:
sent.append(message)
auth = AsyncMock(side_effect=AssertionError("LiteLLM auth ran on OPTIONS"))
with patch.object(mcp_server, "extract_mcp_auth_context", auth):
await mcp_server.handle_streamable_http_mcp(scope, receive, send)
auth.assert_not_awaited()
start = next(m for m in sent if m["type"] == "http.response.start")
assert start["status"] == 204
headers = {k.decode().lower(): v.decode() for k, v in start["headers"]}
assert headers["allow"] == "GET, POST, DELETE, OPTIONS"