Merge pull request #623 from H-Chris233/fix/crash-guards-llm-endpoint-url-asr-model-validation
fix: crash guards for LLM endpoint URL + ASR model loading
This commit is contained in:
3 files changed
+100
-10
No files matched your search
@@ -47,6 +47,43 @@ pub async fn run_test(model_id: ModelId) -> Result<TestResult> {
|
||||
anyhow::bail!("模型目录不存在:{}(请先下载)", dir.display());
|
||||
}
|
||||
|
||||
// ── 模型文件完整性检查 ────────────────────────────────────────────
|
||||
// 在调 C FFI 之前先检查关键文件是否齐全、尺寸是否合理,避免因下载不完整
|
||||
// 或文件损坏导致 C 端 qwen_load / qwen_transcribe_audio segfault 杀死进程。
|
||||
// 需要的文件清单见 QwenAsrEngine::load() 注释。
|
||||
let required_files = ["config.json", "vocab.json", "merges.txt"];
|
||||
for fname in &required_files {
|
||||
let path = dir.join(fname);
|
||||
if !path.exists() {
|
||||
anyhow::bail!("模型文件缺失:{fname},请重新下载(预期路径:{})", path.display());
|
||||
}
|
||||
let meta = std::fs::metadata(&path)
|
||||
.map_err(|e| anyhow::anyhow!("读取 {fname} 元数据失败:{e}"))?;
|
||||
if meta.len() == 0 {
|
||||
anyhow::bail!("模型文件为空:{fname},请重新下载");
|
||||
}
|
||||
}
|
||||
// safetensors 可能是单文件 model.safetensors 或分片 model-00001-of-NNNN.safetensors
|
||||
let has_safetensors: Vec<_> = std::fs::read_dir(&dir)
|
||||
.map_err(|e| anyhow::anyhow!("读取模型目录失败:{e}"))?
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|e| e.path().extension().is_some_and(|ext| ext == "safetensors"))
|
||||
.collect();
|
||||
if has_safetensors.is_empty() {
|
||||
anyhow::bail!("模型目录中没有 .safetensors 权重文件,请重新下载");
|
||||
}
|
||||
for entry in &has_safetensors {
|
||||
let meta = std::fs::metadata(entry.path())
|
||||
.map_err(|e| anyhow::anyhow!("读取 {} 元数据失败:{e}", entry.path().display()))?;
|
||||
if meta.len() < 1024 {
|
||||
anyhow::bail!(
|
||||
"权重文件太小({} bytes):{},请重新下载",
|
||||
meta.len(),
|
||||
entry.path().display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let samples = decode_wav_16k_mono(TEST_WAV)?;
|
||||
let audio_ms = (samples.len() as u64) * 1000 / 16_000;
|
||||
|
||||
|
||||
@@ -135,12 +135,19 @@ where
|
||||
);
|
||||
return StreamingPolishOutcome::UnsupportedFallback;
|
||||
}
|
||||
let provider = match build_active_llm_provider(llm_thinking_enabled) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
let provider = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
build_active_llm_provider(llm_thinking_enabled)
|
||||
})) {
|
||||
Ok(Ok(p)) => p,
|
||||
Ok(Err(e)) => {
|
||||
log::error!("[coord] streaming polish: build provider failed: {e}");
|
||||
return StreamingPolishOutcome::Failed(e.to_string());
|
||||
}
|
||||
Err(panic) => {
|
||||
let msg = format!("build_active_llm_provider panicked: {:?}", panic);
|
||||
log::error!("[coord] {msg}");
|
||||
return StreamingPolishOutcome::Failed(msg);
|
||||
}
|
||||
};
|
||||
if !provider.supports_streaming_polish() {
|
||||
log::info!(
|
||||
@@ -259,7 +266,17 @@ pub(crate) async fn polish_text(
|
||||
.await?);
|
||||
}
|
||||
|
||||
let provider = build_active_llm_provider(llm_thinking_enabled)?;
|
||||
let provider = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
build_active_llm_provider(llm_thinking_enabled)
|
||||
})) {
|
||||
Ok(Ok(p)) => p,
|
||||
Ok(Err(e)) => return Err(e),
|
||||
Err(panic) => {
|
||||
let msg = format!("build_active_llm_provider panicked: {:?}", panic);
|
||||
log::error!("[coord] {msg}");
|
||||
anyhow::bail!(msg);
|
||||
}
|
||||
};
|
||||
Ok(provider
|
||||
.polish(
|
||||
raw,
|
||||
@@ -305,7 +322,17 @@ pub(crate) async fn translate_text(
|
||||
.await?);
|
||||
}
|
||||
|
||||
let provider = build_active_llm_provider(llm_thinking_enabled)?;
|
||||
let provider = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
build_active_llm_provider(llm_thinking_enabled)
|
||||
})) {
|
||||
Ok(Ok(p)) => p,
|
||||
Ok(Err(e)) => return Err(e),
|
||||
Err(panic) => {
|
||||
let msg = format!("build_active_llm_provider panicked: {:?}", panic);
|
||||
log::error!("[coord] {msg}");
|
||||
anyhow::bail!(msg);
|
||||
}
|
||||
};
|
||||
Ok(provider
|
||||
.translate_to(
|
||||
raw,
|
||||
@@ -597,7 +624,17 @@ where
|
||||
.await?);
|
||||
}
|
||||
|
||||
let provider = build_active_llm_provider(llm_thinking_enabled)?;
|
||||
let provider = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
build_active_llm_provider(llm_thinking_enabled)
|
||||
})) {
|
||||
Ok(Ok(p)) => p,
|
||||
Ok(Err(e)) => return Err(e),
|
||||
Err(panic) => {
|
||||
let msg = format!("build_active_llm_provider panicked: {:?}", panic);
|
||||
log::error!("[coord] {msg}");
|
||||
anyhow::bail!(msg);
|
||||
}
|
||||
};
|
||||
Ok(provider
|
||||
.answer_chat_streaming(
|
||||
messages,
|
||||
@@ -687,8 +724,17 @@ pub(crate) fn resolve_ark_endpoint_with_policy(
|
||||
if api_key.trim().is_empty() && endpoint.is_none() {
|
||||
anyhow::bail!("API Key 为空");
|
||||
}
|
||||
let resolved = endpoint
|
||||
let mut resolved = endpoint
|
||||
.unwrap_or_else(|| "https://ark.cn-beijing.volces.com/api/v3/chat/completions".to_string());
|
||||
// 兜底:用户可能在自定义 endpoint 时只写了 "192.168.1.100:8080/v1" 漏了 scheme,
|
||||
// 被 reqwest::IntoUrl 自动补 https:// 后对纯 HTTP 的 llama.cpp 发请求会 TLS 握手失败。
|
||||
// 检测到无 scheme 时自动补 http://(局域网自托管服务最常见的形态)。
|
||||
// 用 to_ascii_lowercase 防用户输大写如 HTTP://... 时被误判为缺 scheme。
|
||||
let lower = resolved.to_ascii_lowercase();
|
||||
if !lower.starts_with("http://") && !lower.starts_with("https://") {
|
||||
resolved = format!("http://{resolved}");
|
||||
log::info!("[llm] endpoint missing scheme, auto-prepended http://");
|
||||
}
|
||||
// issue #609 F-01(SSRF):用户自定义 endpoint 是 attacker-controlled,直接拿来发
|
||||
// 带 API Key 的请求等于把凭据指哪打哪。这里对 host/IP 段 + scheme 做配置时校验,
|
||||
// 默认官方 endpoint 也过一遍(它本就合法)。注意这只防住"配置即字面内网地址",
|
||||
|
||||
@@ -1252,9 +1252,16 @@ async fn send_with_transient_retry(
|
||||
request: reqwest::RequestBuilder,
|
||||
) -> Result<reqwest::Response, LLMError> {
|
||||
const RETRY_DELAY_MS: u64 = 500;
|
||||
let initial = request
|
||||
.try_clone()
|
||||
.expect("memory-backed body (json/form) must be clonable for retry");
|
||||
let Some(initial) = request.try_clone() else {
|
||||
// try_clone 失败(如 stream body 不可 clone)→ 不走重试,直接 send 一次。
|
||||
// 用 expect 会 panic 杀死整个进程,这里兜底为单次发送。
|
||||
log::warn!("[llm] request body not clonable, skipping retry");
|
||||
return match request.send().await {
|
||||
Ok(r) => Ok(r),
|
||||
Err(e) if e.is_timeout() => Err(LLMError::Timeout),
|
||||
Err(e) => Err(LLMError::Network(e.to_string())),
|
||||
};
|
||||
};
|
||||
match initial.send().await {
|
||||
Ok(r) => Ok(r),
|
||||
Err(e) if e.is_connect() || e.is_request() => {
|
||||
|
||||
Reference in new issue
Block a user