spring-projects/spring-ai · error · IllegalArgumentException
Unknown tool_choice type:
Error message
Unknown tool_choice type:
What it means
parseToolChoice maps a parsed toolChoice JSON node to the OpenAI SDK's ChatCompletionToolChoiceOption. It reads the "type" field and supports only "function", "auto", "required", and "none"; any other value hits the default branch and throws this IllegalArgumentException. It runs when toolChoice was supplied as a JSON string (not a keyword or SDK object) and the JSON parsed successfully but its type discriminator is not one of the four known values.
Source
Thrown at models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatModel.java:974
String type = node.get("type").asString();
switch (type) {
case "function":
String functionName = node.get("function").get("name").asString();
ChatCompletionNamedToolChoice.Function func = ChatCompletionNamedToolChoice.Function.builder()
.name(functionName)
.build();
ChatCompletionNamedToolChoice named = ChatCompletionNamedToolChoice.builder().function(func).build();
return ChatCompletionToolChoiceOption.ofNamedToolChoice(named);
case "auto":
// There is a built-in “auto” option — but how to get it depends on SDK
// version
return ChatCompletionToolChoiceOption.ofAuto(ChatCompletionToolChoiceOption.Auto.AUTO);
case "required":
return ChatCompletionToolChoiceOption.ofAuto(ChatCompletionToolChoiceOption.Auto.REQUIRED);
case "none":
return ChatCompletionToolChoiceOption.ofAuto(ChatCompletionToolChoiceOption.Auto.NONE);
default:
throw new IllegalArgumentException("Unknown tool_choice type: " + type);
}
}
private String fromAudioData(Object audioData) {
if (audioData instanceof byte[] bytes) {
return Base64.getEncoder().encodeToString(bytes);
}
throw new IllegalArgumentException("Unsupported audio data type: " + audioData.getClass().getSimpleName());
}
private String fromMediaData(org.springframework.util.MimeType mimeType, Object mediaContentData) {
if (mediaContentData instanceof byte[] bytes) {
// Assume the bytes are an image. So, convert the bytes to a base64 encoded
// following the prefix pattern.
return String.format("data:%s;base64,%s", mimeType.toString(), Base64.getEncoder().encodeToString(bytes));
}
else if (mediaContentData instanceof String text) {
// Assume the text is a URLs or a base64 encoded image prefixed by the user.View on GitHub (pinned to 98a7beda4f)
Solutions
- Use one of the supported type values in the JSON: "function", "auto", "required", or "none" (all lowercase).
- For forcing a specific function, use exactly {"type":"function","function":{"name":"yourFunction"}} — the function name goes in function.name, not in type.
- Translate values from other providers: Anthropic "any" → "required", "tool" → {"type":"function","function":{"name":...}}.
- Better: build a ChatCompletionToolChoiceOption with the OpenAI SDK (ofAuto / ofNamedToolChoice) and set that as toolChoice to skip JSON parsing altogether.
Example fix
// before
options.setToolChoice("{\"type\":\"tool\",\"name\":\"get_weather\"}"); // Anthropic-style, unknown type
// after
options.setToolChoice("{\"type\":\"function\",\"function\":{\"name\":\"get_weather\"}}"); Defensive patterns
Strategy: validation
Validate before calling
public static void validateToolChoiceJson(String json) throws Exception {
com.fasterxml.jackson.databind.JsonNode node =
org.springframework.ai.converter.JacksonUtils.getDefaultJsonMapper().readTree(json);
String type = node.get("type").asString();
if (!java.util.Set.of("function", "auto", "required", "none").contains(type)) {
throw new IllegalArgumentException("Unsupported tool_choice type: " + type);
}
} Type guard
boolean hasKnownToolChoiceType(String json) {
try {
String t = JacksonUtils.getDefaultJsonMapper().readTree(json).get("type").asString();
return java.util.Set.of("function", "auto", "required", "none").contains(t);
} catch (Exception e) { return false; }
} Try / catch
try {
return chatModel.call(prompt);
} catch (IllegalArgumentException e) {
if (e.getMessage() != null && e.getMessage().startsWith("Unknown tool_choice type:")) {
logger.warn("Unsupported tool_choice type, falling back to auto: {}", e.getMessage());
prompt.getOptions().setToolChoice(ChatCompletionToolChoiceOption
.ofAuto(ChatCompletionToolChoiceOption.Auto.AUTO));
return chatModel.call(prompt);
}
throw e;
} Prevention
- Use only type values "function", "auto", "required", "none" in toolChoice JSON (lowercase, exact).
- Translate provider-specific variants before use: Anthropic "any" → "required", "tool" → OpenAI's function form.
- Put the function name in function.name, never as the type discriminator.
- Build ChatCompletionToolChoiceOption via the SDK instead of hand-written JSON to eliminate this class of error.
When it happens
Trigger: Setting OpenAiChatOptions.toolChoice to a JSON string whose "type" field is something else — e.g. {"type":"named_tool_choice",...}, {"type":"any"} (Anthropic-style), {"type":"tool"} — or omitting structure so node.get("type") yields an unexpected value, then invoking the model so createRequest parses it.
Common situations: Copying tool_choice JSON from another provider's SDK (Anthropic uses {"type":"any"} or {"type":"tool","name":...}); writing a custom discriminator like "specific_function"; typos such as "autuo" or "Auto" (case-sensitive); embedding the function name at the wrong level instead of inside function.name.
Understand the failure class
Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.
Related errors
- Failed to parse toolChoice JSON:
- Unsupported message type:
- Unsupported response format type:
- Unsupported media type: . Supported types are: images (image
- Unsupported image type: . Supported types: image/png, image/
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/1ea1ce5f50983c64.
Report an issue: GitHub.