Skip to content

Commit d60cafb

Browse files
pko89403claude
andcommitted
Fix confidence scorer loader misrouting joblib artifacts
load_lightgbm_scorer routed by whether metadata_path was passed: any metadata_path (explicit or auto-discovered sidecar) forced the raw LightGBM Booster loader. But train_confidence_scorer exports a self-contained joblib dict and also offers write_metadata_json, so a caller with both files who passes metadata_path (exactly what spec_confidence_runtime_readiness's from_artifact documents) got 'Unknown model format' on the pipeline's own artifact. Route by artifact content instead: try joblib.load first; fall back to the Booster loader only when the file is not a joblib pickle. metadata stays authoritative from the joblib dict; the sidecar/metadata_path is used only for raw Booster files that cannot carry it. Adds a regression test loading a joblib dict WITH metadata_path. Verified end-to-end on a real trained scorer.joblib. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent b614175 commit d60cafb

2 files changed

Lines changed: 48 additions & 7 deletions

File tree

src/ranksmith/confidence/scorer.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -156,20 +156,39 @@ def load_lightgbm_scorer(
156156
*,
157157
metadata_path: str | Path | None = None,
158158
) -> StructuralConfidenceScorer:
159+
# Route by artifact content, not by whether metadata_path was passed: the
160+
# training pipeline exports a self-contained joblib dict, so a caller who
161+
# also has a metadata sidecar (write_metadata_json) and passes metadata_path
162+
# must still load correctly. Only a raw LightGBM Booster file — which
163+
# joblib cannot unpickle — needs the sidecar/explicit metadata.
159164
artifact_path = Path(path)
165+
artifact = _try_load_joblib(artifact_path)
166+
if artifact is not _JOBLIB_LOAD_FAILED:
167+
return _joblib_scorer_from_artifact(artifact)
168+
160169
resolved_metadata_path = _resolve_metadata_path(artifact_path, metadata_path)
161-
if resolved_metadata_path is not None:
162-
return _load_lightgbm_booster_scorer(
163-
artifact_path,
164-
metadata_path=resolved_metadata_path,
170+
if resolved_metadata_path is None:
171+
raise ConfidenceArtifactError(
172+
"raw LightGBM model file requires a metadata sidecar or metadata_path"
165173
)
166-
return _load_joblib_scorer(artifact_path)
174+
return _load_lightgbm_booster_scorer(
175+
artifact_path,
176+
metadata_path=resolved_metadata_path,
177+
)
178+
167179

180+
_JOBLIB_LOAD_FAILED = object()
168181

169-
def _load_joblib_scorer(path: Path) -> StructuralConfidenceScorer:
182+
183+
def _try_load_joblib(path: Path) -> object:
170184
joblib = import_optional_dependency("joblib")
171-
artifact = joblib.load(path)
185+
try:
186+
return joblib.load(path)
187+
except Exception:
188+
return _JOBLIB_LOAD_FAILED
189+
172190

191+
def _joblib_scorer_from_artifact(artifact: object) -> StructuralConfidenceScorer:
173192
if _has_predict_confidence(artifact) and hasattr(artifact, "metadata"):
174193
return JoblibScorerWrapper(
175194
scorer=artifact,

tests/test_confidence_scorer.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,28 @@ def test_load_lightgbm_scorer_loads_joblib_dict_model(
299299
assert scorer.predict_confidence([0.0] * 70) == 0.6
300300

301301

302+
def test_load_lightgbm_scorer_loads_joblib_dict_with_metadata_path(
303+
monkeypatch: pytest.MonkeyPatch,
304+
tmp_path: Path,
305+
) -> None:
306+
# Regression: the training pipeline exports a joblib dict AND offers a
307+
# metadata sidecar, so a caller that passes metadata_path must still load
308+
# the joblib artifact instead of misrouting to the raw-Booster loader.
309+
install_fake_joblib(
310+
monkeypatch,
311+
{"model": FakePredictVectorModel(), "metadata": metadata_dict()},
312+
)
313+
metadata_path = tmp_path / "artifact.metadata.json"
314+
metadata_path.write_text(json.dumps(metadata_dict()), encoding="utf-8")
315+
316+
scorer = load_lightgbm_scorer(
317+
tmp_path / "artifact.joblib",
318+
metadata_path=metadata_path,
319+
)
320+
321+
assert scorer.predict_confidence([0.0] * 70) == 0.6
322+
323+
302324
def test_load_lightgbm_scorer_loads_joblib_wrapper_object(
303325
monkeypatch: pytest.MonkeyPatch,
304326
tmp_path: Path,

0 commit comments

Comments
 (0)