fix(proxy): keep a name the ChatGPT account lists on the subscription despite a saved alias

This commit is contained in:
mateo-berri 2026-11-05 17:57:42 -08:00
parent 6680a8dbc4
commit 5123c3100a
3 changed files with 65 additions and 10 deletions

View file

@ -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", ""),

View file

@ -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)

View file

@ -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: