feat(proxy): make CORS expose_headers configurable via LITELLM_CORS_EXPOSE_HEADERS

This commit is contained in:
Devin AI 2026-07-13 22:09:23 +00:00
parent c0ff81a947
commit 55722dabcf
2 changed files with 60 additions and 1 deletions

View file

@ -1463,7 +1463,30 @@ def _get_cors_config(
return computed_origins, computed_credentials
def _get_cors_expose_headers(
expose_headers_env: str | None = None,
) -> list[str]:
"""
Compute the CORS Access-Control-Expose-Headers value.
Defaults to LITELLM_UI_ALLOW_HEADERS. When LITELLM_CORS_EXPOSE_HEADERS is set
(comma-separated, parsed like LITELLM_CORS_ORIGINS), its entries are appended to
the defaults (de-duplicated, order preserved) so browser clients can read
additional x-litellm-* response headers. Unset leaves the default behaviour.
Args:
expose_headers_env: Value of LITELLM_CORS_EXPOSE_HEADERS (defaults to os.getenv).
Returns:
List[str]: headers to expose via Access-Control-Expose-Headers.
"""
_raw = expose_headers_env if expose_headers_env is not None else os.getenv("LITELLM_CORS_EXPOSE_HEADERS")
extra = [h.strip() for h in _raw.split(",") if h.strip()] if _raw else []
return list(dict.fromkeys([*LITELLM_UI_ALLOW_HEADERS, *extra]))
origins, allow_cors_credentials = _get_cors_config()
cors_expose_headers = _get_cors_expose_headers()
# get current directory
@ -1776,7 +1799,7 @@ app.add_middleware(
allow_credentials=allow_cors_credentials,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=LITELLM_UI_ALLOW_HEADERS,
expose_headers=cors_expose_headers,
)
app.add_middleware(PrometheusAuthMiddleware)

View file

@ -123,6 +123,42 @@ def test_cors_explicit_credentials_case_insensitive():
assert allow_false is False
def test_cors_expose_headers_defaults_to_ui_allow_headers():
"""should return exactly LITELLM_UI_ALLOW_HEADERS when the env var is unset/empty."""
from litellm.constants import LITELLM_UI_ALLOW_HEADERS
from litellm.proxy.proxy_server import _get_cors_expose_headers
for empty in ("", " ", "\t"):
headers = _get_cors_expose_headers(expose_headers_env=empty)
assert headers == list(LITELLM_UI_ALLOW_HEADERS), f"Unexpected headers for {repr(empty)}"
def test_cors_expose_headers_appends_extra_headers():
"""should append configured headers to the defaults, parsed like LITELLM_CORS_ORIGINS."""
from litellm.constants import LITELLM_UI_ALLOW_HEADERS
from litellm.proxy.proxy_server import _get_cors_expose_headers
headers = _get_cors_expose_headers(
expose_headers_env=" x-litellm-response-cost , x-litellm-model-api-base ,,"
)
assert headers == [
*LITELLM_UI_ALLOW_HEADERS,
"x-litellm-response-cost",
"x-litellm-model-api-base",
]
def test_cors_expose_headers_dedupes_preserving_order():
"""should not duplicate a header already present in the defaults."""
from litellm.constants import LITELLM_UI_ALLOW_HEADERS
from litellm.proxy.proxy_server import _get_cors_expose_headers
dup = LITELLM_UI_ALLOW_HEADERS[0]
headers = _get_cors_expose_headers(expose_headers_env=f"{dup}, x-litellm-response-cost")
assert headers == [*LITELLM_UI_ALLOW_HEADERS, "x-litellm-response-cost"]
assert headers.count(dup) == 1
def test_proxy_server_cors_invariant():
"""should verify that proxy_server module-level origins and allow_cors_credentials
are consistent catches any future drift in the module-level call to _get_cors_config.