@ondeinference / cli / commits / c7744a4

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