Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 51 additions & 27 deletions openless-all/app/src-tauri/src/polish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -514,15 +514,7 @@ impl OpenAICompatibleLLMProvider {
}
let request = request.json(body);

let response = match request.send().await {
Ok(r) => r,
Err(e) => {
if e.is_timeout() {
return Err(LLMError::Timeout);
}
return Err(LLMError::Network(e.to_string()));
}
};
let response = send_with_transient_retry(request).await?;

let status = response.status();
let body_text = response
Expand Down Expand Up @@ -588,15 +580,7 @@ impl OpenAICompatibleLLMProvider {
}
let request = request.json(&body);

let response = match request.send().await {
Ok(r) => r,
Err(e) => {
if e.is_timeout() {
return Err(LLMError::Timeout);
}
return Err(LLMError::Network(e.to_string()));
}
};
let response = send_with_transient_retry(request).await?;

let status = response.status();
if !status.is_success() {
Expand Down Expand Up @@ -717,15 +701,7 @@ impl OpenAICompatibleLLMProvider {
}
let request = request.json(&body);

let response = match request.send().await {
Ok(r) => r,
Err(e) => {
if e.is_timeout() {
return Err(LLMError::Timeout);
}
return Err(LLMError::Network(e.to_string()));
}
};
let response = send_with_transient_retry(request).await?;

let status = response.status();
if !status.is_success() {
Expand Down Expand Up @@ -1260,6 +1236,54 @@ pub(crate) fn http_client_builder(base_url: &str, timeout_secs: u64) -> reqwest:
}
}

/// 发请求 + 网络抖动 retry:**只**对 `is_connect()` / `is_request()` 这两类「服务端
/// 必然没收到」的失败重试一次。`is_timeout()` 故意**不**重试——超时时服务端可能已经
/// 在处理请求并扣计费(LLM completion 是非幂等动作),重试会导致重复 billing + 重复
/// completion。HTTP 4xx/5xx 不在这里触发——那些走 response.status() 分支单独处理。
///
/// 调用前提:传入的 RequestBuilder body 必须是内存型(json / form),不能是 stream
/// reader——retry 用 `try_clone()` 复制 RequestBuilder,stream body 不支持。
///
/// 对流式 SSE 路径 retry 是安全的:connect / request 类失败发生在 TCP 握手 / HTTP
/// 请求写出阶段,response 还没回 → on_delta 必然未被调用 → 不会有「已流式输出的字
/// 被重复」的问题。
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");
match initial.send().await {
Ok(r) => Ok(r),
Err(e) if e.is_connect() || e.is_request() => {
log::warn!(
"[llm] send transient failure, retry in {}ms: {}",
RETRY_DELAY_MS,
e
);
tokio::time::sleep(Duration::from_millis(RETRY_DELAY_MS)).await;
match request.send().await {
Ok(r) => Ok(r),
Err(e2) => {
if e2.is_timeout() {
Err(LLMError::Timeout)
} else {
Err(LLMError::Network(e2.to_string()))
}
}
}
}
Err(e) => {
if e.is_timeout() {
Err(LLMError::Timeout)
} else {
Err(LLMError::Network(e.to_string()))
}
}
}
}

fn should_bypass_proxy_for_base_url(base_url: &str) -> bool {
let Ok(url) = reqwest::Url::parse(base_url.trim()) else {
return false;
Expand Down
Loading