Extend square-hole creation flow with visual asset timeout guard

This commit is contained in:
kdletters
2026-05-05 15:27:09 +08:00
parent 2252afb080
commit 60b667a9d1
30 changed files with 2838 additions and 215 deletions

View File

@@ -68,6 +68,7 @@ pub struct LlmTextRequest {
pub max_tokens: Option<u32>,
pub enable_web_search: bool,
pub protocol: LlmTextProtocol,
pub request_timeout_ms: Option<u64>,
}
// 文本协议必须由业务请求显式选择,避免全局默认模型把不同场景混到同一上游形态。
@@ -421,6 +422,7 @@ impl LlmTextRequest {
max_tokens: None,
enable_web_search: false,
protocol: LlmTextProtocol::ChatCompletions,
request_timeout_ms: None,
}
}
@@ -451,6 +453,11 @@ impl LlmTextRequest {
self
}
pub fn with_request_timeout_ms(mut self, request_timeout_ms: u64) -> Self {
self.request_timeout_ms = Some(request_timeout_ms);
self
}
fn validate(&self) -> Result<(), LlmError> {
if self.messages.is_empty() {
return Err(LlmError::InvalidRequest(
@@ -474,6 +481,14 @@ impl LlmTextRequest {
));
}
if let Some(request_timeout_ms) = self.request_timeout_ms
&& request_timeout_ms == 0
{
return Err(LlmError::InvalidRequest(
"LLM request_timeout_ms 必须大于 0".to_string(),
));
}
Ok(())
}
@@ -484,6 +499,12 @@ impl LlmTextRequest {
.filter(|value| !value.is_empty())
.unwrap_or(fallback_model)
}
fn resolved_request_timeout_ms(&self, fallback_timeout_ms: u64) -> u64 {
self.request_timeout_ms
.filter(|value| *value > 0)
.unwrap_or(fallback_timeout_ms)
}
}
impl LlmTextProtocol {
@@ -825,7 +846,9 @@ impl LlmClient {
.post(url.as_str())
.bearer_auth(self.config.api_key())
.json(&request_body)
.timeout(Duration::from_millis(self.config.request_timeout_ms()))
.timeout(Duration::from_millis(
request.resolved_request_timeout_ms(self.config.request_timeout_ms()),
))
.send()
.await;
@@ -1592,6 +1615,48 @@ mod tests {
assert_eq!(response.response_id.as_deref(), Some("resp_retry"));
}
#[tokio::test]
async fn request_text_uses_request_level_timeout_override() {
let listener = TcpListener::bind("127.0.0.1:0").expect("listener should bind");
let address = listener.local_addr().expect("listener should have addr");
thread::spawn(move || {
let (mut stream, _) = listener.accept().expect("request should connect");
let _ = read_request(&mut stream);
thread::sleep(StdDuration::from_millis(200));
write_response(
&mut stream,
MockResponse {
status_line: "200 OK",
content_type: "application/json; charset=utf-8",
body: r#"{"choices":[{"message":{"content":"too late"},"finish_reason":"stop"}]}"#
.to_string(),
extra_headers: Vec::new(),
},
);
});
let config = LlmConfig::new(
LlmProvider::Ark,
format!("http://{address}"),
"test-key".to_string(),
"test-model".to_string(),
10_000,
0,
1,
)
.expect("config should be valid");
let client = LlmClient::new(config).expect("client should be created");
let error = client
.request_text(
LlmTextRequest::single_turn("系统", "用户").with_request_timeout_ms(20),
)
.await
.expect_err("request override should timeout before the global timeout");
assert_eq!(error, LlmError::Timeout { attempts: 1 });
}
#[tokio::test]
async fn request_text_sends_web_search_options_when_enabled() {
let listener = TcpListener::bind("127.0.0.1:0").expect("listener should bind");