fix(llm): pin OpenAI-compatible HTTP client to HTTP/1.1 (#89)

This commit is contained in:
matevip 2026-05-11 11:22:23 +08:00
parent 3f1f83bbfe
commit 9fd5843dda
2 changed files with 27 additions and 2 deletions

View File

@ -1611,6 +1611,7 @@ public class AgentGraphBuilder {
private RestClient.Builder applyHttpTimeouts(RestClient.Builder builder, Integer readTimeoutOverride) {
HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(vip.mate.llm.chatmodel.HttpTimeouts.CONNECT_TIMEOUT)
.version(HttpClient.Version.HTTP_1_1)
.build();
JdkClientHttpRequestFactory rf = new JdkClientHttpRequestFactory(httpClient);
rf.setReadTimeout(vip.mate.llm.chatmodel.HttpTimeouts.resolveReadTimeout(readTimeoutOverride));
@ -1641,8 +1642,13 @@ public class AgentGraphBuilder {
* {@link #applyHttpTimeouts(RestClient.Builder, Integer)}.
*/
private WebClient.Builder applyHttpTimeoutsToWebClient(WebClient.Builder builder, Integer readTimeoutOverride) {
// Pin HTTP/1.1: many self-hosted OpenAI-compatible servers (vLLM, lmstudio,
// llama.cpp, ollama all uvicorn/ASGI based) only speak HTTP/1.1 over
// cleartext and slam the socket on the JDK client's default H2C upgrade
// probe, surfacing as "header parser received no bytes" with no body sent.
HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(vip.mate.llm.chatmodel.HttpTimeouts.CONNECT_TIMEOUT)
.version(HttpClient.Version.HTTP_1_1)
.build();
org.springframework.http.client.reactive.JdkClientHttpConnector connector =
new org.springframework.http.client.reactive.JdkClientHttpConnector(httpClient);

View File

@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestClient;
@ -13,6 +14,7 @@ import vip.mate.exception.MateClawException;
import vip.mate.llm.model.*;
import vip.mate.llm.oauth.OpenAIOAuthService;
import java.net.http.HttpClient;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.CompletableFuture;
@ -413,7 +415,7 @@ public class ModelDiscoveryService {
}
String apiKey = provider.getApiKey();
RestClient client = RestClient.builder()
RestClient client = openAiCompatibleClientBuilder()
.baseUrl(baseUrl)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.build();
@ -623,7 +625,7 @@ public class ModelDiscoveryService {
Map<String, Object> kwargs = modelProviderService.readProviderGenerateKwargs(provider);
String completionsPath = resolveCompletionsPath(baseUrl, kwargs);
RestClient.RequestHeadersSpec<?> spec = RestClient.builder()
RestClient.RequestHeadersSpec<?> spec = openAiCompatibleClientBuilder()
.baseUrl(baseUrl)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build()
@ -915,6 +917,23 @@ public class ModelDiscoveryService {
return normalized;
}
/**
* Build a RestClient.Builder pinned to HTTP/1.1 for self-hosted OpenAI-compatible
* servers. Java's HttpClient defaults to HTTP/2 and over cleartext attempts an
* H2C upgrade ({@code Upgrade: h2c, Connection: Upgrade, HTTP2-Settings: ...}).
* Uvicorn-based stacks (vLLM, lmstudio, llama.cpp, ollama) reject the upgrade
* by closing the socket mid-handshake surfacing as either
* "header parser received no bytes" on the chat path or, more subtly, a
* 400 with body=None on the test path because the body never makes it past
* the upgrade negotiation.
*/
private RestClient.Builder openAiCompatibleClientBuilder() {
HttpClient httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.build();
return RestClient.builder().requestFactory(new JdkClientHttpRequestFactory(httpClient));
}
@SuppressWarnings("unchecked")
private void applyCustomHeaders(RestClient.RequestHeadersSpec<?> spec, Map<String, Object> kwargs) {
if (kwargs == null) {