mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +08:00
fix(llm): apply read timeout to streaming chat WebClient (openai-compat + anthropic)
This commit is contained in:
parent
92a7508dac
commit
6390abdecc
@ -1066,7 +1066,8 @@ public class AgentGraphBuilder {
|
|||||||
String completionsPath = resolveOpenAiCompletionsPath(baseUrl, kwargs);
|
String completionsPath = resolveOpenAiCompletionsPath(baseUrl, kwargs);
|
||||||
RestClient.Builder restClientBuilder = applyHttpTimeouts(
|
RestClient.Builder restClientBuilder = applyHttpTimeouts(
|
||||||
restClientBuilderProvider.getIfAvailable(RestClient::builder));
|
restClientBuilderProvider.getIfAvailable(RestClient::builder));
|
||||||
WebClient.Builder webClientBuilder = webClientBuilderProvider.getIfAvailable(WebClient::builder);
|
WebClient.Builder webClientBuilder = applyHttpTimeoutsToWebClient(
|
||||||
|
webClientBuilderProvider.getIfAvailable(WebClient::builder));
|
||||||
|
|
||||||
// Spring AI OpenAiApi 构造函数会先 set User-Agent 为 "spring-ai",再 addAll 我们的 headers,
|
// Spring AI OpenAiApi 构造函数会先 set User-Agent 为 "spring-ai",再 addAll 我们的 headers,
|
||||||
// 导致自定义 User-Agent 被追加而非覆盖。因此对需要伪装客户端身份的 provider(如 kimi-code),
|
// 导致自定义 User-Agent 被追加而非覆盖。因此对需要伪装客户端身份的 provider(如 kimi-code),
|
||||||
@ -1431,6 +1432,31 @@ public class AgentGraphBuilder {
|
|||||||
return builder.requestFactory(rf);
|
return builder.requestFactory(rf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply equivalent timeouts to the WebClient that backs OpenAI-compatible
|
||||||
|
* STREAMING calls (chat completions with {@code stream:true}). The
|
||||||
|
* RestClient version above only protects synchronous HTTP — without this,
|
||||||
|
* the streaming code path uses the default {@code WebClient} which has
|
||||||
|
* neither connect nor read timeout, so a stalled provider can hang the
|
||||||
|
* call forever (observed: a single volcengine-plan request held the agent
|
||||||
|
* thread for 9+ minutes with no error, until the user manually pressed
|
||||||
|
* Stop). That kept the failover chain idle because nothing threw.
|
||||||
|
* <p>
|
||||||
|
* Uses {@link JdkClientHttpConnector} with the same {@link HttpClient} we
|
||||||
|
* already use for the RestClient so the dependency surface stays clean
|
||||||
|
* (reactor-netty is not on this project's classpath — Spring's webflux
|
||||||
|
* starter is excluded by design).
|
||||||
|
*/
|
||||||
|
private WebClient.Builder applyHttpTimeoutsToWebClient(WebClient.Builder builder) {
|
||||||
|
HttpClient httpClient = HttpClient.newBuilder()
|
||||||
|
.connectTimeout(Duration.ofSeconds(10))
|
||||||
|
.build();
|
||||||
|
org.springframework.http.client.reactive.JdkClientHttpConnector connector =
|
||||||
|
new org.springframework.http.client.reactive.JdkClientHttpConnector(httpClient);
|
||||||
|
connector.setReadTimeout(Duration.ofSeconds(180));
|
||||||
|
return builder.clientConnector(connector);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从 generateKwargs.headers 中提取需要强制覆盖的 headers。
|
* 从 generateKwargs.headers 中提取需要强制覆盖的 headers。
|
||||||
* 用于通过 RestClient/WebClient 拦截器绕过 Spring AI OpenAiApi 的默认 User-Agent。
|
* 用于通过 RestClient/WebClient 拦截器绕过 Spring AI OpenAiApi 的默认 User-Agent。
|
||||||
|
|||||||
@ -86,7 +86,8 @@ public class AgentAnthropicChatModelBuilder implements ChatModelBuilder {
|
|||||||
String baseUrl = provider.getBaseUrl();
|
String baseUrl = provider.getBaseUrl();
|
||||||
RestClient.Builder restClientBuilder = applyHttpTimeouts(
|
RestClient.Builder restClientBuilder = applyHttpTimeouts(
|
||||||
restClientBuilderProvider.getIfAvailable(RestClient::builder));
|
restClientBuilderProvider.getIfAvailable(RestClient::builder));
|
||||||
WebClient.Builder webClientBuilder = webClientBuilderProvider.getIfAvailable(WebClient::builder);
|
WebClient.Builder webClientBuilder = applyHttpTimeoutsToWebClient(
|
||||||
|
webClientBuilderProvider.getIfAvailable(WebClient::builder));
|
||||||
|
|
||||||
AnthropicApi.Builder builder = AnthropicApi.builder()
|
AnthropicApi.Builder builder = AnthropicApi.builder()
|
||||||
.apiKey(apiKey.trim())
|
.apiKey(apiKey.trim())
|
||||||
@ -200,4 +201,25 @@ public class AgentAnthropicChatModelBuilder implements ChatModelBuilder {
|
|||||||
rf.setReadTimeout(Duration.ofSeconds(180));
|
rf.setReadTimeout(Duration.ofSeconds(180));
|
||||||
return builder.requestFactory(rf);
|
return builder.requestFactory(rf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Streaming counterpart of {@link #applyHttpTimeouts(RestClient.Builder)}.
|
||||||
|
* Without this, Spring AI's AnthropicApi would back its streaming chat
|
||||||
|
* call by a default WebClient with neither connect nor read timeout — a
|
||||||
|
* stalled provider could hang the agent thread indefinitely while the
|
||||||
|
* failover chain idles (no exception = no signal).
|
||||||
|
* <p>
|
||||||
|
* Mirrors AgentGraphBuilder.applyHttpTimeoutsToWebClient: same JDK
|
||||||
|
* HttpClient + JdkClientHttpConnector path, so the dependency surface
|
||||||
|
* doesn't pull in reactor-netty (excluded by this project's pom).
|
||||||
|
*/
|
||||||
|
static WebClient.Builder applyHttpTimeoutsToWebClient(WebClient.Builder builder) {
|
||||||
|
HttpClient httpClient = HttpClient.newBuilder()
|
||||||
|
.connectTimeout(Duration.ofSeconds(10))
|
||||||
|
.build();
|
||||||
|
org.springframework.http.client.reactive.JdkClientHttpConnector connector =
|
||||||
|
new org.springframework.http.client.reactive.JdkClientHttpConnector(httpClient);
|
||||||
|
connector.setReadTimeout(Duration.ofSeconds(180));
|
||||||
|
return builder.clientConnector(connector);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user