feat(proxy): make CORS expose_headers configurable via LITELLM_CORS_EXPOSE_HEADERS#33125
Conversation
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
|
Greptile SummaryThis PR makes the
Confidence Score: 4/5The change is additive and isolated to CORS expose-headers configuration; when the env var is unset, runtime behaviour is identical to before. The one correctness gap is case-sensitive deduplication against case-insensitive HTTP header names — a mixed-case duplicate from the env var would survive into the final list. Everything else — parsing logic, defaults preservation, test coverage, and module-level wiring — looks correct. The deduplication logic in
|
| Filename | Overview |
|---|---|
| litellm/proxy/proxy_server.py | Adds _get_cors_expose_headers() helper and module-level cors_expose_headers variable, replacing the hardcoded LITELLM_UI_ALLOW_HEADERS passed to CORSMiddleware(expose_headers=...) with the env-configurable list. Logic is straightforward; deduplication is case-sensitive. |
| tests/test_litellm/proxy/test_cors_config.py | Adds three new pure-mock tests for _get_cors_expose_headers: defaults, append, and dedup. Tests are isolated, make no network calls, and correctly cover the core behaviour. |
Reviews (1): Last reviewed commit: "feat(proxy): make CORS expose_headers co..." | Re-trigger Greptile
| _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])) |
There was a problem hiding this comment.
The deduplication uses a case-sensitive
dict.fromkeys, but HTTP header names are case-insensitive (RFC 7230). If a user sets LITELLM_CORS_EXPOSE_HEADERS=X-LiteLLM-Semantic-Filter (mixed-case), it won't be recognised as a duplicate of the lowercase x-litellm-semantic-filter already in LITELLM_UI_ALLOW_HEADERS, so the final list will contain two entries for what the browser treats as the same header. Normalising to lowercase before the dedup check preserves correctness without changing the wire value.
| _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])) | |
| _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 [] | |
| seen: dict[str, str] = {} | |
| for h in [*LITELLM_UI_ALLOW_HEADERS, *extra]: | |
| seen.setdefault(h.lower(), h) | |
| return list(seen.values()) |
Relevant issues
Fixes #33123
Linear ticket
Pre-Submission checklist
Screenshots / Proof of Fix
Captured at commit
55722dabcfagainst a live proxy hitting the real Anthropic API (claude-haiku-4-5)With
LITELLM_CORS_EXPOSE_HEADERS="x-litellm-response-cost, x-litellm-model-api-base":With the env var unset (default, unchanged), only the built-in defaults are exposed:
Type
🆕 New Feature
Changes
expose_headerson the proxy'sCORSMiddlewarewas hardcoded toLITELLM_UI_ALLOW_HEADERS, so browser clients calling the proxy cross-origin could not read usefulx-litellm-*response headers such asx-litellm-response-costandx-litellm-model-api-base, even thoughallow_origins/credentials were already env-configurableThis adds a
LITELLM_CORS_EXPOSE_HEADERSenv var (comma-separated, parsed likeLITELLM_CORS_ORIGINS). A new_get_cors_expose_headershelper appends the configured headers to the existingLITELLM_UI_ALLOW_HEADERSdefaults, de-duplicated with order preserved, and the result is passed toCORSMiddleware(expose_headers=...). When the var is unset or empty the behaviour is unchanged (defaults only)Docs for the new env var are added in BerriAI/litellm-docs#552; the documentation validation check here checks out that docs repo and stays red until it merges
Final Attestation
Link to Devin session: https://app.devin.ai/sessions/4db978bbfa1045a8a89c634e6648eeba