Conversation
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>
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>
| 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.
There was a problem hiding this comment.
@safaricd but cloning after load_model() would equally carry learned state to the new estimator. I think what we want is just one variable but rather a model_id_ to differentiate from hyperparams. This would solve the cloning problem and also other unexpected behaviors like setting estimator.model_id silently not working if we ever called fit() on the model.
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>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
There are 3 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a27babb. Configure here.
… model_id_: drop resolver, legacy fitted_ flag and constructor param; store handle id top-level; restore optional predict_row_pairs_budget
ggprior
left a comment
There was a problem hiding this comment.
LGTM mostly,
ignore the stuff about save/load model -- if it's already there, fine
I think we should address leakage of server side internals
Also the fit_mode which looks auto-generated
Then this is GTG
|
|
||
| _VALID_THINKING_EFFORT_LEVELS = frozenset({"medium", "high"}) | ||
|
|
||
| # `FitMode` exposes only the two values gapi wires end-to-end: |
There was a problem hiding this comment.
internal info which is not needed here?
| ) = None | ||
|
|
||
|
|
||
| class FitMode(str, Enum): |
There was a problem hiding this comment.
why do we declare enums like low_memory? clients cannot set that
| self._fit_count = 0 | ||
|
|
||
| def __sklearn_is_fitted__(self) -> bool: | ||
| return getattr(self, "model_id_", None) is not None |
There was a problem hiding this comment.
is this actually true (even if you don't fit with cache, is this parameter set correctly)
There was a problem hiding this comment.
| @overload | ||
| def save_model(self, path: None = None) -> dict[str, Any]: ... | ||
| @overload | ||
| def save_model(self, path: str | Path) -> Path: ... |
There was a problem hiding this comment.
pretty cool! but isn't it conflicting with the server-side caching/dedup logic? Shouldn't we remove that first and then add save/load semantics afterwards?
There was a problem hiding this comment.
I feel this is overloading this PR to be honest. I.e. save/load semantics are a separate concern from adding KV cache support, and should be tracked separately.
I think this needs a lot more work in terms of testing before shipping something here
| self._last_train_set_description = None | ||
| self._fit_count = 0 | ||
|
|
||
| def __sklearn_is_fitted__(self) -> bool: |
There was a problem hiding this comment.
Why do we need the is fitted function here, it would be perfectly valid with underscore attribute convention as well
https://scikit-learn.org/stable/modules/generated/sklearn.utils.validation.check_is_fitted.html#sklearn.utils.validation.check_is_fitted
There was a problem hiding this comment.
On the classifier classes_ is set before the remote fit call succeeds, so model_id_ might never get populated. I don't think the regressor needs it today, but it doesn't hurt to keep it on both for consistency.

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