fix(openai): mint workload identity tokens for PrivateLink and regional api.openai.com hosts - #39652
Conversation
…al api.openai.com hosts
Greptile SummaryThis PR expands OpenAI workload-identity authentication from the default API hostname to HTTPS regional and PrivateLink subdomains while preserving rejection of plaintext and lookalike hosts.
Confidence Score: 5/5The PR appears safe to merge with the intended workload-identity expansion constrained to HTTPS OpenAI API hostnames. The shared predicate preserves all previously accepted cases, rejects plaintext and suffix-lookalike domains, and the affected chat and Responses authentication paths receive focused mocked regression coverage.
|
| Filename | Overview |
|---|---|
| litellm/llms/openai/workload_identity.py | Broadens the workload-identity host gate to OpenAI-controlled regional and PrivateLink subdomains while retaining HTTPS and static-key safeguards. |
| tests/test_litellm/llms/openai/test_openai_workload_identity.py | Adds mocked positive and negative regression cases across configuration resolution, SDK client construction, and Responses bearer generation. |
Reviews (1): Last reviewed commit: "fix(openai): mint workload identity toke..." | Re-trigger Greptile
|
bugbot run |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 19da217. Configure here.
a3afbb1
into
litellm_internal_staging
TLDR
Problem this solves:
api.openai.com<region>.privatelink.api.openai.com) and regional (eu.api.openai.com) hosts get no token/v1/chat/completionsthen fails locally with "The api_key client option must be set"/v1/responsesand/v1/messagesgo out with no bearer and get 401 backHow it solves it:
is_openai_backed_api_basefrom fix(openai): default stream usage on PrivateLink and regional api.openai.com hosts #39614api.openai.comand any*.api.openai.comhost qualify; plaintexthttp://still does notapi.openai.com.evil.examplestill get no tokenUser Flow
Before: a proxy admin who points a keyless
openai/deployment at their PrivateLink endpoint gets an auth failure on every call, while the same setup againstapi.openai.comworksOPENAI_IDENTITY_PROVIDER_ID,OPENAI_SERVICE_ACCOUNT_ID, andOPENAI_IDENTITY_TOKEN_FILE, adds anopenai/gpt-5.6deployment withapi_base: https://southcentralus.privatelink.api.openai.com/v1and noapi_key, and starts the proxy{"model": "gpt-5.6-privatelink", "messages": [{"role": "user", "content": "Reply with exactly the word: pong"}]}The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable, and nothing ever calls https://auth.openai.com/oauth/token{"model": "gpt-5.6-privatelink", "input": "Reply with exactly the word: pong"}and get HTTP 401Incorrect API key provided: None, because the request reached their endpoint with no bearer token{"model": "gpt-5.6-privatelink", "max_tokens": 20, "messages": [...]}and get the same HTTP 401Incorrect API key provided: Noneapi_baseand get HTTP 200pongfrom every one, so the credentials are fine and only the hostname differsAfter: the same PrivateLink deployment exchanges the identity token and answers on all three endpoints
OPENAI_IDENTITY_PROVIDER_ID,OPENAI_SERVICE_ACCOUNT_ID, andOPENAI_IDENTITY_TOKEN_FILE, adds anopenai/gpt-5.6deployment withapi_base: https://southcentralus.privatelink.api.openai.com/v1and noapi_key, and starts the proxy{"model": "gpt-5.6-privatelink", "messages": [{"role": "user", "content": "Reply with exactly the word: pong"}]}"content": "pong"; the proxy exchanged the identity token at https://auth.openai.com/oauth/token first and sent the request to the PrivateLink endpoint with that bearer"status": "completed"and output textpong"stop_reason": "end_turn"and textpongapi_baseand still get HTTP 200pongfrom every oneRelevant issues
Linear ticket
Resolves LIT-6902
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Shared setup for both legs. The proxy boots from the named commit with
python litellm/proxy/proxy_cli.py --config config.yaml --port 48007 --num_workers 2 --detailed_debug. Its environment carries a real OpenAI identity provider and service account (OPENAI_IDENTITY_PROVIDER_ID,OPENAI_SERVICE_ACCOUNT_ID,OPENAI_IDENTITY_TOKEN_FILEpointing at a freshly minted subject token) and noOPENAI_API_KEYorOPENAI_BASE_URLanywhere, so every 200 below is a real gpt-5.6 call paid for through the exchanged tokendisable_cooldownskeeps the eu deployment's expected 4xx on one endpoint from cooling it down before the next endpoint's call. PrivateLink hostnames only resolve inside the customer's VPC, so the proxy runs withHTTPS_PROXYpointed at a local CONNECT forwarder that terminates TLS forsouthcentralus.privatelink.api.openai.comwith a locally trusted CA (SSL_CERT_FILE) and relays every byte toapi.openai.com, tunneling every other host (auth.openai.com,eu.api.openai.com) untouched. Its only edit is theHostheader, because Cloudflare in front ofapi.openai.comanswers 403 to a host it does not serve; the deployment keeps the genuine PrivateLink URL, which is all the auth decision reads.eu.api.openai.comresolves publicly and is reached directly; the test org has no EU data-residency project, so the After answer there is OpenAI's "only accessible by projects with geography restrictions enabled", an authenticated rejection, where Before got "Incorrect API key provided: None" for a request that carried no bearer at all. The forwarder log lines quoted under each case show which hosts were actually contactedEvery step below runs the same three commands, one per deployment, with
MODELswapped in:Before (8699998)
POST /v1/chat/completions
MODEL=gpt-5.6-privatelinkThe forwarder log shows no
CONNECT auth.openai.com:443and noCONNECT southcentralus.privatelink.api.openai.com:443for this call: the request never left the proxyMODEL=gpt-5.6-euMODEL=gpt-5.6-defaultForwarder log:
14:44:02 CONNECT auth.openai.com:443then14:44:02 CONNECT api.openai.com:443POST /v1/responses
MODEL=gpt-5.6-privatelinkForwarder log:
14:44:00 CONNECT southcentralus.privatelink.api.openai.com:443 -> MITM, relaying bytes to api.openai.com:443with noCONNECT auth.openai.com:443before it: the request went out with no bearerMODEL=gpt-5.6-euForwarder log:
14:44:01 CONNECT eu.api.openai.com:443 -> raw tunnel to the real host, again with no token exchange before itMODEL=gpt-5.6-defaultPOST /v1/messages
MODEL=gpt-5.6-privatelinkMODEL=gpt-5.6-euMODEL=gpt-5.6-defaultAfter (19da217)
POST /v1/chat/completions
MODEL=gpt-5.6-privatelinkForwarder log:
14:44:31 CONNECT auth.openai.com:443(token exchange) then14:44:31 CONNECT southcentralus.privatelink.api.openai.com:443 -> MITM, relaying bytes to api.openai.com:443andrewrote Host header to api.openai.com on POST /v1/chat/completionsMODEL=gpt-5.6-euForwarder log:
14:44:38 CONNECT auth.openai.com:443then14:44:38 CONNECT eu.api.openai.com:443. OpenAI now evaluates the authenticated project's residency instead of rejecting a missing keyMODEL=gpt-5.6-defaultForwarder log:
14:44:47 CONNECT auth.openai.com:443then14:44:48 CONNECT api.openai.com:443POST /v1/responses
MODEL=gpt-5.6-privatelinkForwarder log:
14:44:33 CONNECT auth.openai.com:443thenrewrote Host header to api.openai.com on POST /v1/responsesMODEL=gpt-5.6-euMODEL=gpt-5.6-defaultPOST /v1/messages
MODEL=gpt-5.6-privatelinkForwarder log:
14:44:35 CONNECT auth.openai.com:443then14:44:36 CONNECT southcentralus.privatelink.api.openai.com:443 -> MITMandrewrote Host header to api.openai.com on POST /v1/responses(the Messages route rides the Responses API upstream)MODEL=gpt-5.6-euMODEL=gpt-5.6-defaultObservations from the run, none caused by this PR:
eu.api.openai.comanswers varied between runs (pong once, geography error later), OpenAI-sideType
🐛 Bug Fix
Caveats (if any)
Low
_targets_openai_apiand should keep this versionci/circleci: litellm_router_unit_testingfailstest_no_linear_scans_in_routeron staging too (methods added by feat(router): limit heuristic_v2 auto-routers to one without the auto_router license feature #39468); this PR does not touch router.pyproxy-extras / Run testsfails fourTestMigrateDeployAttemptAccountingcases on staging's last Unit Tests runs too; this PR does not touch proxy-extrasci/circleci: e2e_ui_testingfailssearchUsers.spec.tson every branch that includes fix(ui): let the Internal Users search box match user_id as well as email #39604, whose placeholder rename the spec never followed (LIT-6919); the last staging pipeline that passed it predates that merge, and this PR touches no UI. None of the three is a required checkFinal Attestation
The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR
19da217 passes /live-pr-risk