fix: harvest safe bug fixes from PR #2880

Harvests 7 safe fixes from PR #2880 by @HUQIANTAO: tool-name hex-digit
guard, token-usage u32 clamp, read-file line usize::try_from, grep
context-lines cap, UTF-8 PDF trim, run_skill dedup, and
Volcengine/SiliconflowCn reasoning_content support. Excludes the
DeepSeek stream-stop change and the unwired prompt_persist module
(deferred for separate review).

Co-Authored-By: HUQIANTAO <58421104+HUQIANTAO@users.noreply.github.com>
This commit is contained in:
Hunter B
2026-06-07 10:32:54 -07:00
parent ab65495b0e
commit e2b7d5e197
5 changed files with 29 additions and 10 deletions
+6 -3
View File
@@ -61,7 +61,10 @@ pub(super) fn from_api_tool_name(name: &str) -> String {
break;
}
}
if let Ok(code) = u32::from_str_radix(&hex, 16)
// Only decode if we got exactly 6 hex digits (matching encoder output).
// Fewer digits means a truncated/malformed sequence — pass through as-is.
if hex.len() == 6
&& let Ok(code) = u32::from_str_radix(&hex, 16)
&& let Some(decoded) = std::char::from_u32(code)
{
if let Some('-') = iter.peek().copied() {
@@ -1395,8 +1398,8 @@ pub(super) fn parse_usage(usage: Option<&Value>) -> Usage {
});
Usage {
input_tokens: input_tokens as u32,
output_tokens: output_tokens as u32,
input_tokens: input_tokens.min(u64::from(u32::MAX)) as u32,
output_tokens: output_tokens.min(u64::from(u32::MAX)) as u32,
prompt_cache_hit_tokens,
prompt_cache_miss_tokens,
reasoning_tokens,
+2
View File
@@ -1990,6 +1990,8 @@ fn provider_accepts_reasoning_content(provider: ApiProvider) -> bool {
| ApiProvider::Novita
| ApiProvider::Fireworks
| ApiProvider::Siliconflow
| ApiProvider::SiliconflowCn
| ApiProvider::Volcengine
| ApiProvider::Arcee
| ApiProvider::Sglang
)
+2 -2
View File
@@ -682,8 +682,8 @@ pub fn execute(cmd: &str, app: &mut App) -> CommandResult {
_ => {
// Third source: skills (lowest precedence after native and user-config).
// Try to run a skill whose name matches the command.
if skills::run_skill_by_name(app, command, arg).is_some() {
return skills::run_skill_by_name(app, command, arg).unwrap();
if let Some(result) = skills::run_skill_by_name(app, command, arg) {
return result;
}
let suggestions = suggest_command_names(command, 3);
if suggestions.is_empty() {
+16 -3
View File
@@ -114,7 +114,11 @@ impl ToolSpec for ReadFileTool {
"start_line must be 1-based and greater than 0".to_string(),
));
}
Some(v) => v as usize,
Some(v) => usize::try_from(v).map_err(|_| {
ToolError::invalid_input(
"start_line exceeds platform addressable range".to_string(),
)
})?,
None => 1,
};
@@ -124,7 +128,14 @@ impl ToolSpec for ReadFileTool {
"max_lines must be greater than 0".to_string(),
));
}
Some(v) => std::cmp::min(v as usize, HARD_MAX_READ_LINES),
Some(v) => {
let converted = usize::try_from(v).map_err(|_| {
ToolError::invalid_input(
"max_lines exceeds platform addressable range".to_string(),
)
})?;
std::cmp::min(converted, HARD_MAX_READ_LINES)
}
None => DEFAULT_READ_LINES,
};
@@ -292,7 +303,9 @@ fn clean_pdf_text(raw: &str) -> String {
if any_content {
let start = out.find(|c: char| c != '\n').unwrap_or(0);
// Walk back from end to find the last non-newline character.
let end = out.rfind(|c: char| c != '\n').map_or(out.len(), |i| i + 1);
let end = out.rfind(|c: char| c != '\n').map_or(out.len(), |i| {
i + out[i..].chars().next().map_or(1, |c| c.len_utf8())
});
out[start..end].to_string()
} else {
String::new()
+3 -2
View File
@@ -100,8 +100,9 @@ impl ToolSpec for GrepFilesTool {
async fn execute(&self, input: Value, context: &ToolContext) -> Result<ToolResult, ToolError> {
let pattern_str = required_str(&input, "pattern")?;
let path_str = optional_str(&input, "path").unwrap_or(".");
let context_lines =
usize::try_from(optional_u64(&input, "context_lines", 2)).unwrap_or(usize::MAX);
let context_lines = usize::try_from(optional_u64(&input, "context_lines", 2))
.unwrap_or(usize::MAX)
.min(1000);
let case_insensitive = optional_bool(&input, "case_insensitive", false);
let max_results = usize::try_from(optional_u64(&input, "max_results", MAX_RESULTS as u64))
.unwrap_or(MAX_RESULTS);