Fix GGUF detail upload hint and widen size detection
The GGUF detail status line advertised a "u" key that was never wired up: upload has always been Enter, and only for local fine-tune outputs. The status now matches the footer, pointing at Enter for uploadable GGUFs and at the chat test for GGUFs that came from HF. Also adds the missing parameter classes (0.6B, 1.7B, 8B, 30B, and more of the Qwen2.5/Qwen3 lineups) and teaches contains_size_token to reject matches preceded by a dot, so 7B can no longer match inside 1.7B. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
paydii committed
Jul 15, 2026 at 08:06 UTC
c7744a481b04071d151e22d457cc1fdf1b8284e8
1 file changed
+42
-6
src/app.rs
+42
-6
@@ -1408,7 +1408,13 @@ fn handle_key_model_detail(
1408
app.upload_progress = None;
1409
app.upload_running = false;
1410
app.screen = Screen::GgufDetail;
1411
- app.status = Status::neutral("GGUF model details. u · upload to HuggingFace");
1411
+ // Upload only applies to local fine-tune outputs; GGUFs that
1412
+ // came from HF (App Group / HF cache) have nowhere to go.
1413
+ app.status = if entry.is_uploadable() {
1414
+ Status::neutral("GGUF model details. Enter · upload to HuggingFace")
1415
+ } else {
1416
+ Status::neutral("GGUF model details. c · test in chat")
1417
+ };
1418
}
1419
}
1420
// Enter or 'm' — merge the selected LoRA adapter (skip fine-tuning)
@@ -1820,18 +1826,20 @@ fn trigger_publish_model(app: &mut App, tx: mpsc::UnboundedSender<AuthEvent>) {
1826
/// on both sides by a non-alphanumeric character (or the string edge). "3B"
1827
/// is a plain substring of "13B"/"43B" (digit before) and of "3BETA"/"3B2"
1828
/// (alphanumeric after), so a naive `.contains()` would misdetect those.
1829
+/// A preceding "." also disqualifies a match: "7B" inside "1.7B" is the tail
1830
+/// of a decimal size, not a 7B model.
1831
fn contains_size_token(haystack: &str, size: &str) -> bool {
1832
let mut start = 0;
1833
while let Some(idx) = haystack[start..].find(size) {
1834
let abs_idx = start + idx;
1835
let end_idx = abs_idx + size.len();
1828
- let preceded_by_digit = haystack.as_bytes()[..abs_idx]
1836
+ let preceded_by_digit_or_dot = haystack.as_bytes()[..abs_idx]
1837
.last()
1830
- .is_some_and(u8::is_ascii_digit);
1838
+ .is_some_and(|b| b.is_ascii_digit() || *b == b'.');
1839
let followed_by_alnum = haystack.as_bytes()[end_idx..]
1840
.first()
1841
.is_some_and(u8::is_ascii_alphanumeric);
1834
- if !preceded_by_digit && !followed_by_alnum {
1842
+ if !preceded_by_digit_or_dot && !followed_by_alnum {
1843
return true;
1844
}
1845
start = abs_idx + 1;
@@ -1872,7 +1880,14 @@ fn derive_base_model_meta(base_model_id: Option<&str>) -> (String, String, Strin
1880
}
1881
.to_string();
1882
1875
- const SIZES: &[&str] = &["0.5B", "1.5B", "3B", "4B", "7B", "14B", "32B", "72B"];
1883
+ // Covers the Qwen2.5 (0.5B–72B) and Qwen3 (0.6B–235B) lineups plus other
1884
+ // common parameter counts. `contains_size_token` enforces token boundaries
1885
+ // (no digit or "." before, no alphanumeric after), so no entry here can
1886
+ // match inside another — ordering is not load-bearing.
1887
+ const SIZES: &[&str] = &[
1888
+ "0.5B", "0.6B", "1.5B", "1.7B", "1.8B", "2B", "3B", "4B", "7B", "8B", "9B", "13B", "14B",
1889
+ "30B", "32B", "70B", "72B", "235B",
1890
+ ];
1891
let haystack_upper = haystack.to_uppercase();
1892
let parameter_class = SIZES
1893
.iter()
@@ -3119,8 +3134,29 @@ mod tests {
3134
3135
#[test]
3136
fn test_derive_base_model_meta_larger_size_not_shadowed_by_shorter_substring() {
3122
- // "13B" contains the substring "3B" -- this must not resolve to "3B".
3137
+ // "13B" contains the substring "3B" -- this must resolve to "13B",
3138
+ // not "3B".
3139
let (_, _, parameter_class) = derive_base_model_meta(Some("someone/Mystery-13B-GGUF"));
3140
+ assert_eq!(parameter_class, "13B");
3141
+ }
3142
+
3143
+ #[test]
3144
+ fn test_derive_base_model_meta_qwen3_small_sizes() {
3145
+ let (_, family, parameter_class) = derive_base_model_meta(Some("Qwen/Qwen3-0.6B"));
3146
+ assert_eq!(family, "qwen3");
3147
+ assert_eq!(parameter_class, "0.6B");
3148
+ let (_, _, parameter_class) = derive_base_model_meta(Some("Qwen/Qwen3-8B"));
3149
+ assert_eq!(parameter_class, "8B");
3150
+ }
3151
+
3152
+ #[test]
3153
+ fn test_derive_base_model_meta_decimal_size_not_matched_as_integer_tail() {
3154
+ // "1.7B" contains the substring "7B" -- this must resolve to "1.7B",
3155
+ // not "7B".
3156
+ let (_, _, parameter_class) = derive_base_model_meta(Some("Qwen/Qwen3-1.7B"));
3157
+ assert_eq!(parameter_class, "1.7B");
3158
+ // "2.7B" isn't a known size; its "7B" tail must not match either.
3159
+ let (_, _, parameter_class) = derive_base_model_meta(Some("someone/Mystery-2.7B-GGUF"));
3160
assert_eq!(parameter_class, "unknown");
3161
}
3162