Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit bc2e672. Configure here.
Root-cause fixes for the two cursor review findings on the KV-cache branch, plus follow-through on the CI failures that surfaced during the fix. - Rename `_last_fitted_train_set_id` -> `fitted_train_set_id_` to adopt sklearn's convention for learned attributes, and route reads through a new `_resolve_train_set_id(estimator)` helper. This dissolves the model_id / clone / set_params desync: `fit()` no longer mutates `self.model_id`, so `get_params()` / `clone()` stay sklearn-correct. - Call `init()` at the top of `_predict` so the cross-process `load_model().predict()` flow authorizes the HTTP client instead of 401-ing. `init()` is idempotent so it's free on the hot path. - Make the resolver swallow ValueError/TypeError from `_cast_fitted_id` so a bad `model_id` degrades to "not fitted" (surfacing sklearn's standard NotFittedError) rather than crashing `__sklearn_is_fitted__`. - Add `@overload`s to `save_model()` so the return type narrows on the `path` argument (dict when omitted, Path when given). Improves IDE ergonomics for real users and eliminates the union-narrowing pyright errors that were failing Trunk. - Use `FitMode.FIT_WITH_CACHE` (not the raw string) in tests and drop an unused test fixture. Ruff format on the touched files. - New targeted tests: clone preserves constructor `model_id` and stays fitted; predict from a cold state triggers `init()`; an invalid `model_id` reports unfitted rather than crashing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| self.inference_precision = inference_precision | ||
| self.random_state = random_state | ||
| self.inference_config = inference_config | ||
| self.fit_mode: FitMode | None = fit_mode |
There was a problem hiding this comment.
Nit: probably not needed to specify the type as it will infer it anyway.
|
|
||
|
|
||
| def _build_model_handle( | ||
| estimator: Any, |
There was a problem hiding this comment.
We could have a type for the estimator here.
| ) -> dict[str, Any] | Path: | ||
| """Shared implementation of `save_model()` for both estimators.""" | ||
| check_is_fitted(estimator) | ||
| params = estimator.get_params(deep=False) |
There was a problem hiding this comment.
Here you want to use: estimator._get_tabpfn_config(), type safe and you get exactly the params that end up in tabpfn (no client_options for instance). Make it public if you prefer.
There was a problem hiding this comment.
The reason we use get_params is because _get_tabpfn_config returns wide-config fields only - it drops thinking_*, model_id etc., and save_model needs the full reproducibility set.
| return None | ||
|
|
||
|
|
||
| def _read_model_handle(handle: dict[str, Any] | str | Path) -> dict[str, Any]: |
There was a problem hiding this comment.
Nit: too much indirection here, I would avoid the extra function.
Drop `low_memory` and `batched` from the client-side FitMode enum. The server's public API (`FitRequest._validate_fit_mode`) only accepts `fit_preprocessors` and `fit_with_cache`; exposing the internal values in the client only invited runtime 400s. Type checkers now catch bad picks at construction time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| """Read + validate a `save_model()` handle for the target estimator class.""" | ||
| data = _read_model_handle(handle) | ||
| version = data.get("format_version") | ||
| if version != _MODEL_HANDLE_VERSION: |
There was a problem hiding this comment.
I would avoid the concept of _MODEL_HANDLE_VERSION altogether. The handle can be malformed for a multitude of reasons and in fact this could pass even when params is missing. I'd make a pydantic class for the model handle where params is a **TabpfnConfig, and if we fail to deserialize it we give a "Malformed error...possibly no longer support format" + we explain why the deserialization failed.
| def _load_model_handle_for( | ||
| cls: type, | ||
| handle: dict[str, Any] | str | Path, | ||
| task: Literal["classification", "regression"], |
There was a problem hiding this comment.
PredictionTask from api_models for type safety?
| crash `__sklearn_is_fitted__`. The estimator degrades to "not fitted" | ||
| and `predict` surfaces the standard `NotFittedError`. | ||
| """ | ||
| fitted = getattr(estimator, "fitted_train_set_id_", None) |
There was a problem hiding this comment.
I don't think we still need fitted_train_set_id_, why not always use model_id?
There was a problem hiding this comment.
I'd keep the fitted_train_set_id_ because of sklearn-compatibility. Specifically, if fit() writes to self.model_id, then clone(fitted_estimator) carries the fitted ID into the clone, which violates sklearn's clone contract - which should strip learned state.
| served from the cache instead of re-fitting. | ||
| client_options : ClientOptions, default=None | ||
| Client specific options (e.g. timeout, headers). | ||
| model_id : str, UUID or None, default=None |
There was a problem hiding this comment.
Safe to set the model_id directly? Shouldn't it be private and save/load are basically setter and getter?
There was a problem hiding this comment.
I'd keep it private because sklearn requires init params to be public attributes with the same name as the constructor args. Making it _model_id would break get_params() and clone()
| self._validate_targets_and_classes(y) | ||
|
|
||
| if Config.use_server: | ||
| # Create a new sentry trace at every fit, provided that: |
There was a problem hiding this comment.
I wonder whether we should change trace_id on predict_count > 0?
simo-prior
left a comment
There was a problem hiding this comment.
@safaricd left comments, let me know.
Focused cleanups from the ENG-880 review that don't need discussion: - Inline `_cast_fitted_id` into `_resolve_train_set_id`; only one caller remains after the resolver refactor. - Drop redundant `: FitMode | None` annotations on `self.fit_mode`; the assignment already inherits the parameter type. - Use `PredictionTask` enum in `_build_model_handle` / `_load_model_handle_for` signatures and at both call sites, replacing the free-form `Literal["classification", "regression"]`. - Replace the dict-based handle with a pydantic `_ModelHandle` model: free structural validation on load with a clear error, `format_version` kept as an explicit schema-evolution gate (orthogonal to structural validation), classes typed as an optional list. `save_model()` still returns a plain dict / Path for JSON portability. - Introduce a small `_SavableEstimator` Protocol so the helpers declare what they actually read instead of taking `Any`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Change Description
fit_with_cachepredictions - previously setting thefit_modewas blocked by the API.scikit-learnstandards.