From 5123c3100ae87f608e72bf28dc93982b75469f2a Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 5 Nov 2026 17:57:42 -0800 Subject: [PATCH] fix(proxy): keep a name the ChatGPT account lists on the subscription despite a saved alias --- litellm/proxy/auth/user_api_key_auth.py | 5 +++ .../codex_passthrough_endpoints.py | 36 ++++++++++++++----- .../test_codex_passthrough_endpoints.py | 34 ++++++++++++++++-- 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/litellm/proxy/auth/user_api_key_auth.py b/litellm/proxy/auth/user_api_key_auth.py index 29924edf450..bcb9c30b799 100644 --- a/litellm/proxy/auth/user_api_key_auth.py +++ b/litellm/proxy/auth/user_api_key_auth.py @@ -344,6 +344,11 @@ async def _resolve_router_settings_model_group_alias( target: Final = resolve_model_group_alias(settings.get("model_group_alias"), requested) if target is None or target == requested: return + if request_dispatched_to_provider_pass_through(request): + from litellm.proxy.pass_through_endpoints.codex_passthrough_endpoints import native_turn_serves_through_litellm + + if not await native_turn_serves_through_litellm(requested, llm_router, request, valid_token): + return verbose_proxy_logger.debug( "router_settings.model_group_alias resolved %s -> %s before auth", requested.replace("\r", "").replace("\n", ""), diff --git a/litellm/proxy/pass_through_endpoints/codex_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/codex_passthrough_endpoints.py index dd4ee43197e..5430769ca68 100644 --- a/litellm/proxy/pass_through_endpoints/codex_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/codex_passthrough_endpoints.py @@ -276,6 +276,32 @@ async def _serves_through_litellm( return account_slugs is not None and model not in account_slugs +def _gateway_token(request: Request) -> str: + gateway_parts: Final = request.headers.get("x-litellm-api-key", "").split() + return ( + gateway_parts[-1] + if len(gateway_parts) == 1 or (len(gateway_parts) == 2 and gateway_parts[0].lower() == "bearer") + else "" + ) + + +def _forwarded_headers(request: Request) -> Mapping[str, str]: + return MappingProxyType({name: value for name, value in request.headers.items() if name in _HEADERS}) + + +async def native_turn_serves_through_litellm( + model: str, llm_router: "Router", request: Request, user_api_key_dict: UserAPIKeyAuth +) -> bool: + return await _serves_through_litellm( + model, + llm_router, + user_api_key_dict, + _models_etag_caller(user_api_key_dict, _gateway_token(request)), + _forwarded_headers(request), + codex_shared_cache(), + ) + + def _json_object(value: object) -> Mapping[str, object]: try: return _JSON_OBJECT.validate_python(value) @@ -672,13 +698,7 @@ async def codex_passthrough_route( authorization: Final = request.headers.get("authorization", "") scheme, _, token = authorization.partition(" ") - gateway_key: Final = request.headers.get("x-litellm-api-key", "") - gateway_parts: Final = gateway_key.split() - gateway_token: Final = ( - gateway_parts[-1] - if len(gateway_parts) == 1 or (len(gateway_parts) == 2 and gateway_parts[0].lower() == "bearer") - else "" - ) + gateway_token: Final = _gateway_token(request) if scheme.lower() != "bearer" or not token.strip() or not gateway_token: raise HTTPException( status_code=401, @@ -687,7 +707,7 @@ async def codex_passthrough_route( if hmac.compare_digest(token.strip().encode(), gateway_token.encode()): raise HTTPException(status_code=401, detail="ChatGPT and LiteLLM credentials must be separate") - headers: Final = MappingProxyType({name: value for name, value in request.headers.items() if name in _HEADERS}) + headers: Final = _forwarded_headers(request) caller: Final = _models_etag_caller(user_api_key_dict, gateway_token) if endpoint == "models": return await _catalog(request, user_api_key_dict, caller, headers, cache) diff --git a/tests/unit/proxy/pass_through_endpoints/test_codex_passthrough_endpoints.py b/tests/unit/proxy/pass_through_endpoints/test_codex_passthrough_endpoints.py index 7fa3d937214..0e4230d16e9 100644 --- a/tests/unit/proxy/pass_through_endpoints/test_codex_passthrough_endpoints.py +++ b/tests/unit/proxy/pass_through_endpoints/test_codex_passthrough_endpoints.py @@ -39,8 +39,7 @@ _HEADERS: Final = {"authorization": f"Bearer {_TOKEN}", "x-litellm-api-key": MAS @pytest.fixture def client(monkeypatch: pytest.MonkeyPatch, httpx_transport: None) -> Iterator[TestClient]: monkeypatch.setattr(proxy_server, "master_key", MASTER_KEY) - shared_cache: Final = DualCache() - monkeypatch.setitem(proxy_server.app.dependency_overrides, codex_shared_cache, lambda: shared_cache) + monkeypatch.setattr(proxy_server.proxy_logging_obj.internal_usage_cache, "dual_cache", DualCache()) yield TestClient(proxy_server.app) @@ -760,6 +759,37 @@ def test_native_route_keeps_a_chatgpt_turn_on_the_subscription_despite_a_saved_a assert [json.loads(call.request.content)["model"] for call in chatgpt.calls] == [_BUNDLED_SLUG] +def test_native_route_keeps_a_name_the_account_lists_on_the_subscription_despite_a_saved_alias( + client: TestClient, respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setattr(proxy_server, "prisma_client", "test-database") + monkeypatch.setattr( + proxy_server, "llm_router", Router(model_list=[_open_weights("gpt-reserve"), _open_weights("group-b")]) + ) + key: Final = "sk-test-alias-on-a-name-the-account-lists" + proxy_server.user_api_key_cache.set_cache( + key=hash_token(key), + value=UserAPIKeyAuth( + token=hash_token(key), + models=["gpt-reserve", "group-b"], + router_settings={"model_group_alias": {"gpt-reserve": "group-b"}}, + model_max_budget={"group-b": {"max_budget": 0.0, "budget_duration": "1d"}}, + ), + ) + catalog: Final = respx_mock.get(f"{_BASE}/models").respond(json={"models": [{"slug": "gpt-reserve"}]}) + provider: Final = _open_weights_provider(respx_mock) + chatgpt: Final = _chatgpt_responses(respx_mock) + response: Final = client.post( + "/backend-api/codex/responses", + json={"model": "gpt-reserve", "input": "Hello", "stream": True}, + headers={**_ACCOUNT_HEADERS, "x-litellm-api-key": key}, + ) + assert response.status_code == 200, response.text + assert not provider.called + assert [json.loads(call.request.content)["model"] for call in chatgpt.calls] == ["gpt-reserve"] + assert catalog.call_count == 1 + + def test_v1_request_for_a_gpt_name_still_reaches_the_catch_all_with_the_access_group_recipe( client: TestClient, respx_mock: respx.MockRouter, mixed_router: Router ) -> None: