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

  1. Use one of the supported type values in the JSON: "function", "auto", "required", or "none" (all lowercase).
  2. For forcing a specific function, use exactly {"type":"function","function":{"name":"yourFunction"}} — the function name goes in function.name, not in type.
  3. Translate values from other providers: Anthropic "any" → "required", "tool" → {"type":"function","function":{"name":...}}.
  4. 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

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


AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11). Data as JSON: /api/errors/1ea1ce5f50983c64. Report an issue: GitHub.