From 3b7055ed5219dcbfcb0d150f2cb758e82c996a66 Mon Sep 17 00:00:00 2001 From: coso Date: Wed, 7 Jan 2026 00:14:20 +0800 Subject: [PATCH] fix: improve streaming response stability for Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add proper HTTP client timeout configuration for ClaudeCustomProvider - connect_timeout: 30 seconds - timeout: 10 minutes (for long streaming responses) - tcp_keepalive: 60 seconds (keep connection alive) - Enhance SSE streaming response headers: - Cache-Control: no-cache, no-store, must-revalidate - X-Accel-Buffering: no (disable nginx proxy buffering) - Transfer-Encoding: chunked These changes should help resolve streaming truncation issues on Windows when using Kiro OAuth credentials through aiclient2api proxy. Fixes #68 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src-tauri/src/providers/claude_custom.rs | 20 +++++++++++++++++-- .../src/server/handlers/provider_calls.rs | 12 ++++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src-tauri/src/providers/claude_custom.rs b/src-tauri/src/providers/claude_custom.rs index 50167bcd1..23d4264a9 100644 --- a/src-tauri/src/providers/claude_custom.rs +++ b/src-tauri/src/providers/claude_custom.rs @@ -4,6 +4,7 @@ use crate::models::openai::{ChatCompletionRequest, ContentPart, MessageContent}; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::error::Error; +use std::time::Duration; #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ClaudeCustomConfig { @@ -17,11 +18,26 @@ pub struct ClaudeCustomProvider { pub client: Client, } +/// 创建配置好的 HTTP 客户端 +/// +/// 配置说明: +/// - connect_timeout: 连接超时 30 秒 +/// - timeout: 总超时 10 分钟(流式响应可能很长) +/// - 不设置 pool_idle_timeout 以保持连接活跃 +fn create_http_client() -> Client { + Client::builder() + .connect_timeout(Duration::from_secs(30)) + .timeout(Duration::from_secs(600)) // 10 分钟总超时,支持长时间流式响应 + .tcp_keepalive(Duration::from_secs(60)) // TCP keepalive 保持连接活跃 + .build() + .unwrap_or_else(|_| Client::new()) +} + impl Default for ClaudeCustomProvider { fn default() -> Self { Self { config: ClaudeCustomConfig::default(), - client: Client::new(), + client: create_http_client(), } } } @@ -39,7 +55,7 @@ impl ClaudeCustomProvider { base_url, enabled: true, }, - client: Client::new(), + client: create_http_client(), } } diff --git a/src-tauri/src/server/handlers/provider_calls.rs b/src-tauri/src/server/handlers/provider_calls.rs index ac8ca9770..16efe4cda 100644 --- a/src-tauri/src/server/handlers/provider_calls.rs +++ b/src-tauri/src/server/handlers/provider_calls.rs @@ -611,8 +611,10 @@ pub async fn call_provider_anthropic( return Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, "text/event-stream") - .header(header::CACHE_CONTROL, "no-cache") + .header(header::CACHE_CONTROL, "no-cache, no-store, must-revalidate") .header("Connection", "keep-alive") + .header("X-Accel-Buffering", "no") // 禁用 nginx 等代理的缓冲 + .header("Transfer-Encoding", "chunked") .body(Body::from_stream(stream)) .unwrap_or_else(|_| { ( @@ -823,8 +825,10 @@ pub async fn call_provider_anthropic( return Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, "text/event-stream") - .header(header::CACHE_CONTROL, "no-cache") + .header(header::CACHE_CONTROL, "no-cache, no-store, must-revalidate") .header("Connection", "keep-alive") + .header("X-Accel-Buffering", "no") // 禁用 nginx 等代理的缓冲 + .header("Transfer-Encoding", "chunked") .body(Body::from_stream(stream)) .unwrap_or_else(|_| { ( @@ -1834,8 +1838,10 @@ pub async fn call_provider_openai( return Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, "text/event-stream") - .header(header::CACHE_CONTROL, "no-cache") + .header(header::CACHE_CONTROL, "no-cache, no-store, must-revalidate") .header("Connection", "keep-alive") + .header("X-Accel-Buffering", "no") // 禁用 nginx 等代理的缓冲 + .header("Transfer-Encoding", "chunked") .body(Body::from_stream(stream)) .unwrap_or_else(|_| { (