diff --git a/README.md b/README.md index f782b8c39..397643864 100644 --- a/README.md +++ b/README.md @@ -252,6 +252,8 @@ TabPFN requires **Python 3.10+** due to newer language features. Compatible vers On first use, TabPFN will automatically open a browser window where you can log in via [PriorLabs](https://ux.priorlabs.ai) and accept the license terms. Your authentication token is cached locally so you only need to do this once. +Accepting the license unlocks the model weights download. Your datasets stay local and are never sent to Prior Labs, and TabPFN works offline once the weights are downloaded (see the offline usage question below). + **For headless / CI environments** where a browser is not available, visit [https://ux.priorlabs.ai](https://ux.priorlabs.ai), go to the **License** tab to accept the license, and then set the `TABPFN_TOKEN` environment variable with a token obtained from your account. If access via the browser-based flow is not an option for you, please contact us at [`sales@priorlabs.ai`](mailto:sales@priorlabs.ai). diff --git a/changelog/1339.changed.md b/changelog/1339.changed.md new file mode 100644 index 000000000..534db213d --- /dev/null +++ b/changelog/1339.changed.md @@ -0,0 +1 @@ +The license acceptance prompts, license errors and README now state that your datasets stay local and are never sent to Prior Labs. diff --git a/src/tabpfn/browser_auth.py b/src/tabpfn/browser_auth.py index 8588ed32f..d7af97e6e 100644 --- a/src/tabpfn/browser_auth.py +++ b/src/tabpfn/browser_auth.py @@ -26,6 +26,7 @@ from typing import TYPE_CHECKING from tabpfn.errors import ( + LICENSE_ACCEPTANCE_HEADER, TabPFNError, TabPFNHuggingFaceGatedRepoError, TabPFNLicenseError, @@ -368,8 +369,7 @@ def _headless_interactive_login( login_url += f"?hf_repo_id={urllib.parse.quote(hf_repo_id)}" print( # noqa: T201 - "\nTabPFN requires a one-time license acceptance to download" - " model weights for local inference.\n" + f"\n{LICENSE_ACCEPTANCE_HEADER}" "\nNo display detected. Open this URL in a browser on another device:\n" f"\n {login_url}\n" f"\nAfter logging in, accept the license on the Licenses tab,\n" @@ -523,8 +523,7 @@ def try_browser_login(gui_url: str, hf_repo_id: str | None = None) -> str | None # --- print unified instructions --- print( # noqa: T201 - "\nTabPFN requires a one-time license acceptance to download" - " model weights for local inference." + f"\n{LICENSE_ACCEPTANCE_HEADER}" "\nOpening your browser to complete login/registration…\n" f"\n {login_url}\n" "\nWaiting for login to complete…\n" @@ -609,9 +608,8 @@ def ensure_license_accepted(hf_repo_id: str) -> Literal[True]: # noqa: C901 no_browser = os.environ.get("TABPFN_NO_BROWSER", "").strip() if no_browser and no_browser not in ("0", "false", "no", "off"): raise TabPFNLicenseError( - "TabPFN requires a one-time license acceptance to download\n" - "model weights for local inference, but browser login is\n" - "disabled (TABPFN_NO_BROWSER is set).\n\n" + f"{LICENSE_ACCEPTANCE_HEADER}\n" + "Browser login is disabled (TABPFN_NO_BROWSER is set).\n\n" "Set the TABPFN_TOKEN environment variable with a valid API key\n" "obtained from https://ux.priorlabs.ai" ) @@ -619,9 +617,8 @@ def ensure_license_accepted(hf_repo_id: str) -> Literal[True]: # noqa: C901 token = try_browser_login(gui_url, hf_repo_id=hf_repo_id) if token is None: raise TabPFNLicenseError( - "TabPFN requires a one-time license acceptance to download\n" - "model weights for local inference, but no interactive terminal\n" - "is available.\n\n" + f"{LICENSE_ACCEPTANCE_HEADER}\n" + "There is no interactive terminal available.\n\n" "To authenticate in a non-interactive environment:\n" f" 1. Open {gui_url} in a browser and log in (or register)\n" f" 2. Accept the license on the Licenses tab\n" diff --git a/src/tabpfn/errors.py b/src/tabpfn/errors.py index 3e72cb605..3d14371cb 100644 --- a/src/tabpfn/errors.py +++ b/src/tabpfn/errors.py @@ -28,6 +28,15 @@ class TabPFNValidationError(ValueError, TabPFNUserError): """User provided invalid data (shape, NaNs, categories, etc.).""" +# Users hesitate to accept when they think it shares their datasets with us. Only +# the API key and license version are sent; data never leaves the machine. +LICENSE_ACCEPTANCE_HEADER = ( + "TabPFN requires a one-time license acceptance to download model weights\n" + "for local inference. Your datasets stay local and are never sent to Prior\n" + "Labs, and TabPFN works offline once the weights are downloaded.\n" +) + + class TabPFNLicenseError(TabPFNError): """Error raised when the user has not accepted the TabPFN license.""" @@ -35,9 +44,7 @@ def __init__(self, message: str | None = None): if message is None: gui_url = settings.tabpfn.auth_gui_url message = ( - "TabPFN requires a one-time license acceptance" - " to download model weights for local" - " inference.\n\n" + f"{LICENSE_ACCEPTANCE_HEADER}\n" "To authenticate in a non-interactive" " environment:\n" f" 1. Open {gui_url} in a browser" diff --git a/tests/test_browser_auth.py b/tests/test_browser_auth.py index cf49f19ab..93b59edbe 100644 --- a/tests/test_browser_auth.py +++ b/tests/test_browser_auth.py @@ -587,6 +587,17 @@ def test_login_url_includes_hf_repo_id(self, capsys: pytest.CaptureFixture[str]) captured = capsys.readouterr() assert "hf_repo_id=tabpfn_2_6" in captured.out + def test_prompt_says_data_stays_local(self, capsys: pytest.CaptureFixture[str]): + from tabpfn.errors import LICENSE_ACCEPTANCE_HEADER # noqa: PLC0415 + + headless_login = self._import_headless() + with ( + patch("tabpfn.browser_auth._headless_cbreak_loop", return_value=None), + patch("tabpfn.browser_auth._headless_readline_loop", return_value=None), + ): + headless_login("https://ux.priorlabs.ai") + assert LICENSE_ACCEPTANCE_HEADER in capsys.readouterr().out + # --------------------------------------------------------------------------- # try_browser_login routing @@ -625,8 +636,10 @@ def test_headless_routes_to_headless_login(self): "https://ux.priorlabs.ai", hf_repo_id="tabpfn_2_6" ) - def test_graphical_opens_browser(self): + def test_graphical_opens_browser(self, capsys: pytest.CaptureFixture[str]): """TTY + display → opens browser (existing flow).""" + from tabpfn.errors import LICENSE_ACCEPTANCE_HEADER # noqa: PLC0415 + try_login = self._import_try_login() with ( patch("tabpfn.browser_auth.sys.stdin") as mock_stdin, @@ -639,3 +652,4 @@ def test_graphical_opens_browser(self): assert result == "browser-jwt" mock_browser.assert_called_once() + assert LICENSE_ACCEPTANCE_HEADER in capsys.readouterr().out