Skip to content

OpenAiChatOptions where toolChoice can only be of the String type #1899

@zmwei666

Description

@zmwei666

Bug description
Error message:

{
    "error": {
        "message": "Invalid value: '{\"t...\"}}'. Supported values are: 'none', 'auto', and 'required'.",
        "type": "invalid_request_error",
        "param": "tool_choice",
        "code": "invalid_value"
    }
}

The toolChoice of OpenAiChatOptions is defined as a String, but in fact, it should be an Object. It can be a String or a Map. As defined in ToolChoiceBuilder, but now I can't set it to map.
Open ai explains the toolChoice field as follows:

Controls which (if any) tool is called by the model. none means the model will not call any tool and instead generates a message. auto means the model can pick between generating a message or calling one or more tools. required means the model must call one or more tools. Specifying a particular tool via {"type": "function", "function": {"name": "my_function"}} forces the model to call that tool.

Reference link: https://platform.openai.com/docs/api-reference/chat/create

Environment
JDK: 21
Spring AI: 1.0.0-M4
OS: macOS 15.0.1

Steps to reproduce

@SpringBootTest
public class ToolChoiceTest {
    @Resource
    private OpenAiChatModel openAiChatModel;

    @Resource
    private ObjectMapper objectMapper;

    @Test
    void test() throws JsonProcessingException {
        Object currentWeather = OpenAiApi.ChatCompletionRequest.ToolChoiceBuilder.FUNCTION("CurrentWeather");
        OpenAiChatOptions chatOptions = OpenAiChatOptions.builder()
                .withFunctionCallbacks(
                        List.of(FunctionCallback.builder()
                                .description("Get the weather in location")
                                .function("CurrentWeather", new MockWeatherService())
                                .inputType(MockWeatherService.Request.class)
                                .build()))

//                .withToolChoice(OpenAiApi.ChatCompletionRequest.ToolChoiceBuilder.AUTO)  // ok
//                .withToolChoice(OpenAiApi.ChatCompletionRequest.ToolChoiceBuilder.NONE)  // ok
//                .withToolChoice(OpenAiApi.ChatCompletionRequest.ToolChoiceBuilder.FUNCTION("CurrentWeather"))  // compilation failed
                .withToolChoice(objectMapper.writeValueAsString(currentWeather)) // NonTransientAiException: 400
                // request body: {"messages":[{"content":"What is the temperature in Shanghai now?","role":"user"}],"model":"gpt-4o","stream":false,"temperature":0.7,"tools":[{"type":"function","function":{"description":"Get the weather in location","name":"CurrentWeather","parameters":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"location":{"type":"string"},"unit":{"type":"string","enum":["C","F"]}}}}}],"tool_choice":"{\"type\":\"function\",\"function\":{\"name\":\"CurrentWeather\"}}"}
                // response body: {"error":{"message":"Invalid value: '{\"t...\"}}'. Supported values are: 'none', 'auto', and 'required'.","type":"invalid_request_error","param":"tool_choice","code":"invalid_value"}}

                .build();

        chatOptions.setProxyToolCalls(true);
        ChatResponse chatResponse = openAiChatModel.call(new Prompt("What is the temperature in Shanghai now?", chatOptions));
        System.out.println(chatResponse.getResult().getOutput());

    }


    public static class MockWeatherService implements Function<MockWeatherService.Request, MockWeatherService.Response> {

        public enum Unit { C, F }
        public record Request(String location, Unit unit) {}
        public record Response(double temp, Unit unit) {}

        public Response apply(Request request) {
            return new Response(30.0, Unit.C);
        }
    }
}

From the request body, it can be seen that tool_choice is serialized:

"tool_choice":"{\"type\":\"function\",\"function\":{\"name\":\"CurrentWeather\"}

open ai response:

{
    "error": {
        "message": "Invalid value: '{\"t...\"}}'. Supported values are: 'none', 'auto', and 'required'.",
        "type": "invalid_request_error",
        "param": "tool_choice",
        "code": "invalid_value"
    }
}

Expected behavior
Change the toolChoice of OpenAiChatOptions to Object type

After the change
After I changed the toolChoice of OpenAiChatOptions to type Object, the following code worked:

@Test
void test() throws JsonProcessingException {
    OpenAiChatOptions chatOptions = OpenAiChatOptions.builder()
            .withFunctionCallbacks(
                    List.of(FunctionCallback.builder()
                            .description("Get the weather in location")
                            .function("CurrentWeather", new MockWeatherService())
                            .inputType(MockWeatherService.Request.class)
                            .build()))

            .withToolChoice(OpenAiApi.ChatCompletionRequest.ToolChoiceBuilder.FUNCTION("CurrentWeather"))  // ok
            // request body: {"messages":[{"content":"What is the temperature in Shanghai now?","role":"user"}],"model":"gpt-4o","stream":false,"temperature":0.7,"tools":[{"type":"function","function":{"description":"Get the weather in location","name":"CurrentWeather","parameters":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"location":{"type":"string"},"unit":{"type":"string","enum":["C","F"]}}}}}],"tool_choice":{"function":{"name":"CurrentWeather"},"type":"function"}}
            // response body: {"id":"chatcmpl-AcvLRP5ikO8N2ln9DVY8ZbBuhHwHw","object":"chat.completion","created":1733840261,"model":"gpt-4o-2024-08-06","choices":[{"index":0,"message":{"role":"assistant","content":null,"tool_calls":[{"id":"call_VPXdnJqOEbUWmrk1WEGfTPlE","type":"function","function":{"name":"CurrentWeather","arguments":"{\"location\":\"Shanghai\"}"}}],"refusal":null},"logprobs":null,"finish_reason":"stop"}],"usage":{"prompt_tokens":70,"completion_tokens":5,"total_tokens":75,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"system_fingerprint":"fp_9d50cd990b"}

            .build();

    chatOptions.setProxyToolCalls(true);
    ChatResponse chatResponse = openAiChatModel.call(new Prompt("What is the temperature in Shanghai now?", chatOptions));
    System.out.println(chatResponse.getResult().getOutput());

}

After changing toolChoice to type Object, I can use ToolChoiceBuilder directly

OpenAiApi.ChatCompletionRequest.ToolChoiceBuilder.FUNCTION("CurrentWeather")

In the final request body, toolChoice is:

"tool_choice":{"function":{"name":"CurrentWeather"},"type":"function"}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions