mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(llm): per-Model HTTP read-timeout override
This commit is contained in:
parent
0c7554212b
commit
55f4ba1195
@ -1095,6 +1095,16 @@ public class AgentGraphBuilder {
|
||||
|
||||
/** Transitional public visibility for {@code chatmodel} sub-package builders; will move into the builder in PR-0b. */
|
||||
public OpenAiApi buildOpenAiApi(ModelProviderEntity provider) {
|
||||
return buildOpenAiApi(provider, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC-03 Lane B1 overload — accepts a per-model read-timeout override
|
||||
* (seconds). Threaded into both the sync RestClient and streaming
|
||||
* WebClient so timeout behavior is consistent across blocking and
|
||||
* streaming chat completions. Null falls back to the default 180s.
|
||||
*/
|
||||
public OpenAiApi buildOpenAiApi(ModelProviderEntity provider, Integer readTimeoutOverride) {
|
||||
if (provider == null || !modelProviderService.isProviderConfigured(provider.getProviderId())) {
|
||||
throw new MateClawException("err.agent.provider_not_configured", "Provider 未完成配置,请在模型设置中填写有效的 API Key 和 Base URL");
|
||||
}
|
||||
@ -1116,9 +1126,9 @@ public class AgentGraphBuilder {
|
||||
MultiValueMap<String, String> headers = buildOpenAiHeaders(kwargs);
|
||||
String completionsPath = resolveOpenAiCompletionsPath(baseUrl, kwargs);
|
||||
RestClient.Builder restClientBuilder = applyHttpTimeouts(
|
||||
restClientBuilderProvider.getIfAvailable(RestClient::builder));
|
||||
restClientBuilderProvider.getIfAvailable(RestClient::builder), readTimeoutOverride);
|
||||
WebClient.Builder webClientBuilder = applyHttpTimeoutsToWebClient(
|
||||
webClientBuilderProvider.getIfAvailable(WebClient::builder));
|
||||
webClientBuilderProvider.getIfAvailable(WebClient::builder), readTimeoutOverride);
|
||||
|
||||
// Spring AI OpenAiApi 构造函数会先 set User-Agent 为 "spring-ai",再 addAll 我们的 headers,
|
||||
// 导致自定义 User-Agent 被追加而非覆盖。因此对需要伪装客户端身份的 provider(如 kimi-code),
|
||||
@ -1475,11 +1485,19 @@ public class AgentGraphBuilder {
|
||||
* readTimeout=180s(覆盖 nginx 60s 网关超时 + 留足真实长响应余量;超时后由上层 retry 接管)。
|
||||
*/
|
||||
private RestClient.Builder applyHttpTimeouts(RestClient.Builder builder) {
|
||||
return applyHttpTimeouts(builder, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC-03 Lane B1 overload — accepts a per-model read-timeout override
|
||||
* (seconds). Null falls back to the default 180s.
|
||||
*/
|
||||
private RestClient.Builder applyHttpTimeouts(RestClient.Builder builder, Integer readTimeoutOverride) {
|
||||
HttpClient httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(10))
|
||||
.connectTimeout(vip.mate.llm.chatmodel.HttpTimeouts.CONNECT_TIMEOUT)
|
||||
.build();
|
||||
JdkClientHttpRequestFactory rf = new JdkClientHttpRequestFactory(httpClient);
|
||||
rf.setReadTimeout(Duration.ofSeconds(180));
|
||||
rf.setReadTimeout(vip.mate.llm.chatmodel.HttpTimeouts.resolveReadTimeout(readTimeoutOverride));
|
||||
return builder.requestFactory(rf);
|
||||
}
|
||||
|
||||
@ -1499,12 +1517,20 @@ public class AgentGraphBuilder {
|
||||
* starter is excluded by design).
|
||||
*/
|
||||
private WebClient.Builder applyHttpTimeoutsToWebClient(WebClient.Builder builder) {
|
||||
return applyHttpTimeoutsToWebClient(builder, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC-03 Lane B1 overload — same per-model override semantics as
|
||||
* {@link #applyHttpTimeouts(RestClient.Builder, Integer)}.
|
||||
*/
|
||||
private WebClient.Builder applyHttpTimeoutsToWebClient(WebClient.Builder builder, Integer readTimeoutOverride) {
|
||||
HttpClient httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(10))
|
||||
.connectTimeout(vip.mate.llm.chatmodel.HttpTimeouts.CONNECT_TIMEOUT)
|
||||
.build();
|
||||
org.springframework.http.client.reactive.JdkClientHttpConnector connector =
|
||||
new org.springframework.http.client.reactive.JdkClientHttpConnector(httpClient);
|
||||
connector.setReadTimeout(Duration.ofSeconds(180));
|
||||
connector.setReadTimeout(vip.mate.llm.chatmodel.HttpTimeouts.resolveReadTimeout(readTimeoutOverride));
|
||||
return builder.clientConnector(connector);
|
||||
}
|
||||
|
||||
|
||||
@ -63,7 +63,7 @@ public class AgentAnthropicChatModelBuilder implements ChatModelBuilder {
|
||||
|
||||
@Override
|
||||
public ChatModel build(ModelConfigEntity model, ModelProviderEntity provider, RetryTemplate retry) {
|
||||
AnthropicApi api = buildAnthropicApi(provider);
|
||||
AnthropicApi api = buildAnthropicApi(provider, model.getRequestTimeoutSeconds());
|
||||
AnthropicChatOptions options = buildAnthropicOptions(model);
|
||||
return AnthropicChatModel.builder()
|
||||
.anthropicApi(api)
|
||||
@ -74,6 +74,14 @@ public class AgentAnthropicChatModelBuilder implements ChatModelBuilder {
|
||||
}
|
||||
|
||||
AnthropicApi buildAnthropicApi(ModelProviderEntity provider) {
|
||||
return buildAnthropicApi(provider, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC-03 Lane B1 overload — accepts a per-model read-timeout override
|
||||
* (seconds). Null falls back to the default 180s.
|
||||
*/
|
||||
AnthropicApi buildAnthropicApi(ModelProviderEntity provider, Integer readTimeoutOverride) {
|
||||
if (provider == null || !modelProviderService.isProviderConfigured(provider.getProviderId())) {
|
||||
throw new MateClawException("err.agent.anthropic_not_configured",
|
||||
"Anthropic Provider 未完成配置,请在模型设置中填写有效的 API Key 和 Base URL");
|
||||
@ -85,9 +93,9 @@ public class AgentAnthropicChatModelBuilder implements ChatModelBuilder {
|
||||
}
|
||||
String baseUrl = provider.getBaseUrl();
|
||||
RestClient.Builder restClientBuilder = applyHttpTimeouts(
|
||||
restClientBuilderProvider.getIfAvailable(RestClient::builder));
|
||||
restClientBuilderProvider.getIfAvailable(RestClient::builder), readTimeoutOverride);
|
||||
WebClient.Builder webClientBuilder = applyHttpTimeoutsToWebClient(
|
||||
webClientBuilderProvider.getIfAvailable(WebClient::builder));
|
||||
webClientBuilderProvider.getIfAvailable(WebClient::builder), readTimeoutOverride);
|
||||
|
||||
AnthropicApi.Builder builder = AnthropicApi.builder()
|
||||
.apiKey(apiKey.trim())
|
||||
@ -194,11 +202,20 @@ public class AgentAnthropicChatModelBuilder implements ChatModelBuilder {
|
||||
* duplicating the snippet.</p>
|
||||
*/
|
||||
static RestClient.Builder applyHttpTimeouts(RestClient.Builder builder) {
|
||||
return applyHttpTimeouts(builder, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC-03 Lane B1 overload — accepts a per-model read-timeout override
|
||||
* (seconds). Null / zero / negative falls back to {@link vip.mate.llm.chatmodel.HttpTimeouts#DEFAULT_READ_TIMEOUT}
|
||||
* so unset model configs keep the historical 180s.
|
||||
*/
|
||||
static RestClient.Builder applyHttpTimeouts(RestClient.Builder builder, Integer readTimeoutOverride) {
|
||||
HttpClient httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(10))
|
||||
.connectTimeout(vip.mate.llm.chatmodel.HttpTimeouts.CONNECT_TIMEOUT)
|
||||
.build();
|
||||
JdkClientHttpRequestFactory rf = new JdkClientHttpRequestFactory(httpClient);
|
||||
rf.setReadTimeout(Duration.ofSeconds(180));
|
||||
rf.setReadTimeout(vip.mate.llm.chatmodel.HttpTimeouts.resolveReadTimeout(readTimeoutOverride));
|
||||
return builder.requestFactory(rf);
|
||||
}
|
||||
|
||||
@ -214,12 +231,20 @@ public class AgentAnthropicChatModelBuilder implements ChatModelBuilder {
|
||||
* doesn't pull in reactor-netty (excluded by this project's pom).
|
||||
*/
|
||||
static WebClient.Builder applyHttpTimeoutsToWebClient(WebClient.Builder builder) {
|
||||
return applyHttpTimeoutsToWebClient(builder, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC-03 Lane B1 overload — same per-model override semantics as
|
||||
* {@link #applyHttpTimeouts(RestClient.Builder, Integer)}.
|
||||
*/
|
||||
static WebClient.Builder applyHttpTimeoutsToWebClient(WebClient.Builder builder, Integer readTimeoutOverride) {
|
||||
HttpClient httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(10))
|
||||
.connectTimeout(vip.mate.llm.chatmodel.HttpTimeouts.CONNECT_TIMEOUT)
|
||||
.build();
|
||||
org.springframework.http.client.reactive.JdkClientHttpConnector connector =
|
||||
new org.springframework.http.client.reactive.JdkClientHttpConnector(httpClient);
|
||||
connector.setReadTimeout(Duration.ofSeconds(180));
|
||||
connector.setReadTimeout(vip.mate.llm.chatmodel.HttpTimeouts.resolveReadTimeout(readTimeoutOverride));
|
||||
return builder.clientConnector(connector);
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,7 +96,7 @@ public class AgentClaudeCodeChatModelBuilder implements ChatModelBuilder {
|
||||
String accessToken = oauthService.getValidToken();
|
||||
|
||||
// 2) Build the Anthropic API client wired with OAuth headers.
|
||||
AnthropicApi api = buildOauthAnthropicApi(accessToken);
|
||||
AnthropicApi api = buildOauthAnthropicApi(accessToken, model.getRequestTimeoutSeconds());
|
||||
|
||||
// 3) Reuse the canonical Anthropic options builder — same Claude 4.7
|
||||
// sampling-params handling, thinking-budget mapping, prompt cache.
|
||||
@ -122,6 +122,15 @@ public class AgentClaudeCodeChatModelBuilder implements ChatModelBuilder {
|
||||
* can verify header composition without spinning up a chat model.
|
||||
*/
|
||||
AnthropicApi buildOauthAnthropicApi(String accessToken) {
|
||||
return buildOauthAnthropicApi(accessToken, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC-03 Lane B1 overload — same OAuth-stamped Anthropic client, with a
|
||||
* per-model read-timeout override threaded through to the underlying
|
||||
* RestClient + WebClient timeouts.
|
||||
*/
|
||||
AnthropicApi buildOauthAnthropicApi(String accessToken, Integer readTimeoutOverride) {
|
||||
String authHeader = apiHeaders.bearerAuth(accessToken);
|
||||
String userAgent = apiHeaders.userAgent();
|
||||
String xApp = apiHeaders.xApp();
|
||||
@ -135,7 +144,7 @@ public class AgentClaudeCodeChatModelBuilder implements ChatModelBuilder {
|
||||
// rate-limited harder than spec'd. Reference: openclaw
|
||||
// anthropic-transport-stream.ts:567-574.
|
||||
RestClient.Builder restClientBuilder = AgentAnthropicChatModelBuilder.applyHttpTimeouts(
|
||||
restClientBuilderProvider.getIfAvailable(RestClient::builder))
|
||||
restClientBuilderProvider.getIfAvailable(RestClient::builder), readTimeoutOverride)
|
||||
.defaultHeader(HttpHeaders.AUTHORIZATION, authHeader)
|
||||
.defaultHeader(HttpHeaders.USER_AGENT, userAgent)
|
||||
.defaultHeader(HttpHeaders.ACCEPT, "application/json")
|
||||
@ -152,7 +161,8 @@ public class AgentClaudeCodeChatModelBuilder implements ChatModelBuilder {
|
||||
// staring at SDK internals.
|
||||
.requestInterceptor(new RateLimitDiagnosticInterceptor());
|
||||
|
||||
WebClient.Builder webClientBuilder = webClientBuilderProvider.getIfAvailable(WebClient::builder)
|
||||
WebClient.Builder webClientBuilder = AgentAnthropicChatModelBuilder.applyHttpTimeoutsToWebClient(
|
||||
webClientBuilderProvider.getIfAvailable(WebClient::builder), readTimeoutOverride)
|
||||
.defaultHeader(HttpHeaders.AUTHORIZATION, authHeader)
|
||||
.defaultHeader(HttpHeaders.USER_AGENT, userAgent)
|
||||
.defaultHeader(HttpHeaders.ACCEPT, "application/json")
|
||||
|
||||
@ -42,7 +42,10 @@ public class AgentOpenAiCompatibleChatModelBuilder implements ChatModelBuilder {
|
||||
|
||||
@Override
|
||||
public ChatModel build(ModelConfigEntity model, ModelProviderEntity provider, RetryTemplate retry) {
|
||||
OpenAiApi api = agentGraphBuilder.buildOpenAiApi(provider);
|
||||
// RFC-03 Lane B1 — pass model.requestTimeoutSeconds so providers /
|
||||
// models with extended-thinking p99s don't false-positive on the
|
||||
// hardcoded 180s read timeout.
|
||||
OpenAiApi api = agentGraphBuilder.buildOpenAiApi(provider, model.getRequestTimeoutSeconds());
|
||||
OpenAiChatOptions options = agentGraphBuilder.buildOpenAiOptions(model, provider);
|
||||
ChatModel raw = OpenAiChatModel.builder()
|
||||
.openAiApi(api)
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
package vip.mate.llm.chatmodel;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* RFC-03 Lane B1 — central resolver for the per-LLM-request HTTP read
|
||||
* timeout, so {@link vip.mate.llm.model.ModelConfigEntity#getRequestTimeoutSeconds()}
|
||||
* can override the legacy 180s default without each chatmodel builder
|
||||
* inventing its own fallback chain.
|
||||
*
|
||||
* <p>Used by:
|
||||
* <ul>
|
||||
* <li>{@code AgentAnthropicChatModelBuilder.applyHttpTimeouts}</li>
|
||||
* <li>{@code AgentAnthropicChatModelBuilder.applyHttpTimeoutsToWebClient}</li>
|
||||
* <li>{@code AgentClaudeCodeChatModelBuilder} (via Anthropic helper)</li>
|
||||
* <li>{@code AgentGraphBuilder} legacy timeout helpers</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Connect timeout stays at the canonical 10s — long-tail thinking
|
||||
* latency manifests on the read path, not on connect.
|
||||
*/
|
||||
public final class HttpTimeouts {
|
||||
|
||||
/** Connect timeout — never overridable; 10s is enough for any sane endpoint. */
|
||||
public static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(10);
|
||||
|
||||
/**
|
||||
* Default read timeout when no per-model override is set. Matches the
|
||||
* historical hardcoded value so unset rows behave identically to the
|
||||
* pre-RFC-03 baseline.
|
||||
*/
|
||||
public static final Duration DEFAULT_READ_TIMEOUT = Duration.ofSeconds(180);
|
||||
|
||||
private HttpTimeouts() {}
|
||||
|
||||
/**
|
||||
* Resolve the effective read timeout: the override if positive, else the
|
||||
* canonical 180s default. Null and non-positive values fall back, so
|
||||
* callers can pass {@code modelConfig.getRequestTimeoutSeconds()} directly
|
||||
* without null-checks.
|
||||
*/
|
||||
public static Duration resolveReadTimeout(Integer override) {
|
||||
if (override == null || override <= 0) {
|
||||
return DEFAULT_READ_TIMEOUT;
|
||||
}
|
||||
return Duration.ofSeconds(override);
|
||||
}
|
||||
}
|
||||
@ -35,6 +35,21 @@ public class ModelConfigEntity {
|
||||
/** 模型最大输入 token 数(上下文窗口),0 或 null 表示使用全局默认 */
|
||||
private Integer maxInputTokens;
|
||||
|
||||
/**
|
||||
* RFC-03 Lane B1 — per-model HTTP read timeout (seconds).
|
||||
*
|
||||
* <p>Null / zero / negative → fall back to the global default of 180s
|
||||
* (existing behavior, see {@code AgentAnthropicChatModelBuilder.applyHttpTimeouts}
|
||||
* and the corresponding helper in {@code AgentGraphBuilder}). A positive
|
||||
* value overrides for this specific model.
|
||||
*
|
||||
* <p>Use cases: {@code o1-pro} / Claude thinking-mode / large-prompt
|
||||
* generation calls that legitimately exceed 3 min, where the default
|
||||
* raises false-positive timeouts; conversely {@code haiku}-class models
|
||||
* that p99 well under 30s, where a tighter timeout fails fast.
|
||||
*/
|
||||
private Integer requestTimeoutSeconds;
|
||||
|
||||
private Double topP;
|
||||
|
||||
private Boolean enableSearch;
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
-- V75: Per-model HTTP read timeout (RFC-03 Lane B1).
|
||||
-- Lets thinking models (o1-pro, claude opus extended-thinking, qwen3-max
|
||||
-- with deep reasoning) override the default 180s read timeout when their
|
||||
-- p99 legitimately exceeds it. Null / zero keeps the existing global
|
||||
-- default — no behavior change for existing rows.
|
||||
|
||||
ALTER TABLE mate_model_config ADD COLUMN IF NOT EXISTS request_timeout_seconds INTEGER;
|
||||
@ -0,0 +1,15 @@
|
||||
-- V75: Per-model HTTP read timeout (RFC-03 Lane B1).
|
||||
-- Lets thinking models (o1-pro, claude opus extended-thinking, qwen3-max
|
||||
-- with deep reasoning) override the default 180s read timeout when their
|
||||
-- p99 legitimately exceeds it. Null / zero keeps the existing global
|
||||
-- default — no behavior change for existing rows.
|
||||
--
|
||||
-- MySQL lacks `ADD COLUMN IF NOT EXISTS`; use INFORMATION_SCHEMA guard.
|
||||
SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'mate_model_config'
|
||||
AND COLUMN_NAME = 'request_timeout_seconds');
|
||||
SET @s := IF(@c = 0,
|
||||
'ALTER TABLE mate_model_config ADD COLUMN request_timeout_seconds INT DEFAULT NULL',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
Loading…
Reference in New Issue
Block a user