From 4898b79d49ec7f8b4775eaa74858d50ec9ab0fec Mon Sep 17 00:00:00 2001 From: matevip Date: Mon, 27 Apr 2026 20:35:24 +0800 Subject: [PATCH] fix(agent): strip tool_choice="auto" so strict OpenAI-compatible servers accept the request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some self-hosted OpenAI-compatible serving frameworks return a 400 Bad Request with a generic Pydantic "body=None / Field required" error when the outbound request carries tool_choice="auto" but the server was launched without an auto-tool-choice opt-in flag. The error message hides the real cause: the request is rejected at validation time before the body is parsed, so the upstream client sees only the generic body-missing error. Per the OpenAI spec, omitting tool_choice when tools is non-empty is functionally equivalent to "auto" — the server defaults to auto-pick. Adding a stripAutoToolChoice patcher to the buildOpenAiApi chain: - changes nothing on compliant servers (OpenAI / DashScope / DeepSeek / Kimi default to auto when tools are present) - unblocks strict OpenAI-compatible self-hosted endpoints Explicit values other than "auto" ({"none", "required", or a function descriptor}) are passed through unchanged. Run on both chatCompletionEntity and chatCompletionStream paths so both buffered and streaming calls benefit. --- .../vip/mate/agent/AgentGraphBuilder.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java b/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java index 1462e5d1..49f5c421 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java @@ -1058,6 +1058,7 @@ public class AgentGraphBuilder { chatRequest = sanitizeReasoningEffortForProvider(chatRequest, provider); chatRequest = patchReasoningContent(chatRequest, provider); chatRequest = stripReasoningEffortIfIncompatible(chatRequest); + chatRequest = stripAutoToolChoice(chatRequest); chatRequest = patchVideoMediaContent(chatRequest); if (kimiSearchEnabled) { chatRequest = injectKimiWebSearch(chatRequest); @@ -1078,6 +1079,7 @@ public class AgentGraphBuilder { chatRequest = sanitizeReasoningEffortForProvider(chatRequest, provider); chatRequest = patchReasoningContent(chatRequest, provider); chatRequest = stripReasoningEffortIfIncompatible(chatRequest); + chatRequest = stripAutoToolChoice(chatRequest); chatRequest = patchVideoMediaContent(chatRequest); if (kimiSearchEnabled) { chatRequest = injectKimiWebSearch(chatRequest); @@ -1857,6 +1859,65 @@ public class AgentGraphBuilder { return family.isThinking(); } + /** + * Strip {@code tool_choice="auto"} from outbound chat-completion requests. + * + *

Per the OpenAI spec, omitting {@code tool_choice} when {@code tools} is non-empty + * is functionally equivalent to {@code "auto"} (the server defaults to auto-pick). + * Stripping the explicit literal {@code "auto"}: + *

+ * + *

Explicit values other than {@code "auto"} ({@code "none"}, {@code "required"}, + * or a specific function descriptor) are passed through unchanged. + */ + private static OpenAiApi.ChatCompletionRequest stripAutoToolChoice(OpenAiApi.ChatCompletionRequest request) { + Object tc = request.toolChoice(); + if (tc == null || !"auto".equals(String.valueOf(tc))) { + return request; + } + return new OpenAiApi.ChatCompletionRequest( + request.messages(), + request.model(), + request.store(), + request.metadata(), + request.frequencyPenalty(), + request.logitBias(), + request.logprobs(), + request.topLogprobs(), + request.maxTokens(), + request.maxCompletionTokens(), + request.n(), + request.outputModalities(), + request.audioParameters(), + request.presencePenalty(), + request.responseFormat(), + request.seed(), + request.serviceTier(), + request.stop(), + request.stream(), + request.streamOptions(), + request.temperature(), + request.topP(), + request.tools(), + null, // toolChoice — strip "auto" so strict OpenAI-compatible servers accept the request + request.parallelToolCalls(), + request.user(), + request.reasoningEffort(), + request.webSearchOptions(), + request.verbosity(), + request.promptCacheKey(), + request.safetyIdentifier(), + request.extraBody() + ); + } + /** * 将 Spring AI 错误地序列化为 image_url 的视频内容块转换为 video_url 格式。 *