diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index 71bcdb15e71..13f78f30fa0 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -2113,29 +2113,47 @@ async def cursor_proxy_route( - GET /v0/repositories — List GitHub repositories Uses Basic Authentication (base64-encoded `API_KEY:`). + + Credential lookup order: + 1. passthrough_endpoint_router (config.yaml deployments with use_in_pass_through) + 2. litellm.credential_list (credentials added via UI) + 3. CURSOR_API_KEY environment variable """ import base64 base_target_url = os.getenv("CURSOR_API_BASE") or "https://api.cursor.com" - encoded_endpoint = httpx.URL(endpoint).path - - if not encoded_endpoint.startswith("/"): - encoded_endpoint = "/" + encoded_endpoint - - base_url = httpx.URL(base_target_url) - updated_url = base_url.copy_with(path=encoded_endpoint) cursor_api_key = passthrough_endpoint_router.get_credentials( custom_llm_provider="cursor", region_name=None, ) + if cursor_api_key is None: + for credential in litellm.credential_list: + if ( + credential.credential_info + and credential.credential_info.get("custom_llm_provider") == "cursor" + ): + cursor_api_key = credential.credential_values.get("api_key") + credential_api_base = credential.credential_values.get("api_base") + if credential_api_base: + base_target_url = credential_api_base + break + if cursor_api_key is None: raise HTTPException( status_code=401, - detail="Required 'CURSOR_API_KEY' in environment or credentials to make pass-through calls to Cursor.", + detail="Cursor API key not found. Add Cursor credentials via the UI (Models + Endpoints → LLM Credentials) or set CURSOR_API_KEY environment variable.", ) + encoded_endpoint = httpx.URL(endpoint).path + + if not encoded_endpoint.startswith("/"): + encoded_endpoint = "/" + encoded_endpoint + + base_url = httpx.URL(base_target_url) + updated_url = base_url.copy_with(path=encoded_endpoint) + auth_value = base64.b64encode( f"{cursor_api_key}:".encode("utf-8") ).decode("ascii") diff --git a/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py b/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py index 2c5662e69ac..8acfa2231cc 100644 --- a/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py +++ b/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py @@ -2439,6 +2439,9 @@ class TestCursorProxyRoute: with patch( "litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.passthrough_endpoint_router.get_credentials", return_value=None, + ), patch( + "litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.litellm.credential_list", + [], ): with pytest.raises(Exception) as exc_info: await cursor_proxy_route( @@ -2449,6 +2452,50 @@ class TestCursorProxyRoute: ) assert exc_info.value.status_code == 401 + @pytest.mark.asyncio + async def test_cursor_proxy_route_uses_ui_credential(self): + """should use credentials added via UI (litellm.credential_list) when env var is not set""" + from litellm.types.utils import CredentialItem + + mock_request = MagicMock(spec=Request) + mock_request.method = "GET" + mock_request.query_params = {} + mock_request.headers = {} + mock_response = MagicMock(spec=Response) + mock_user_api_key_dict = MagicMock() + + ui_credential = CredentialItem( + credential_name="my-cursor-key", + credential_values={"api_key": "crsr_ui_test_key", "api_base": "https://api.cursor.com"}, + credential_info={"custom_llm_provider": "cursor"}, + ) + + with patch( + "litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.passthrough_endpoint_router.get_credentials", + return_value=None, + ), patch( + "litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.litellm.credential_list", + [ui_credential], + ), patch( + "litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.create_pass_through_route" + ) as mock_create_route: + mock_endpoint_func = AsyncMock(return_value={"models": []}) + mock_create_route.return_value = mock_endpoint_func + + result = await cursor_proxy_route( + endpoint="v0/models", + request=mock_request, + fastapi_response=mock_response, + user_api_key_dict=mock_user_api_key_dict, + ) + + call_args = mock_create_route.call_args[1] + assert call_args["target"] == "https://api.cursor.com/v0/models" + + import base64 + expected_auth = base64.b64encode(b"crsr_ui_test_key:").decode("ascii") + assert call_args["custom_headers"]["Authorization"] == f"Basic {expected_auth}" + @pytest.mark.asyncio async def test_cursor_proxy_route_custom_api_base(self): """should use CURSOR_API_BASE env var when set"""