mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 03:55:09 +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 = sanitizeReasoningEffortForProvider(chatRequest, provider);
|
||||||
chatRequest = patchReasoningContent(chatRequest, provider);
|
chatRequest = patchReasoningContent(chatRequest, provider);
|
||||||
chatRequest = stripReasoningEffortIfIncompatible(chatRequest);
|
chatRequest = stripReasoningEffortIfIncompatible(chatRequest);
|
||||||
|
chatRequest = stripAutoToolChoice(chatRequest);
|
||||||
chatRequest = patchVideoMediaContent(chatRequest);
|
chatRequest = patchVideoMediaContent(chatRequest);
|
||||||
if (kimiSearchEnabled) {
|
if (kimiSearchEnabled) {
|
||||||
chatRequest = injectKimiWebSearch(chatRequest);
|
chatRequest = injectKimiWebSearch(chatRequest);
|
||||||
@ -1078,6 +1079,7 @@ public class AgentGraphBuilder {
|
|||||||
chatRequest = sanitizeReasoningEffortForProvider(chatRequest, provider);
|
chatRequest = sanitizeReasoningEffortForProvider(chatRequest, provider);
|
||||||
chatRequest = patchReasoningContent(chatRequest, provider);
|
chatRequest = patchReasoningContent(chatRequest, provider);
|
||||||
chatRequest = stripReasoningEffortIfIncompatible(chatRequest);
|
chatRequest = stripReasoningEffortIfIncompatible(chatRequest);
|
||||||
|
chatRequest = stripAutoToolChoice(chatRequest);
|
||||||
chatRequest = patchVideoMediaContent(chatRequest);
|
chatRequest = patchVideoMediaContent(chatRequest);
|
||||||
if (kimiSearchEnabled) {
|
if (kimiSearchEnabled) {
|
||||||
chatRequest = injectKimiWebSearch(chatRequest);
|
chatRequest = injectKimiWebSearch(chatRequest);
|
||||||
@ -1857,6 +1859,65 @@ public class AgentGraphBuilder {
|
|||||||
return family.isThinking();
|
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 格式。
|
* 将 Spring AI 错误地序列化为 image_url 的视频内容块转换为 video_url 格式。
|
||||||
* <p>
|
* <p>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user