mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(agent): strip tool_choice="auto" so strict OpenAI-compatible servers accept the request
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.
This commit is contained in:
parent
03c8584910
commit
4898b79d49
@ -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.
|
||||
*
|
||||
* <p>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"}:
|
||||
* <ul>
|
||||
* <li>does not change behavior on compliant servers (e.g. OpenAI, DashScope) — they
|
||||
* still default to auto when tools are present</li>
|
||||
* <li>unblocks strict OpenAI-compatible self-hosted serving frameworks that reject
|
||||
* {@code tool_choice="auto"} at request validation time unless launched with an
|
||||
* auto-tool-choice opt-in flag, which is a common reason custom endpoints
|
||||
* respond with a generic 400 / "body=None" Pydantic error</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>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 格式。
|
||||
* <p>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user