Skip to content

Discover capability filters hide models that Nativ itself labels as capable #601

Closed

Description

@lizen

TL;DR

Discover uses two different rule sets for the same question:

  • Rule set A decides which capability pills to show on a model row (resolveCapabilities).
  • Rule set B builds the HuggingFace query when a capability filter is checked (hubTags / pipelineTag).

Rule set B is strictly narrower than rule set A, so models are filtered out server-side before matches() can accept them. The visible result is a contradiction: a Discover row shows a Reasoning pill, but checking Reasoning makes that same row disappear.

The symptom, in one model

mlx-community/MiniCPM5-1B-Claude-Opus-Fable5-V2-Thinking-OptiQ-4bit

HuggingFace tags thinking, tool-calling, function-calling, conversational
pipeline_tag text-generation
Pills Nativ shows on the row Reasoning, Tool Calling
Capability Filter Model appears?
(nothing checked) O
Tool Calling O
Reasoning X
Reasoning + Tool Calling X

The row advertises Reasoning. Filtering by Reasoning hides it. Checking both hides it too.

Three models, one matrix

All results below come from live HuggingFace API calls replicating the exact request Discover builds (HuggingFaceHub.swift:436-463).

Model HF tags / pipeline Pills shown none Text Reasoning Tools R + T
mlx-community/MiniCPM5-1B-Claude-Opus-Fable5-V2-Thinking-OptiQ-4bit thinking, tool-calling, function-calling Reasoning, Tool Calling O O X O X
mlx-community/Qwen3.8-27B-Uncensored-OptiQ-4bit reasoning, function-calling, tool-use Reasoning, Tool Calling O O O X X
mlx-community/Qwen3.8-27B-4bit pipeline = image-text-to-text Vision O X X X X

Rows 1 and 2 are exact mirrors. Both models are classified reasoning-capable and tool-capable by Nativ itself, yet each fails a different filter, and both fail when both filters are checked.

Root cause

Bug 1 — the Text filter drops vision-language models

resolveCapabilities accepts six pipelines as text-producing (HuggingFaceHub.swift:280):

let textPipelines: Set<String> = [
    "text-generation",
    "image-text-to-text",          // <- VLMs live here
    "image-to-text",
    "visual-question-answering",
    "audio-text-to-text",
    "video-text-to-text",
]
if textPipelines.contains(pipeline) { result.insert(.text) }

The query builder asks for one (HuggingFaceHub.swift:116):

case .text:
    return "text-generation"
Filter off:  /api/models?filter=safetensors&search=Qwen3.8-27B-4bit
Text on:     /api/models?filter=safetensors&search=Qwen3.8-27B-4bit&pipeline_tag=text-generation

Qwen3.8-27B-4bit is image-text-to-text, so it never comes back. Same for Qwen3.5-9B-MLX-4bit, Muse-Glimmer-30B-OptiQ-4bit and Tiel-Coder-35B-A3B-MLX-oQ4e.

Passing several pipeline_tag values as a workaround does not work: the Hub API returns 0 results for duplicate pipeline_tag parameters (verified).

Bug 2 — Reasoning and Tools match on the wrong tag

The classifier uses substring matching (HuggingFaceHub.swift:341, 352):

if descriptors.contains("reasoning") || descriptors.contains("thinking") {
    result.insert(.reasoning)
}
if descriptors.contains("tool") || descriptors.contains("function-call") {
    result.insert(.tools)
}

The query builder sends exact tag names (HuggingFaceHub.swift:91):

if capabilities.contains(.reasoning) { tags.append("reasoning") }
if capabilities.contains(.tools)     { tags.append("tool-calling") }

HuggingFace matches tags exactly, with no synonym handling:

Tag on the model Classifier says Hub filter=reasoning Hub filter=tool-calling
reasoning Reasoning match -
thinking Reasoning no match -
tool-calling Tool Calling - match
function-calling Tool Calling - no match
tool-use Tool Calling - no match

Why checking both filters is worse than either alone

Tags go into a single comma-joined filter parameter, which HuggingFace treats as AND:

filter=safetensors,reasoning,tool-calling
  • MiniCPM5 has tool-calling but not reasoning -> AND fails -> hidden
  • Qwen3.8-Uncensored has reasoning but not tool-calling -> AND fails -> hidden

The more filters a user checks, the more capable models disappear.

Side-by-side summary

Capability What Nativ accepts (pills) What Nativ asks HuggingFace for
Text 6 pipelines + conversational + causal-lm pipeline_tag=text-generation only
Reasoning any tag containing reasoning or thinking tag reasoning, exact
Tool Calling any tag containing tool or function-call tag tool-calling, exact

Notes on fixing

Removing the server-side narrowing altogether is not a safe fix. Measured against the top 50 downloads with filter=safetensors,mlx, the share of results that pass the local predicate is:

Filter passes local predicate verdict
Text 35/50 server-side narrowing is not needed
Vision 16/50 borderline
Reasoning 1/50 narrowing is load-bearing
Tools 1/50 narrowing is load-bearing
Embeddings 0/50 narrowing is load-bearing
Reranking 0/50 narrowing is load-bearing

With pageSize = 24, API limit = 50 and maximumFillFetches = 8, dropping the tag filters would mean scanning up to 400 models to fill one page for Reasoning/Tools/Embeddings/Reranking, which is what 01e7d27 set out to avoid. So Text and the tag filters need different treatment.

The repo already solves this exact shape of problem for drafters: drafterCandidateTags lists tag aliases and hubTagSets fans out into parallel requests that pages(at:) merges and de-duplicates. The same pattern would cover reasoning/thinking and tool-calling/function-calling/tool-use.

Separately, LocalModelCapability.hubQueryItem (ModelsView.swift:3254) duplicates the same narrow mapping and is currently unreferenced. Pre-existing dead code, flagged only so a future change doesn't reintroduce the bug through it.

Environment

  • Nativ 0.3.8 (build 202609141816)
  • bundled runtime: mlx-vlm 0.7.1, mlx-audio 0.5.4
  • macOS 27.0, Apple M5 Pro, 48 GB

Activity

  1. linear-code commented on Sep 21, 2026

    @linear-code
  2. lizen commented on Sep 24, 2026

    @lizen
    ContributorAuthor

    Still reproduces on the latest main

    I re-checked this on main at b8a0204 (2026-09-24) against the live Hub API, and the results match the original report:

    Model Hub tags / pipeline None Text Reasoning Tools R + T
    mlx-community/MiniCPM5-1B-Claude-Opus-Fable5-V2-Thinking-OptiQ-4bit thinking, tool-calling, function-calling O O X O X
    mlx-community/Qwen3.8-27B-Uncensored-OptiQ-4bit reasoning, function-calling, tool-use O O O X X
    mlx-community/Qwen3.8-27B-4bit pipeline image-text-to-text O X X X X

    Steps to reproduce in the app

    1. Open Models → Discover and search for Qwen3.8-27B-Uncensored-OptiQ-4bit. The row shows Reasoning and Tool Calling.
    2. Open Capability and check Tool Calling under Features. The row disappears.
    3. Search for Qwen3.8-27B-4bit with no filter. The row shows Vision, and the model is also classified as Text. Check Text under Tasks, and the row disappears.

    Expected: a model whose row shows a capability stays visible when that capability filter is checked.

    Corrections to the original report

    • The server-side narrowing was introduced in bf58d94 (pipeline filters, "Fix hub discover pagination: filter at the API + paginate over visible rows") and 05cf10a (tag filters, Improve model discovery, downloads, and provider branding #262), not 01e7d27.
    • Line references have shifted slightly. It's safer to use symbol names: HuggingFaceCapabilityFilter.pipelineTag(for:) (the .text case is now at HuggingFaceHub.swift:124), and the reasoning and tools checks in resolveCapabilities are now at :342 and :353.
    • One page can scan up to 450 models, not 400: the first request plus maximumFillFetches = 8 more, each with limit = 50.
    • In the top-50 table, Reasoning and Tools are now 0/50 each (they were 1/50). The rankings moved, but the conclusion is the same.

    How the two stages disagree

    Discover filters in two stages, and a row shows only if it passes both:

    1. Hub query: exact tag names, comma-joined as AND, and at most one pipeline_tag. filter=function returns 0 results even though filter=function-calling returns 354. The search parameter matches model IDs only, not tags.
    2. Local matches(): it uses resolveCapabilities, which joins the pipeline, the library name, and every tag into one string, including dataset:â€Ķ and base_model:â€Ķ tags. It then runs substring checks OR'ed together.

    The capability pills come from stage 2 alone. So filtering is correct only if every model stage 2 accepts can also be returned by stage 1. Right now stage 1 is narrower, because each capability is mapped to a single canonical value.

    What alias fan-out would and wouldn't fix

    I sampled 2,578 MLX safetensors models (the top 1,000 by downloads, trending, and last modified, deduplicated). Of the models resolveCapabilities classifies, this many can be found by exact-tag Hub queries:

    Capability Today (one tag) With aliases Never found
    Reasoning (reasoning, thinking) 80% 82% ~16%
    Tools (tool-calling, function-calling, tool-use) 34% 84% ~16%

    The aliases fix most of the problem, especially for Tools. The remaining ~16% get their pill only from substrings inside tags that no exact-tag query can match. For example:

    • mlx-community/Kimi-K2-Thinking: base_model:moonshotai/kimi-k2-thinking
    • mlx-community/Hermes-4-14B-4bit: tool use (with a space)
    • mlx-community/dolphin3.0-llama3.2-3B-4Bit: dataset:nousresearch/hermes-function-calling-v1

    These rows would still show a pill but disappear under that filter, unless the classifier is changed too.

    Question on the approach

    Before I open a PR, which direction would you prefer for Reasoning and Tools?

    1. One alias list for both stages. Classify these capabilities by exact alias tags and use the same list for the Hub fan-out, as Drafter already does. A pill then always survives its filter. However, models like Kimi-K2-Thinking would lose the pill.
    2. Widen the query only. Keep the substring classifier and add the alias fan-out. Pills stay as they are, but about 16% of pills still disappear under their filter.

    For Text, dropping the server-side pipeline_tag narrowing looks safe, since 35 of the top 50 models pass the local check. Combined filters would use the product of the alias lists, so Reasoning + Tools means 2 × 3 = 6 parallel requests.

  3. Yavaren commented on Sep 24, 2026

    @Yavaren
    Collaborator

    @lizen thank you for your comment! I will work on this issue

  4. self-assigned this
    on Sep 24, 2026
  5. Yavaren commented on Sep 24, 2026

    @Yavaren
    Collaborator

    @lizen the fix is out on a new branch, you can build it from there and let me know if this fixes your issue!

  6. lizen commented on Sep 25, 2026

    @lizen
    ContributorAuthor

    @Yavaren Thank you for the quick fix, and sorry I didn't get to test the branch before it was merged!

    I fetched the latest main (928154f), then built and ran it to check #617 in Discover:

    • The models from the report now show up under every filter whose badge they have. That includes Reasoning + Tool Calling together for MiniCPM5 and Qwen3.8-27B-Uncensored, and Text for Qwen3.8-27B-4bit.
    • All 20 HuggingFaceCapabilityFilterTests pass locally.

    For anyone else checking: v0.3.10 was released before #617 was merged, so the fix will ship in the next release.

    Sharing one alias list between the badges and the Hub queries fixes the mismatch at its root. Thanks again!

  7. Yavaren commented on Sep 25, 2026

    @Yavaren
    Collaborator

    @lizen you are always welcome! Let us know if you have any other suggestions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions