Repository navigation
Discover capability filters hide models that Nativ itself labels as capable #601
Description
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-callingbut notreasoning-> AND fails -> hidden - Qwen3.8-Uncensored has
reasoningbut nottool-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
linear-code commentedon Sep 21, 2026
NEY-215
lizen commentedon Sep 24, 2026
Still reproduces on the latest
mainI re-checked this on
mainatb8a0204(2026-09-24) against the live Hub API, and the results match the original report:mlx-community/MiniCPM5-1B-Claude-Opus-Fable5-V2-Thinking-OptiQ-4bitthinking,tool-calling,function-callingmlx-community/Qwen3.8-27B-Uncensored-OptiQ-4bitreasoning,function-calling,tool-usemlx-community/Qwen3.8-27B-4bitimage-text-to-textSteps to reproduce in the app
Qwen3.8-27B-Uncensored-OptiQ-4bit. The row shows Reasoning and Tool Calling.Qwen3.8-27B-4bitwith 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
bf58d94(pipeline filters, "Fix hub discover pagination: filter at the API + paginate over visible rows") and05cf10a(tag filters, Improve model discovery, downloads, and provider branding #262), not01e7d27.HuggingFaceCapabilityFilter.pipelineTag(for:)(the.textcase is now atHuggingFaceHub.swift:124), and the reasoning and tools checks inresolveCapabilitiesare now at:342and:353.maximumFillFetches = 8more, each withlimit = 50.How the two stages disagree
Discover filters in two stages, and a row shows only if it passes both:
pipeline_tag.filter=functionreturns 0 results even thoughfilter=function-callingreturns 354. Thesearchparameter matches model IDs only, not tags.matches(): it usesresolveCapabilities, which joins the pipeline, the library name, and every tag into one string, includingdataset:âĶandbase_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
resolveCapabilitiesclassifies, this many can be found by exact-tag Hub queries:reasoning,thinking)tool-calling,function-calling,tool-use)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-thinkingmlx-community/Hermes-4-14B-4bit:tool use(with a space)mlx-community/dolphin3.0-llama3.2-3B-4Bit:dataset:nousresearch/hermes-function-calling-v1These 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?
For Text, dropping the server-side
pipeline_tagnarrowing 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.Yavaren commentedon Sep 24, 2026
@lizen thank you for your comment! I will work on this issue
Yavaren commentedon Sep 24, 2026
@lizen the fix is out on a new branch, you can build it from there and let me know if this fixes your issue!
lizen commentedon Sep 25, 2026
@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:HuggingFaceCapabilityFilterTestspass 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!
Yavaren commentedon Sep 25, 2026
@lizen you are always welcome! Let us know if you have any other suggestions