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.

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.

Appendix: 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)