@ondeinference / onde / commits / 2299d31

Truncate log strings on UTF-8 char boundaries

Slicing a str at a byte index panics when the index falls inside a multi-byte character, so truncate_for_log now walks the limit back to the nearest char boundary before slicing. Co-Authored-By: siGit Code v1.5.12-acp <noreply@sigit.si>

Seto Elkahfi committed Oct 1, 2026 at 19:58 UTC 2299d31e8982f9a7045c196355e957b9ec9485f6
1 file changed +32 -1
src/inference/engine.rs
+32 -1
@@ -2096,7 +2096,11 @@ fn chat_role_to_mistral(role: &ChatRole) -> TextMessageRole {
2096 /// Truncate a string for log output, appending `"..."` if truncated.
2097 fn truncate_for_log(s: &str, max_len: usize) -> String {
2098 if s.len() > max_len {
2099 - format!("{}...", &s[..max_len])
2099 + let mut end = max_len;
2100 + while !s.is_char_boundary(end) {
2101 + end -= 1;
2102 + }
2103 + format!("{}...", &s[..end])
2104 } else {
2105 s.to_string()
2106 }
@@ -2692,6 +2696,33 @@ mod tests {
2696 assert_eq!(result.len(), 53); // 50 chars + "..."
2697 }
2698
2699 + #[test]
2700 + fn truncate_for_log_unicode_boundaries() {
2701 + for character in ['é', '—', '界', '🦀'] {
2702 + let input = format!("{}{}tail", "a".repeat(99), character);
2703 + for limit in 99..99 + character.len_utf8() {
2704 + assert_eq!(
2705 + truncate_for_log(&input, limit),
2706 + format!("{}...", "a".repeat(99))
2707 + );
2708 + }
2709 + let boundary = 99 + character.len_utf8();
2710 + assert_eq!(
2711 + truncate_for_log(&input, boundary),
2712 + format!("{}{}...", "a".repeat(99), character)
2713 + );
2714 + }
2715 + }
2716 +
2717 + #[test]
2718 + fn truncate_for_log_empty_and_zero_limit() {
2719 + assert_eq!(truncate_for_log("", 0), "");
2720 + assert_eq!(truncate_for_log("—", 0), "...");
2721 + assert_eq!(truncate_for_log("—", 1), "...");
2722 + assert_eq!(truncate_for_log("—", 3), "—");
2723 + assert_eq!(truncate_for_log("hello", 5), "hello");
2724 + }
2725 +
2726 #[test]
2727 fn gguf_model_config_qwen25_1_5b() {
2728 let cfg = GgufModelConfig::qwen25_1_5b();