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 }}

+ + @@ -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);