fix(backends): surface real cause on HF model load failures#21
fix(backends): surface real cause on HF model load failures#21shingonoide wants to merge 9 commits into
Conversation
lucasmundim
left a comment
There was a problem hiding this comment.
Solid PR — the exception hierarchy ordering is correct (verified against the live huggingface_hub MRO: GatedRepoError → RepositoryNotFoundError → HfHubHTTPError → OSError → Exception), all raises chain via from e, and messages are actionable. Test coverage is thorough with one test per branch plus chain-cause verification.
One minor suggestion inline.
|
|
||
| def test_timm_import_error_gives_actionable_message(self): | ||
| with patch( | ||
| "transformers.AutoImageProcessor.from_pretrained", |
There was a problem hiding this comment.
Nit: every test in this class patches AutoImageProcessor.from_pretrained (the first call in the try block). Since both from_pretrained calls sit in the same try, the handlers work identically regardless of which call raises — so this is correct today.
However, if someone later refactors the try block (e.g. splits processor and model loading into separate try/except), the model-download error path would be untested. Consider adding one test (e.g. for RepositoryNotFoundError) that patches AutoModelForImageClassification.from_pretrained instead, to anchor both paths. Same applies to the object-detection test file.
Low priority — not blocking.
There was a problem hiding this comment.
Good catch — applied in c3ecda6. Added one model-side patch test per file (test_repository_not_found_on_model_download_gives_actionable_message) so both from_pretrained call sites stay covered if the try/except ever gets split.
There was a problem hiding this comment.
Pull request overview
Improves debuggability of HuggingFace model load failures by surfacing specific, actionable root-cause errors (Hub auth/gating, missing repo/revision, HTTP failures, architecture mismatches) in the vision backends, with parallel unit-test coverage and a lockfile sync.
Changes:
- Add structured exception handling in
ObjectDetectionBackend.load()andImageClassificationBackend.load()to emit targeted error messages while preserving chained tracebacks (raise ... from e). - Add unit tests covering each error branch for both backends.
- Sync
uv.lockto match the existingopenfilter[all]>=0.1.27pin inpyproject.toml.
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
filter_huggingface_vision/backends/object_detection.py |
Adds granular HuggingFace Hub + ValueError/fallback error handling for object detection model loads. |
filter_huggingface_vision/backends/image_classification.py |
Adds parallel granular error handling for image classification model loads. |
tests/test_object_detection.py |
Adds targeted tests validating the new object detection load error messages and exception chaining. |
tests/test_image_classification.py |
Adds targeted tests validating the new image classification load error messages and exception chaining. |
uv.lock |
Updates the locked openfilter[all] specifier to >=0.1.27 to match pyproject.toml. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Reviewer nit from PR #21 — keeps both from_pretrained call sites covered if the single try/except ever gets split. Signed-off-by: Rui Andrada <randrada@plainsight.ai>
|
Round 1 reply:
|
leandrobmarinho
left a comment
There was a problem hiding this comment.
Code Review
Overview
This PR significantly improves the developer experience by replacing a single catch-all except Exception in both backend load() methods with eight targeted exception branches. Errors that were previously opaque ("not compatible with ...") now surface actionable messages — especially valuable for GT Automation operators hitting misconfigured model IDs, gated repos, or wrong revision pins.
The exception ordering is correct: GatedRepoError is caught before RepositoryNotFoundError (which is its parent in huggingface_hub ≥ 1.10). Test coverage is thorough, all 9 branches per backend are exercised.
Issues / Suggestions
1. Duplicated exception-handling logic between backends (maintainability risk)
The entire exception block in object_detection.py and image_classification.py is nearly identical. If a new HF Hub exception appears in a future huggingface_hub release (e.g. EntryNotFoundError, which already exists), both files must be updated in sync. A shared context manager or utility function would eliminate this drift risk:
# backends/_hf_load_errors.py
from contextlib import contextmanager
from huggingface_hub import errors as _hf_errors
@contextmanager
def hf_load_error_handler(model_id: str, revision: str, task: str):
try:
yield
except ImportError as e:
if "timm" in str(e).lower():
raise ImportError(
f"This model (e.g. {model_id}) requires the timm library. "
"Install it with: pip install timm"
) from e
raise
except _hf_errors.GatedRepoError as e:
raise RuntimeError(
f"Access to '{model_id}' requires accepting its license on the Hub. "
"Accept it with an authenticated token."
) from e
# ... rest of branches
except Exception as e:
raise RuntimeError(
f"Unexpected failure loading '{model_id}@{revision}' for {task}: {repr(e)}"
) from eThen both load() methods become:
with hf_load_error_handler(model_id, revision, "image classification"):
self._processor = AutoImageProcessor.from_pretrained(...)
self._model = AutoModelForImageClassification.from_pretrained(...)2. Inconsistent repr(e) inclusion in error messages
HfHubHTTPError, ValueError, and the fallback Exception include repr(e) in the message, but GatedRepoError, RepositoryNotFoundError, and RevisionNotFoundError do not. The original exception details (e.g. the specific 401/403 response body from HF Hub) can be useful when the structured message alone isn't enough to diagnose the issue.
Suggest adding {repr(e)} to those three branches for consistency:
except _hf_errors.GatedRepoError as e:
raise RuntimeError(
f"Access to '{model_id}' requires accepting its license on the Hub. "
f"Accept it with an authenticated token. Detail: {repr(e)}"
) from e3. Tests only mock AutoImageProcessor.from_pretrained, not AutoModel*.from_pretrained
All test cases inject exceptions via AutoImageProcessor.from_pretrained. However, the same HF Hub errors (RepositoryNotFoundError, RevisionNotFoundError, etc.) can also be raised by AutoModelForImageClassification.from_pretrained / AutoModelForObjectDetection.from_pretrained — e.g., when the processor is cached locally but the model weights are not. The exception handlers cover both calls, but the tests don't verify the model-loading half.
This is a low-risk gap for now (the handlers are identical), but worth noting so a future refactor doesn't accidentally break the second from_pretrained call.
4. _make_hf_error helper is duplicated across test classes
TestImageClassificationBackendLoadErrors._make_hf_error and TestObjectDetectionBackendLoadErrors._make_hf_error are identical. A module-level helper or a shared BaseHFLoadErrorTests mixin would eliminate the duplication:
def _make_hf_error(cls, message):
mock_response = MagicMock()
mock_response.status_code = 404
return cls(message, response=mock_response)5. uv.lock manual edit concern
The uv.lock change updates the requires-dist constraint for openfilter from >=0.1.26 to >=0.1.27. Lock files should generally be regenerated via uv lock rather than hand-edited to avoid subtle inconsistencies between the resolved package graph and what's recorded. If this is intentional (pyproject.toml was already bumped separately), it's fine — but it's worth confirming that uv lock --check passes after this change.
Summary
| Core logic | Correct — exception ordering, from e chaining, and message content are all sound |
| Code duplication | Moderate concern — two identical 30-line blocks will diverge over time |
| Test coverage | Good overall; minor gap on the model-weights loading path |
repr(e) consistency |
Minor — three branches omit it while others include it |
uv.lock |
Low risk but verify uv lock --check passes |
The approach is solid and the improvement to operator experience is clear. Addressing the duplication (issue 1) would be the highest-value change before merging; the rest are polish.
| "Install it with: pip install timm" | ||
| ) from e | ||
| raise | ||
| except _hf_errors.GatedRepoError as e: |
There was a problem hiding this comment.
Inconsistent repr(e) in error messages
HfHubHTTPError, ValueError, and the fallback Exception include repr(e), but GatedRepoError, RepositoryNotFoundError, and RevisionNotFoundError (this line and the two below) do not. The original HF Hub response body — e.g. a 401/403 payload — can be valuable when the structured message alone is not enough.
Consider adding it for consistency:
except _hf_errors.GatedRepoError as e:
raise RuntimeError(
f"Access to '{model_id}' requires accepting its license on the Hub. "
f"Accept it with an authenticated token. Detail: {repr(e)}"
) from eSame applies to RepositoryNotFoundError and RevisionNotFoundError.
There was a problem hiding this comment.
Addressed in 42d6b89: the shared hf_load_error_handler context manager now carries repr(e) on every branch (including GatedRepoError, RepositoryNotFoundError, RevisionNotFoundError). Thanks for the catch.
| self._model = AutoModelForImageClassification.from_pretrained( | ||
| model_id, revision=revision, trust_remote_code=False | ||
| ) | ||
| except ImportError as e: |
There was a problem hiding this comment.
Duplicated exception-handling block — consider a shared context manager
This entire except ladder is nearly identical to the one in object_detection.py. If a new HF Hub exception needs to be added in the future (e.g. EntryNotFoundError, which already exists in huggingface_hub), both files must be updated in sync — a common source of drift.
A shared helper in backends/_hf_load_errors.py (or similar) would centralise this logic:
from contextlib import contextmanager
from huggingface_hub import errors as _hf_errors
@contextmanager
def hf_load_error_handler(model_id: str, revision: str, task: str):
try:
yield
except ImportError as e:
if "timm" in str(e).lower():
raise ImportError(
f"This model (e.g. {model_id}) requires the timm library. "
"Install it with: pip install timm"
) from e
raise
except _hf_errors.GatedRepoError as e:
raise RuntimeError(
f"Access to '{model_id}' requires accepting its license on the Hub. "
"Accept it with an authenticated token."
) from e
# … other branches …
except Exception as e:
raise RuntimeError(
f"Unexpected failure loading '{model_id}@{revision}' for {task}: {repr(e)}"
) from eThen both load() methods collapse to:
with hf_load_error_handler(model_id, revision, "image classification"):
self._processor = AutoImageProcessor.from_pretrained(...)
self._model = AutoModelForImageClassification.from_pretrained(...)Not a blocker, but worth addressing before the next time this block needs to change.
There was a problem hiding this comment.
Applied in 42d6b89: extracted filter_huggingface_vision/backends/_hf_load_errors.py with a hf_load_error_handler(model_id, revision, task) context manager. Both object_detection and image_classification now delegate to it, and round 5 (1f3de3d) extends the same wrapper to grounding_dino and owlvit.
| "Install it with: pip install timm" | ||
| ) from e | ||
| raise | ||
| except _hf_errors.GatedRepoError as e: |
There was a problem hiding this comment.
Same repr(e) inconsistency as in image_classification.py
GatedRepoError, RepositoryNotFoundError, and RevisionNotFoundError drop the original exception detail. Adding {repr(e)} to their messages (as done for HfHubHTTPError and the fallback) keeps error reporting consistent and preserves HF Hub response bodies in logs.
Also mirrors the duplication comment on the classification backend — this block is a copy of the one there.
There was a problem hiding this comment.
Same as the classification-side thread — repr(e) is now consistent across every branch after the shared handler extraction in 42d6b89.
|
|
||
| # --- HuggingFace Hub error branches --- | ||
|
|
||
| def _make_hf_error(self, cls, message): |
There was a problem hiding this comment.
_make_hf_error is duplicated in test_object_detection.py
Both test classes define the identical helper. A module-level function or a shared BaseHFLoadErrorTests mixin would remove the duplication:
# at module level, before the test classes
def _make_hf_error(cls, message):
mock_response = MagicMock()
mock_response.status_code = 404
return cls(message, response=mock_response)Alternatively, both test classes could inherit from a common base that provides this method. Minor, but avoids the two copies drifting if the constructor signature changes in a future huggingface_hub release.
There was a problem hiding this comment.
Applied in 42d6b89: extracted tests/_hf_test_utils.py with a shared make_hf_error(cls, message, status_code=404) helper. Both test files import it instead of defining local copies.
|
|
||
| # --- HuggingFace Hub error branches --- | ||
|
|
||
| def _make_hf_error(self, cls, message): |
There was a problem hiding this comment.
Duplicate of _make_hf_error defined in test_image_classification.py
Same note as the classification test: this helper is byte-for-byte identical in both files. Moving it to a shared location removes the duplication.
There was a problem hiding this comment.
Same fix — _make_hf_error moved to tests/_hf_test_utils.py in 42d6b89.
| { name = "huggingface-hub" }, | ||
| { name = "isort", marker = "extra == 'dev'", specifier = "==7.0.0" }, | ||
| { name = "openfilter", extras = ["all"], specifier = ">=0.1.26" }, | ||
| { name = "openfilter", extras = ["all"], specifier = ">=0.1.27" }, |
There was a problem hiding this comment.
Lock file manually edited — consider regenerating via uv lock
The requires-dist specifier for openfilter was changed from >=0.1.26 to >=0.1.27 by hand. Lock files are generated artifacts; editing them directly can leave the recorded dependency graph inconsistent with what uv lock would produce.
If pyproject.toml already has >=0.1.27, running uv lock (or uv lock --check to verify) is the safer path. If this was done intentionally because the uv version used in CI cannot regenerate the file cleanly, a note in the PR description explaining why would help future readers.
There was a problem hiding this comment.
Thanks — to clarify, no manual edit here. The main branch's pyproject.toml already pinned openfilter[all]>=0.1.27 (see pyproject.toml:28 on main), but uv.lock on main was still at >=0.1.26 — it had drifted out of sync. When I ran uv sync to set up the environment, uv regenerated the lockfile to match pyproject.toml, and I'm shipping that sync in this PR rather than leaving main drifted. uv lock --check passes on the branch.
There was a problem hiding this comment.
Just to make the earlier reply crisper: the uv.lock change wasn't a manual edit. It was produced automatically by uv sync reconciling the lockfile with pyproject.toml (which on main already pins openfilter[all]>=0.1.27). So uv itself made the adjustment, we're just shipping the sync that should already have been on main.
… everywhere Addresses Leandro's review on PR #21 (review PRR_kwDORDqaqs74SxH4): 1. Shared exception handler (Leandro #1): extracted `hf_load_error_handler` into `filter_huggingface_vision/backends/_hf_load_errors.py` as a `@contextmanager`. Both `object_detection.py` and `image_classification.py` now wrap their two `from_pretrained` calls in `with hf_load_error_handler(...)`, eliminating the duplicated eight-branch try/except. Future huggingface_hub error types only need to be updated in one place. 2. repr(e) consistency (Leandro #2): `GatedRepoError`, `RepositoryNotFoundError`, and `RevisionNotFoundError` now include `Detail: {repr(e)}` in their messages, consistent with the HfHubHTTPError/ValueError/fallback branches. 3. Model-side patch coverage (Leandro #3): addressed in prior commit c3ecda6 per @lucasmundim's inline nit. 4. _make_hf_error deduplication (Leandro #4): moved to `tests/_hf_test_utils.py` as a module-level `make_hf_error` function; both test files import from there. Behavior unchanged — same messages, same exception mapping, de-duplicated. Signed-off-by: Rui Andrada <randrada@plainsight.ai>
|
Round 2 reply to @leandrobmarinho (commit e67b1ef):
|
… everywhere Addresses Leandro's review on PR #21 (review PRR_kwDORDqaqs74SxH4): 1. Shared exception handler (Leandro #1): extracted `hf_load_error_handler` into `filter_huggingface_vision/backends/_hf_load_errors.py` as a `@contextmanager`. Both `object_detection.py` and `image_classification.py` now wrap their two `from_pretrained` calls in `with hf_load_error_handler(...)`, eliminating the duplicated eight-branch try/except. Future huggingface_hub error types only need to be updated in one place. 2. repr(e) consistency (Leandro #2): `GatedRepoError`, `RepositoryNotFoundError`, and `RevisionNotFoundError` now include `Detail: {repr(e)}` in their messages, consistent with the HfHubHTTPError/ValueError/fallback branches. 3. Model-side patch coverage (Leandro #3): addressed in prior commit c3ecda6 per @lucasmundim's inline nit. 4. _make_hf_error deduplication (Leandro #4): moved to `tests/_hf_test_utils.py` as a module-level `make_hf_error` function; both test files import from there. Behavior unchanged — same messages, same exception mapping, de-duplicated. Bumps VERSION v0.4.4 → v0.4.5 (patch bump; behavioral fix, no API change) and adds the corresponding RELEASE.md entry to satisfy CI check-release-log. Signed-off-by: Rui Andrada <randrada@plainsight.ai>
e67b1ef to
42d6b89
Compare
There was a problem hiding this comment.
Pull request overview
This PR improves debuggability of HuggingFace model load failures in the vision backends by replacing a generic catch-all with structured, actionable exception handling shared via a new context manager.
Changes:
- Added a shared
hf_load_error_handlercontext manager that maps common HF Hub/load failures to clearer, chained exceptions. - Updated
ObjectDetectionBackend.load()andImageClassificationBackend.load()to use the shared handler. - Added/expanded unit tests to cover each error branch and synced
uv.lock/release metadata for the new version.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
filter_huggingface_vision/backends/_hf_load_errors.py |
New shared context manager implementing structured HF load error handling. |
filter_huggingface_vision/backends/object_detection.py |
Swaps inline try/except for the shared load error handler. |
filter_huggingface_vision/backends/image_classification.py |
Swaps inline try/except for the shared load error handler. |
tests/_hf_test_utils.py |
Adds shared helper for constructing HF Hub exceptions in tests. |
tests/test_object_detection.py |
Adds backend load-error tests for object detection. |
tests/test_image_classification.py |
Adds backend load-error tests for image classification. |
uv.lock |
Synces openfilter[all] to >=0.1.27 per pyproject.toml. |
VERSION |
Bumps version to v0.4.5. |
RELEASE.md |
Adds v0.4.5 release notes describing the new error handling and tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| """Shared HuggingFace Hub load-error handler for vision backends.""" | ||
| from contextlib import contextmanager | ||
|
|
||
| from huggingface_hub import errors as _hf_errors |
There was a problem hiding this comment.
Good catch — applied in daa596b. Added huggingface-hub>=0.23 to pyproject.toml (and regenerated uv.lock via uv lock). 0.23 is the floor that has the huggingface_hub.errors module layout used here (GatedRepoError, RevisionNotFoundError, RepositoryNotFoundError, HfHubHTTPError all present).
There was a problem hiding this comment.
Addressed in commit 6ed6413. Instead of bumping the floor, I switched the import from huggingface_hub.errors to huggingface_hub.utils — that path has been stable since hub ~0.16 and is available at the declared floor (>=0.23). Every exception class we need (GatedRepoError, RepositoryNotFoundError, RevisionNotFoundError, HfHubHTTPError, EntryNotFoundError, LocalEntryNotFoundError) is importable from huggingface_hub.utils across the whole supported range. Floor in pyproject.toml is unchanged.
| def make_hf_error(cls, message): | ||
| """Build a huggingface_hub error instance with a minimal mock response | ||
| attached, suitable for use in backend load-error tests.""" | ||
| mock_response = MagicMock() | ||
| mock_response.status_code = 404 |
There was a problem hiding this comment.
Good catch — applied in daa596b. make_hf_error now takes an optional status_code parameter (defaults to 404) so tests simulating 401/403/503 can set the code without monkey-patching. Existing call sites keep working unchanged.
…lper - pyproject.toml / uv.lock: add `huggingface-hub>=0.23` lower bound. The new `_hf_load_errors` module imports `huggingface_hub.errors.GatedRepoError` and `RevisionNotFoundError`, which are only present from ~0.22+. Without a floor, an older resolved hub version would break at module import time. uv.lock is regenerated via `uv lock` (no manual edit). - tests/_hf_test_utils.py: `make_hf_error` now accepts an optional `status_code` parameter (defaults to 404). Future tests that simulate 401/403/503 conditions can set the code without monkey-patching the helper, and assertions touching the status code stay accurate. Addresses Copilot review comments on PR #21 (discussion_r3133786636 and discussion_r3133786674). Signed-off-by: Rui Andrada <randrada@plainsight.ai>
|
Round 3 — Copilot follow-ups applied on top of Copilot comments addressed:
State:
|
|
Round 4 reply (commit 6ed6413): B1 — HF exception import path switched from B2 — S1 — ValueError message softened: dropped the "exposes the required head" assertion — S2 — two sub-items:
S4 — Tests: 85/85 pass. New cases cover |
lucasmundim
left a comment
There was a problem hiding this comment.
Looks good. Both comments from the first review have been addressed: the shared hf_load_error_handler context manager eliminates duplication, and the model-download path is now tested (test_repository_not_found_on_model_download_gives_actionable_message).
Exception hierarchy ordering is correct (verified against the live MRO: GatedRepoError → RepositoryNotFoundError → HfHubHTTPError → OSError → Exception), all raises chain via from e, repr(e) is now consistently included in every branch, and the huggingface-hub>=0.23 floor correctly guards the errors module import.
A few non-blocking observations inline.
| except Exception as e: | ||
| raise RuntimeError( | ||
| f"Unexpected failure loading '{model_id}@{revision}' for {task}: {repr(e)}" | ||
| ) from e |
There was a problem hiding this comment.
Follow-up suggestion: grounding_dino.py and owlvit.py also call from_pretrained with no error handling at all — they’d be natural candidates to wrap with this context manager in a subsequent PR.
There was a problem hiding this comment.
Applied in 1f3de3d. Both grounding_dino.py and owlvit.py now wrap their from_pretrained calls in hf_load_error_handler, and tests/test_grounding_dino.py + tests/test_owlvit.py grow matching load-error test classes covering RepositoryNotFoundError, RevisionNotFoundError, and ValueError.
| @@ -0,0 +1,14 @@ | |||
| """Shared test utilities for HuggingFace Hub error-handling tests.""" | |||
There was a problem hiding this comment.
Nit: this bare import (from _hf_test_utils import make_hf_error) works via pytest’s default prepend import mode, but there’s no conftest.py or __init__.py in tests/ to make it explicit. Consider adding [tool.pytest.ini_options] pythonpath = ["tests"] in pyproject.toml so the mechanism is documented rather than relying on pytest internals.
There was a problem hiding this comment.
Applied in 1f3de3d. Added [tool.pytest.ini_options] pythonpath = ["tests"] to pyproject.toml so the from _hf_test_utils import make_hf_error shorthand is documented via pytest's own config rather than relying on prepend import mode implicitly.
| - Deduplicated the `make_hf_error` test helper into `tests/_hf_test_utils.py` (shared util). | ||
| - Synced `uv.lock` with `pyproject.toml`'s existing `openfilter[all]>=0.1.27` pin. | ||
|
|
||
|
|
There was a problem hiding this comment.
Ultra-nit: double blank line here (before v0.4.4).
There was a problem hiding this comment.
Applied in 1f3de3d — double blank line before v0.4.4 removed; v0.4.5 notes also expanded to reflect the rounds 3-5 additions.
|
Round 5 — Lucas's follow-ups applied, previously-missed Leandro threads acknowledged. Commit: Applied from @lucasmundim's round-4 review:
Replied to @leandrobmarinho's earlier threads (repr(e) consistency, shared context manager, duplicated `_make_hf_error`) — all were addressed by the 42d6b89 extraction; inline replies posted pointing to that commit. Follow-up filed: PLAT-962 tracks adding retry/backoff on transient Hub 429/5xx at load time. Not included in this PR — different concern (reliability, vs error surface quality). Status: 47/47 non-torchvision tests pass locally; torchvision-dependent tests (object_detection / image_classification) rely on CI. CI running on `1f3de3d`. |
lucasmundim
left a comment
There was a problem hiding this comment.
Good expansion of the error coverage. Verified the new exception hierarchy against huggingface_hub 1.7.1:
EntryNotFoundError→Exception(notHfHubHTTPError) — docstring is correctLocalEntryNotFoundError→FileNotFoundError→OSError→EntryNotFoundError
Catch order is correct. The _MISSING_DEP_HINTS allowlist, 401 detection, and huggingface_hub.utils import path are all solid choices.
One minor observation inline (non-blocking).
| ) from e | ||
| except (LocalEntryNotFoundError, EntryNotFoundError) as e: | ||
| raise RuntimeError( | ||
| f"Model entry for '{model_id}@{revision}' not found in cache. " |
There was a problem hiding this comment.
Minor: EntryNotFoundError (without "Local") can also indicate a remote file-not-found — e.g. the repo exists but is missing config.json. The "not found in cache" / "pre-download the model" / "retry after the first worker" guidance doesn’t apply to that case.
Consider either splitting the two classes into separate handlers, or broadening the message to cover both scenarios — e.g.:
Non-blocking.
There was a problem hiding this comment.
Right, the cache-miss copy reads wrong when it's actually a missing remote file. Broadened the message to cover both repo missing expected files OR local cache incomplete, plus the offline pre-download hint, in a33fb6d.
There was a problem hiding this comment.
Approval — all items from the previous review have been addressed
Going through each point requested:
1. Duplicated exception-handling logic across backends → Resolved
The hf_load_error_handler context manager was extracted to filter_huggingface_vision/backends/_hf_load_errors.py. Any new huggingface_hub exception type now only needs to be added in one place. All four backends (object_detection, image_classification, grounding_dino, owlvit) now use the shared handler.
2. Inconsistent repr(e) in error messages → Resolved
GatedRepoError, RepositoryNotFoundError, and RevisionNotFoundError now include repr(e) in their messages, consistent with HfHubHTTPError, ValueError, and the fallback Exception.
3. Test coverage limited to processor path → Resolved
test_repository_not_found_on_model_download_gives_actionable_message was added, covering the error path during model weight download — the gap identified in the previous review.
4. Duplicated _make_hf_error helper → Resolved
Centralized in tests/_hf_test_utils.py with [tool.pytest.ini_options] pythonpath = ["tests"] in pyproject.toml, eliminating duplication across test files.
5. Manual uv.lock edit → Resolved
Lock file synced with the existing pin in pyproject.toml.
Solid implementation. Using huggingface_hub.utils (stable since 0.23) instead of huggingface_hub.errors is a good compatibility choice. Extending the handler to GroundingDinoBackend and OwlVitBackend — beyond the original scope — closes an inconsistency that would have surfaced eventually.
LGTM.
stwilt
left a comment
There was a problem hiding this comment.
Net-new findings only (prior threads are out of scope). Three items — all non-blocking; the first two are tied together as a maintenance footgun around the except ladder in hf_load_error_handler, the third is a test-quality nit.
| - (LocalEntryNotFoundError, EntryNotFoundError) before HfHubHTTPError | ||
| because they do NOT inherit from it and would otherwise fall through to | ||
| the generic fallback. |
There was a problem hiding this comment.
Docstring justification is factually wrong at the declared floor. Against huggingface-hub==0.23 (the pin added in daa596b), both classes DO inherit from HfHubHTTPError:
EntryNotFoundError -> HfHubHTTPError -> HTTPError -> RequestException -> OSError -> Exception
LocalEntryNotFoundError -> EntryNotFoundError -> HfHubHTTPError -> HTTPError -> RequestException -> FileNotFoundError -> OSError -> ValueError -> Exception
The catch order is still correct — but for the opposite reason the comment states: these branches must come first precisely because they're subclasses of HfHubHTTPError (and in LocalEntryNotFoundError's case, also ValueError), so without this explicit handler they'd get swallowed by the later generic branches with the wrong message. A future maintainer reading this docstring could reasonably conclude the ordering is cosmetic and safely reorder — please correct the justification.
There was a problem hiding this comment.
Good catch. Verified against huggingface-hub==0.23.0 with the requests backbone. EntryNotFoundError IS an HfHubHTTPError there, and LocalEntryNotFoundError is additionally a ValueError. Rewrote the docstring to cite both eras (0.23 subclass-chain + >=1.0 flattened) so a future maintainer doesn't reorder based on a stale justification. Commit: ab15532.
| except (LocalEntryNotFoundError, EntryNotFoundError) as e: | ||
| raise RuntimeError( | ||
| f"Model entry for '{model_id}@{revision}' not found in cache. " | ||
| f"If running offline, pre-download the model; if concurrent workers " | ||
| f"share a cache, retry after the first worker completes. Detail: {repr(e)}" | ||
| ) from e |
There was a problem hiding this comment.
Follow-up to the docstring nit above — LocalEntryNotFoundError is also a ValueError (via its MRO: … -> FileNotFoundError -> OSError -> ValueError). This makes the ordering relative to L84 load-bearing: if the except ValueError branch is ever hoisted above this one (e.g. during a future refactor), LocalEntryNotFoundError will be silently re-routed and the cache-specific guidance (offline pre-download / shared-cache retry) will disappear without any test catching it.
The existing test_local_entry_not_found_error_message passes because it uses the specific class — it wouldn't catch a reorder. Consider either:
- A one-line comment above the
(LocalEntryNotFoundError, EntryNotFoundError)branch notingLocalEntryNotFoundErrorinherits fromValueErrorand must stay above that branch, or - A regression test that asserts a
LocalEntryNotFoundErrorproduces the cache-specific message verbatim (substring match on "not found in cache" rather than just "cache"), so a reorder falling through to theValueErrorbranch would flip the test red.
There was a problem hiding this comment.
Already in flight: you implemented this on refactor/dry-hf-load-error-tests in #23, which tightens assertIn("cache") to assertIn("not found in cache") in both test files. That is exactly what I was about to push. Since your commit also extracts the shared HFLoadErrorTestsMixin and deletes the two target test classes here, landing my duplicate would just force you to re-rebase, so leaving the assertion tightening to land with your refactor. (Did confirm the substring change has teeth by running a reorder mutation at hub 0.23 in a throwaway venv: the loose assertion passes in both orderings, the tightened one flips red when except ValueError is hoisted above the cache branch, so the new assertion does what we need.)
There was a problem hiding this comment.
| def test_hf_hub_http_error_includes_repr(self): | ||
| from huggingface_hub.utils import HfHubHTTPError | ||
|
|
||
| exc = make_hf_error(HfHubHTTPError, "503 Service Unavailable") | ||
| with patch("transformers.AutoImageProcessor.from_pretrained", side_effect=exc): | ||
| with self.assertRaises(RuntimeError) as ctx: | ||
| self._load_backend() | ||
| msg = str(ctx.exception) | ||
| self.assertIn("org/model", msg) | ||
| self.assertIn("HuggingFace Hub", msg) |
There was a problem hiding this comment.
Test name promises repr(e) verification that the body doesn't deliver. The assertions check "org/model" and "HuggingFace Hub" are in the message, but neither implies repr(e) is there — the handler at filter_huggingface_vision/backends/_hf_load_errors.py:82#MM could silently drop the repr(e) interpolation and this test would still pass. Also "503 Service Unavailable" in the message string is cosmetic since make_hf_error defaults status_code=404 — so this is actually exercising the 404 → non-401 path, not a 503 path.
Two small fixes land both nits at once: (a) pass status_code=503 to make_hf_error so the test label matches behavior, and (b) assert self.assertIn("503 Service Unavailable", msg) (or assertIn(repr(exc), msg)) so a regression on repr(e) actually flips the test red. Same comment applies to the identical test at tests/test_object_detection.py:206#JQ-215#MP.
There was a problem hiding this comment.
Same branch: #23 already tightens test_hf_hub_http_error_includes_repr with status_code=503 and assertIn("503 Service Unavailable"). Mutation-proved the tightened assertion has teeth by dropping repr(e) from the generic HfHubHTTPError branch in _hf_load_errors.py:82 and rerunning: the existing loose assertion stayed green (exactly the hygiene gap you flagged), the tightened one went red, restoring repr(e) flipped it back green. Leaving to land with your refactor to avoid duplicating the test change.
|
FYI — opened #23 as a small follow-up if you want to fold it in. Extracts an Also tightens two assertions from my earlier review pass on this PR: Totally optional — rebase onto your branch, cherry-pick, or ignore. |
The previous docstring in _hf_load_errors said the LocalEntryNotFoundError and EntryNotFoundError catches sit before HfHubHTTPError because they do NOT inherit from it. That is true on huggingface-hub >=1.0 (which flattened the exception hierarchy) but false at the declared floor (>=0.23 with the requests backbone), where both ARE subclasses of HfHubHTTPError and LocalEntryNotFoundError is additionally a ValueError. The current catch order happens to be correct across the supported range, but the documented reason was wrong for the floor era, so a future maintainer could easily reorder based on the stale justification. Rewrite the note to cite both eras explicitly, and point out that LocalEntryNotFoundError's additional ValueError ancestry means the `except ValueError` fallback below must also stay after the cache branch. Verified against `huggingface-hub==0.23.0` in a throwaway venv. Addresses #21 review thread 1 (stwilt). Signed-off-by: Rui Andrada <randrada@plainsight.ai>
…tests) (#23) Follow-up to fix/object-detection-error-messages. The four backend test files each hand-rolled a near-identical TestXBackendLoadErrors class (~180 lines for image_classification/object_detection, ~45 lines for grounding_dino/owlvit with only 3 of the 13 branches covered). Now that `hf_load_error_handler` is shared, the tests covering its branches should be shared too. - Add `HFLoadErrorTestsMixin` to `tests/_hf_test_utils.py` holding all 13 load-error test methods, parameterized by class attrs `backend_cls`, `processor_patch_target`, `model_patch_target`, `task_phrase`. - Replace four bespoke test classes with 5-line subclasses. As a side effect, `grounding_dino` and `owlvit` gain the 10 branches they were previously missing (ImportError allowlist, GatedRepo, HfHubHTTPError 401/non-401, Entry/LocalEntry, fallback, cause chain, model-side patch). - Strengthen two assertions that were flagged in review of #21: (a) `test_hf_hub_http_error_includes_repr` now passes `status_code=503` so label matches behavior, and asserts "503 Service Unavailable" appears in the message — catches any regression that drops `repr(e)`. (b) `test_local_entry_not_found_error_message` / `test_entry_not_found_error_message` assert the cache-specific substring "not found in cache" rather than just "cache", so a future reorder that routes LocalEntryNotFoundError through the `ValueError` branch (it inherits from both) would flip the tests red instead of silently passing. Net: +205 / −452 = −247 lines across 5 files. Load-error tests grow from 32 to 52 (4 backends × 13 branches). Full pytest run: 113 passed.
lucasmundim
left a comment
There was a problem hiding this comment.
Design is solid — the shared context manager is the right abstraction, exception catch order is correct, test coverage via the mixin is thorough, and all prior review feedback has been addressed in the code.
One hard blocker (version conflict) and a few non-blocking items inline.
| @@ -1 +1 @@ | |||
| v0.4.4 | |||
| v0.4.5 | |||
There was a problem hiding this comment.
CI blocker: duplicate v0.4.5
The check-release-log job fails with Duplicated version in changelog: v0.4.5. The main branch already shipped a v0.4.5 entry (SDK bump + secret-names fix in 1e86532). This file and RELEASE.md need to bump to v0.4.6.
| f"Check the revision (commit SHA, tag, or branch). " | ||
| f"Detail: {repr(e)}" | ||
| ) from e | ||
| except (LocalEntryNotFoundError, EntryNotFoundError) as e: |
There was a problem hiding this comment.
Two items on this handler:
LocalEntryNotFoundError/ValueErrorordering is load-bearing at hub 0.23 — stwilt's open comment is valid: at the declared floor,LocalEntryNotFoundErrorinherits fromValueError(verified via MRO). A one-line comment above this branch would protect against a future reorder, e.g.:
# Must stay above except ValueError — LocalEntryNotFoundError inherits ValueError at hub 0.23I verified against hub 1.7.1 that the hierarchy was flattened (neither class inherits from HfHubHTTPError or ValueError anymore), so the docstring's "both eras" claim is accurate.
EntryNotFoundErrormessage may mislead (echoing lucasmundim's open comment) —EntryNotFoundError(without "Local") can mean a remote file genuinely doesn't exist in the repo (e.g., missingconfig.json), not just a cache miss. The current "not found in cache" / "pre-download the model" guidance is wrong for that case. Consider broadening the message, e.g.:
"A required file for '{model_id}@{revision}' was not found — the repo may be missing expected files, or the local cache is incomplete. If running offline, pre-download the model. Detail: {repr(e)}"
Neither is blocking, but both are easy fixes while you're here.
There was a problem hiding this comment.
Added the one-line guard comment above the (LocalEntryNotFoundError, EntryNotFoundError) branch in a33fb6d. Message broadening lands here too, replied on the line 74 thread.
| self.assertIn("HuggingFace Hub", msg) | ||
| self.assertIn("503 Service Unavailable", msg) | ||
|
|
||
| def test_hf_hub_http_401_hints_at_hf_token(self): |
There was a problem hiding this comment.
Test assertion doesn't directly verify repr(e) (echoing stwilt's open comment)
The "503 Service Unavailable" assertion indirectly validates repr(e) is present, but if the repr(e) interpolation in the handler were dropped, this test would still pass as long as the model_id appears in the message. A direct assertion would make the test name truthful:
self.assertIn(repr(exc), msg)Low priority — not blocking.
There was a problem hiding this comment.
Fair, the indirect substring lets a repr(e) regression slip. Tightened to self.assertIn(repr(exc), msg) so the test name matches what it proves, in a33fb6d.
… "not compatible") Split the single `except Exception` catch block in both backend `load()` methods into five distinct, actionable branches. Applies to both `object_detection.py` (AutoModelForObjectDetection) and `image_classification.py` (AutoModelForImageClassification). Exception branches added (in catch order): 1. ImportError + "timm" → clear "pip install timm" hint. 2. ImportError (other) → re-raise unchanged so missing-dep errors surface. 3. GatedRepoError → "accept license with authenticated token" message (caught before RepositoryNotFoundError because it is a subclass of it). 4. RepositoryNotFoundError → model ID / HF_TOKEN hint. 5. RevisionNotFoundError → revision (SHA/tag/branch) hint. 6. HfHubHTTPError → generic download failure with repr(e). 7. ValueError → architecture/config mismatch message with repr(e). 8. Exception (fallback) → generic message now includes repr(e) so the real cause is always visible in logs. All raises use `from e` to preserve the original traceback. HF Hub exceptions are imported at module level via `from huggingface_hub import errors as _hf_errors` (huggingface_hub 1.10.1, where these classes live in `huggingface_hub.errors`). New test class `TestObjectDetectionBackendLoadErrors` and `TestImageClassificationBackendLoadErrors` added to the respective test files; each class covers all eight branches by mocking `transformers.AutoImageProcessor.from_pretrained`. All 75 tests pass; flake8 reports no issues. Also syncs uv.lock with pyproject.toml's existing `openfilter[all]>=0.1.27` pin (lockfile was one patch behind on main). Signed-off-by: Rui Andrada <randrada@plainsight.ai>
Reviewer nit from PR #21 — keeps both from_pretrained call sites covered if the single try/except ever gets split. Signed-off-by: Rui Andrada <randrada@plainsight.ai>
… everywhere Addresses Leandro's review on PR #21 (review PRR_kwDORDqaqs74SxH4): 1. Shared exception handler (Leandro #1): extracted `hf_load_error_handler` into `filter_huggingface_vision/backends/_hf_load_errors.py` as a `@contextmanager`. Both `object_detection.py` and `image_classification.py` now wrap their two `from_pretrained` calls in `with hf_load_error_handler(...)`, eliminating the duplicated eight-branch try/except. Future huggingface_hub error types only need to be updated in one place. 2. repr(e) consistency (Leandro #2): `GatedRepoError`, `RepositoryNotFoundError`, and `RevisionNotFoundError` now include `Detail: {repr(e)}` in their messages, consistent with the HfHubHTTPError/ValueError/fallback branches. 3. Model-side patch coverage (Leandro #3): addressed in prior commit c3ecda6 per @lucasmundim's inline nit. 4. _make_hf_error deduplication (Leandro #4): moved to `tests/_hf_test_utils.py` as a module-level `make_hf_error` function; both test files import from there. Behavior unchanged — same messages, same exception mapping, de-duplicated. Bumps VERSION v0.4.4 → v0.4.5 (patch bump; behavioral fix, no API change) and adds the corresponding RELEASE.md entry to satisfy CI check-release-log. Signed-off-by: Rui Andrada <randrada@plainsight.ai>
…tion works The `from tests._hf_test_utils import make_hf_error` form required the `tests/` directory to be a Python package, which it isn't (no __init__.py). Pytest adds each test file's directory to sys.path, so a flat `from _hf_test_utils import make_hf_error` resolves cleanly and matches how the rest of this repo structures its test-internal imports. Fixes ModuleNotFoundError on CI run 24857999573. Signed-off-by: Rui Andrada <randrada@plainsight.ai>
…lper - pyproject.toml / uv.lock: add `huggingface-hub>=0.23` lower bound. The new `_hf_load_errors` module imports `huggingface_hub.errors.GatedRepoError` and `RevisionNotFoundError`, which are only present from ~0.22+. Without a floor, an older resolved hub version would break at module import time. uv.lock is regenerated via `uv lock` (no manual edit). - tests/_hf_test_utils.py: `make_hf_error` now accepts an optional `status_code` parameter (defaults to 404). Future tests that simulate 401/403/503 conditions can set the code without monkey-patching the helper, and assertions touching the status code stay accurate. Addresses Copilot review comments on PR #21 (discussion_r3133786636 and discussion_r3133786674). Signed-off-by: Rui Andrada <randrada@plainsight.ai>
…tion, dep hints)
B1 — switch HF exception import from huggingface_hub.errors to
huggingface_hub.utils, which has been stable since hub ~0.16 and is
available at the declared floor (>=0.23). Eliminates the AttributeError
risk on older hub installs flagged by Copilot in discussion_r3133786636.
Floor in pyproject.toml unchanged.
B2 — add (LocalEntryNotFoundError, EntryNotFoundError) branch before
HfHubHTTPError. Neither class inherits from HfHubHTTPError so they were
silently falling through to the generic "Unexpected failure" catch-all.
Typical triggers: HF_HUB_OFFLINE=1 with a cold cache, or Vertex workers
racing a shared model cache.
S1 — soften ValueError message: drop "exposes the required head" assertion
since ValueError also fires for corrupt preprocessor_config.json,
unrecognized model type, bad image_size, etc. Message is now:
"Model '{model_id}@{revision}' could not be loaded for {task}. Detail: ..."
S2 — (a) GatedRepoError message now explicitly mentions HF_TOKEN env var.
(b) HfHubHTTPError branch inspects e.response.status_code — 401 emits a
dedicated hint pointing to HF_TOKEN and the gated-repo flow, since older
hub versions return a generic 401 without raising GatedRepoError.
S4 — expand ImportError handling from single-string timm check to a
_MISSING_DEP_HINTS allowlist covering timm, sentencepiece, detectron2,
and torchvision. Unknown deps are still re-raised unchanged.
Tests: new cases for LocalEntryNotFoundError, EntryNotFoundError, 401 branch,
and sentencepiece allowlist entry (both backends). All 85 tests pass.
Signed-off-by: Rui Andrada <randrada@plainsight.ai>
…vit + pytest pythonpath nit Closes the coverage gap flagged by lucasmundim in discussion_r3134262360: `grounding_dino.py` and `owlvit.py` also call `AutoProcessor.from_pretrained` / `OwlViTProcessor.from_pretrained` and were still falling into an unguarded load path, so the same opaque failures that object_detection and image_classification used to emit (gated repo without HF_TOKEN, bad revision, offline/cold-cache, missing deps) kept surfacing as "model not compatible" for these two backends. - Wrap both backends' load() with `hf_load_error_handler(model_id, revision, task)`. Task tags are "open-vocabulary detection (grounding-dino)" and "zero-shot detection (owl-vit)" so the thrown messages identify which backend failed. - Add load-error test classes (RepositoryNotFoundError, RevisionNotFoundError, ValueError) to `tests/test_grounding_dino.py` and `tests/test_owlvit.py`, mirroring the shape of TestObjectDetectionBackendLoadErrors. - Declare `[tool.pytest.ini_options] pythonpath = ["tests"]` in pyproject.toml so the `from _hf_test_utils import make_hf_error` shorthand is documented rather than relying on pytest's `prepend` import mode implicitly (discussion_r3134262364). - Drop the double blank line before v0.4.4 in RELEASE.md (discussion_r3134262367) and expand the v0.4.5 notes to reflect the coverage landing across all four backends. All backend `from_pretrained` calls in this filter now produce the same actionable errors. 47/47 non-torchvision tests pass locally (the torchvision subset runs clean in CI). Signed-off-by: Rui Andrada <randrada@plainsight.ai>
The previous docstring in _hf_load_errors said the LocalEntryNotFoundError and EntryNotFoundError catches sit before HfHubHTTPError because they do NOT inherit from it. That is true on huggingface-hub >=1.0 (which flattened the exception hierarchy) but false at the declared floor (>=0.23 with the requests backbone), where both ARE subclasses of HfHubHTTPError and LocalEntryNotFoundError is additionally a ValueError. The current catch order happens to be correct across the supported range, but the documented reason was wrong for the floor era, so a future maintainer could easily reorder based on the stale justification. Rewrite the note to cite both eras explicitly, and point out that LocalEntryNotFoundError's additional ValueError ancestry means the `except ValueError` fallback below must also stay after the cache branch. Verified against `huggingface-hub==0.23.0` in a throwaway venv. Addresses #21 review thread 1 (stwilt). Signed-off-by: Rui Andrada <randrada@plainsight.ai>
…tighten 503 test Signed-off-by: Rui Andrada <randrada@plainsight.ai>
9b4c4e6 to
a33fb6d
Compare
|
Thanks Lucas. Bumping |
|
Thanks Lucas, glad the shared handler + |
|
Thanks. Verifying against hub 1.7.1 confirmed |
|
Thanks Leandro for going point by point. The grounding_dino + owlvit extension was an obvious gap once the shared handler was in place. |
lucasmundim
left a comment
There was a problem hiding this comment.
Net-new findings only (prior threads are out of scope). Two items, both non-blocking.
| def test_hf_hub_http_error_includes_repr(self): | ||
| from huggingface_hub.utils import HfHubHTTPError | ||
|
|
||
| exc = make_hf_error(HfHubHTTPError, "503 Service Unavailable") |
There was a problem hiding this comment.
Nit (stwilt's open item, still unaddressed): this creates the error with the default status_code=404 while the message string says "503 Service Unavailable". The test passes because the non-401 HfHubHTTPError branch doesn't inspect the status code, but it's misleading — if someone later adds an assertion on the status code, it'll fail for the wrong reason.
Same issue in tests/test_object_detection.py:209.
exc = make_hf_error(HfHubHTTPError, "503 Service Unavailable", status_code=503)| @@ -1 +1 @@ | |||
| v0.4.5 | |||
| v0.4.6 No newline at end of file | |||
There was a problem hiding this comment.
Ultra-nit: missing trailing newline (\ No newline at end of file in the diff). Some Unix tools (e.g. wc -l, POSIX read) behave unexpectedly without it.
lucasmundim
left a comment
There was a problem hiding this comment.
Net-new findings only (prior threads are out of scope). Two items, both non-blocking.
| ) | ||
|
|
||
|
|
||
| class TestImageClassificationBackendLoadErrors(unittest.TestCase): |
There was a problem hiding this comment.
status_code mismatch with message text — stwilt's earlier feedback was partially applied: the assertion is now correct, but the test still constructs the error with default while the message says "503 Service Unavailable". The test works (it exercises the non-401 path), but the mismatch is confusing to future readers.
Passing would make it self-consistent:
Same applies to the identical test in .
| @@ -1 +1 @@ | |||
| v0.4.5 | |||
| v0.4.6 No newline at end of file | |||
There was a problem hiding this comment.
Nit: missing trailing newline ( in the diff). POSIX convention — most tools handle it fine, but 0 will report 0 lines and some CI linters flag it.
Summary
Replaces the single catch-all
except Exceptionin every backend'sload()method with a shared context manager mapping HuggingFace Hub failures to targeted, actionable error messages. Applies uniformly acrossobject_detection,image_classification,grounding_dino, andowlvit.Exception branches (in catch order)
ImportError→_MISSING_DEP_HINTSallowlist (timm,sentencepiece,detectron2,torchvision) with dep-specific install hints; unknown deps re-raised unchanged.GatedRepoError→ "accept license with an authenticated token (set HF_TOKEN)" (caught beforeRepositoryNotFoundError, its parent class).RepositoryNotFoundError→ model ID /HF_TOKENhint.RevisionNotFoundError→ revision (commit SHA, tag, or branch) hint.LocalEntryNotFoundError/EntryNotFoundError→ offline or shared-cache race hint (pre-download model, or retry after the first worker finishes).HfHubHTTPError→ download failure. Ifresponse.status_code == 401a dedicated gated-repo /HF_TOKENhint fires (older hub versions return 401 without raisingGatedRepoError). Other codes keep the generic download message.ValueError→ generic "could not be loaded" withrepr(e), no architecture-head assumption.Exception(fallback) → generic message always includingrepr(e).All raises use
from eto preserve the original traceback.Shared plumbing
filter_huggingface_vision/backends/_hf_load_errors.pyhosts thehf_load_error_handler(model_id, revision, task)context manager so future hub error types only need to be added in one place.huggingface_hub.utils(stable acrosshuggingface-hub>=0.23) rather than thehuggingface_hub.errorssubmodule (which only exposes these attributes from ~0.30+).tests/_hf_test_utils.pyexposesmake_hf_error(cls, message, status_code=404)— a single helper both backends' test suites share.pyproject.tomldeclares[tool.pytest.ini_options] pythonpath = ["tests"]so the barefrom _hf_test_utils import make_hf_erroris documented via pytest config rather than relying onprependimport mode implicitly.Context: a companion PR on the plainsight-api side adds
revisionanddetection_typeto GT Automation templates, which was one of the root causes of the "not compatible" errors. This PR ensures that if any of those fields are still wrong — or any other Hub failure mode fires — the operator sees an actionable error instead of a generic "not compatible".Follow-up filed separately (not in this PR): retry/backoff on transient 429 / 5xx at load time — scoped to reliability rather than error surface quality. Tracked in PLAT-962.
Test plan
TestObjectDetectionBackendLoadErrors— one test per exception branch, all passingTestImageClassificationBackendLoadErrors— parallel coverage for the classification backendTestGroundingDinoBackendLoadErrors— load-error coverage for the grounding-dino backendTestOwlVitBackendLoadErrors— load-error coverage for the owl-vit backendpytest tests/greenflake8reports no issues on all modified filesSigned-off-bytrailer present on every commit