From ce12b29c94daa050cd1fad2e16555c0e28f7fea9 Mon Sep 17 00:00:00 2001
From: pyr0ball
Date: Thu, 9 Apr 2026 09:48:55 -0700
Subject: [PATCH] feat: model compatibility warning on HF lookup
- GET /api/models/lookup now returns compatible: bool and warning: str|null
- compatible=false + warning when pipeline_tag is absent (no task tag on HF)
or present but not in the supported adapter map
- Warning message names the unsupported pipeline_tag and lists supported types
- ModelsView: yellow compat-warning banner below preview description;
Add button relabels to "Add anyway" with muted styling when incompatible
- test_models: accept 405 for path-traversal DELETE tests (StaticFiles mount
returns 405 for non-GET methods when web/dist exists)
---
app/models.py | 24 ++++++++++++++++++++++--
tests/test_models.py | 9 ++++++---
web/src/views/ModelsView.vue | 34 +++++++++++++++++++++++++++++++++-
3 files changed, 61 insertions(+), 6 deletions(-)
diff --git a/app/models.py b/app/models.py
index 0ac40a8..4a6ffcd 100644
--- a/app/models.py
+++ b/app/models.py
@@ -200,8 +200,26 @@ def lookup_model(repo_id: str) -> dict:
data = resp.json()
pipeline_tag = data.get("pipeline_tag")
adapter_recommendation = _TAG_TO_ADAPTER.get(pipeline_tag) if pipeline_tag else None
- if pipeline_tag and adapter_recommendation is None:
- logger.warning("Unknown pipeline_tag %r for %s — no adapter recommendation", pipeline_tag, repo_id)
+
+ # Determine compatibility and surface a human-readable warning
+ _supported = ", ".join(sorted(_TAG_TO_ADAPTER.keys()))
+ if adapter_recommendation is not None:
+ compatible = True
+ warning: str | None = None
+ elif pipeline_tag is None:
+ compatible = False
+ warning = (
+ "This model has no task tag on HuggingFace — adapter type is unknown. "
+ "It may not work with Avocet's email classification pipeline."
+ )
+ logger.warning("No pipeline_tag for %s — no adapter recommendation", repo_id)
+ else:
+ compatible = False
+ warning = (
+ f"\"{pipeline_tag}\" models are not supported by Avocet's email classification adapters. "
+ f"Supported task types: {_supported}."
+ )
+ logger.warning("Unsupported pipeline_tag %r for %s", pipeline_tag, repo_id)
# Estimate model size from siblings list
siblings = data.get("siblings") or []
@@ -216,6 +234,8 @@ def lookup_model(repo_id: str) -> dict:
"repo_id": repo_id,
"pipeline_tag": pipeline_tag,
"adapter_recommendation": adapter_recommendation,
+ "compatible": compatible,
+ "warning": warning,
"model_size_bytes": model_size_bytes,
"description": description,
"tags": data.get("tags") or [],
diff --git a/tests/test_models.py b/tests/test_models.py
index 0e31f04..d73e845 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -371,15 +371,18 @@ def test_delete_installed_not_found_returns_404(client):
def test_delete_installed_path_traversal_blocked(client):
- """DELETE /installed/../../etc must be blocked (400 or 422)."""
+ """DELETE /installed/../../etc must be blocked.
+ Path traversal normalises to a different URL (/api/etc); if web/dist exists
+ the StaticFiles mount intercepts it and returns 405 (GET/HEAD only).
+ """
r = client.delete("/api/models/installed/../../etc")
- assert r.status_code in (400, 404, 422)
+ assert r.status_code in (400, 404, 405, 422)
def test_delete_installed_dotdot_name_blocked(client):
"""A name containing '..' in any form must be rejected."""
r = client.delete("/api/models/installed/..%2F..%2Fetc")
- assert r.status_code in (400, 404, 422)
+ assert r.status_code in (400, 404, 405, 422)
def test_delete_installed_name_with_slash_blocked(client):
diff --git a/web/src/views/ModelsView.vue b/web/src/views/ModelsView.vue
index 10df382..3d7871b 100644
--- a/web/src/views/ModelsView.vue
+++ b/web/src/views/ModelsView.vue
@@ -54,12 +54,18 @@
{{ lookupResult.description }}
+
+ ⚠️
+ {{ lookupResult.warning }}
+
+
@@ -188,6 +194,8 @@ interface LookupResult {
repo_id: string
pipeline_tag: string | null
adapter_recommendation: string | null
+ compatible: boolean
+ warning: string | null
size: number | null
description: string | null
already_installed: boolean
@@ -565,10 +573,34 @@ onUnmounted(() => {
overflow: hidden;
}
+.compat-warning {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.5rem;
+ padding: 0.6rem 0.75rem;
+ border-radius: var(--radius-sm, 0.25rem);
+ background: color-mix(in srgb, var(--color-warning, #f59e0b) 12%, transparent);
+ border: 1px solid color-mix(in srgb, var(--color-warning, #f59e0b) 40%, transparent);
+ font-size: 0.82rem;
+ color: var(--color-text, #1a2338);
+ line-height: 1.45;
+}
+
+.compat-warning-icon {
+ flex-shrink: 0;
+ line-height: 1.45;
+}
+
.btn-add-queue {
align-self: flex-start;
}
+.btn-add-queue-warn {
+ background: var(--color-surface-raised, #e4ebf5);
+ color: var(--color-text-secondary, #6b7a99);
+ border: 1px solid var(--color-border, #d0d7e8);
+}
+
/* ── Model cards (queue + downloads) ── */
.model-card {
border: 1px solid var(--color-border, #a8b8d0);