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)
This commit is contained in:
parent
49ec85706c
commit
ce12b29c94
3 changed files with 61 additions and 6 deletions
|
|
@ -200,8 +200,26 @@ def lookup_model(repo_id: str) -> dict:
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
pipeline_tag = data.get("pipeline_tag")
|
pipeline_tag = data.get("pipeline_tag")
|
||||||
adapter_recommendation = _TAG_TO_ADAPTER.get(pipeline_tag) if pipeline_tag else None
|
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
|
# Estimate model size from siblings list
|
||||||
siblings = data.get("siblings") or []
|
siblings = data.get("siblings") or []
|
||||||
|
|
@ -216,6 +234,8 @@ def lookup_model(repo_id: str) -> dict:
|
||||||
"repo_id": repo_id,
|
"repo_id": repo_id,
|
||||||
"pipeline_tag": pipeline_tag,
|
"pipeline_tag": pipeline_tag,
|
||||||
"adapter_recommendation": adapter_recommendation,
|
"adapter_recommendation": adapter_recommendation,
|
||||||
|
"compatible": compatible,
|
||||||
|
"warning": warning,
|
||||||
"model_size_bytes": model_size_bytes,
|
"model_size_bytes": model_size_bytes,
|
||||||
"description": description,
|
"description": description,
|
||||||
"tags": data.get("tags") or [],
|
"tags": data.get("tags") or [],
|
||||||
|
|
|
||||||
|
|
@ -371,15 +371,18 @@ def test_delete_installed_not_found_returns_404(client):
|
||||||
|
|
||||||
|
|
||||||
def test_delete_installed_path_traversal_blocked(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")
|
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):
|
def test_delete_installed_dotdot_name_blocked(client):
|
||||||
"""A name containing '..' in any form must be rejected."""
|
"""A name containing '..' in any form must be rejected."""
|
||||||
r = client.delete("/api/models/installed/..%2F..%2Fetc")
|
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):
|
def test_delete_installed_name_with_slash_blocked(client):
|
||||||
|
|
|
||||||
|
|
@ -54,12 +54,18 @@
|
||||||
{{ lookupResult.description }}
|
{{ lookupResult.description }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div v-if="lookupResult.warning" class="compat-warning" role="alert">
|
||||||
|
<span class="compat-warning-icon">⚠️</span>
|
||||||
|
<span>{{ lookupResult.warning }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="btn-primary btn-add-queue"
|
class="btn-primary btn-add-queue"
|
||||||
|
:class="{ 'btn-add-queue-warn': !lookupResult.compatible }"
|
||||||
:disabled="lookupResult.already_installed || lookupResult.already_queued || addingToQueue"
|
:disabled="lookupResult.already_installed || lookupResult.already_queued || addingToQueue"
|
||||||
@click="addToQueue"
|
@click="addToQueue"
|
||||||
>
|
>
|
||||||
{{ addingToQueue ? 'Adding…' : 'Add to queue' }}
|
{{ addingToQueue ? 'Adding…' : lookupResult.compatible ? 'Add to queue' : 'Add anyway' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -188,6 +194,8 @@ interface LookupResult {
|
||||||
repo_id: string
|
repo_id: string
|
||||||
pipeline_tag: string | null
|
pipeline_tag: string | null
|
||||||
adapter_recommendation: string | null
|
adapter_recommendation: string | null
|
||||||
|
compatible: boolean
|
||||||
|
warning: string | null
|
||||||
size: number | null
|
size: number | null
|
||||||
description: string | null
|
description: string | null
|
||||||
already_installed: boolean
|
already_installed: boolean
|
||||||
|
|
@ -565,10 +573,34 @@ onUnmounted(() => {
|
||||||
overflow: hidden;
|
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 {
|
.btn-add-queue {
|
||||||
align-self: flex-start;
|
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 cards (queue + downloads) ── */
|
||||||
.model-card {
|
.model-card {
|
||||||
border: 1px solid var(--color-border, #a8b8d0);
|
border: 1px solid var(--color-border, #a8b8d0);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue