Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
1 change: 1 addition & 0 deletions changelog/1339.changed.md
Original file line number Diff line number Diff line change
@@ -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.
17 changes: 7 additions & 10 deletions src/tabpfn/browser_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from typing import TYPE_CHECKING

from tabpfn.errors import (
LICENSE_ACCEPTANCE_HEADER,
TabPFNError,
TabPFNHuggingFaceGatedRepoError,
TabPFNLicenseError,
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -609,19 +608,17 @@ 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"
)

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"
Expand Down
13 changes: 10 additions & 3 deletions src/tabpfn/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,23 @@ 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."""

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"
Expand Down
16 changes: 15 additions & 1 deletion tests/test_browser_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Loading