Bug description

I created a custom ChatClient Bean (instead of declaring the model configuration in the application.yml) and autowired it into my Controller. Everything worked fine before I used this ChatClient to invoke functions in the model. However, as soon as I used it to call a function in the model, an error like java.lang.IllegalStateException: No function callback found for name: weatherByCity occurred. It doesn't work no matter how I try to call it. I spent an entire day troubleshooting it.

In the end, I found that when I define the model, key, and base-url in application.yml, it works.

I'm sorry this might be a silly question.But I’m not sure why this happens. Is this a bug?



Environment

JDK: OpenJDK-21.0.3 SpringBoot: 3.4.2-SNAPSHOT Spring-ai: 1.0.0-SNAPSHOT

Steps to reproduce

As shown below, when the weatherByCity function is called by AI, an IllegalArgumentException will be thrown

Minimal Complete Reproducible example

@Configuration
public class ModelConfig {
  @Bean
  public ChatClient myClient() {
    OpenAiApi openAiApi = new OpenAiApi(MY_BASE_URL, MY_KEY);
    OpenAiChatOptions openAiChatOptions = new OpenAiChatOptions();
    openAiChatOptions.setModel("gpt-4o");
    openAiChatOptions.setStreamOptions(new OpenAiApi.ChatCompletionRequest.StreamOptions(true));
    OpenAiChatModel chatModel = new OpenAiChatModel(openAiApi, openAiChatOptions);
    return ChatClient.builder(chatModel).build();
  }
  }


  @RestController
public class ChatController {

  @Autowired
  @Qualifier("myClient")
  private ChatClient chatClient;

  @GetMapping("/chat/function")
  String chat(String cityName) {
    var userPromptTemplate = "What's the weather like in {cityName} now?";
    return chatClient.prompt()
        .user(userSpec -> userSpec
            .text(userPromptTemplate)
            .param("cityName",cityName )
        )
        .functions("weatherByCity")
        .call()
        .content();
  }


@Configuration(proxyBeanMethods = false)
public class Functions {

    @Bean
    @Description("Get weather based on the input city name")
    public Function<WeatherService.City,WeatherService.WeatherInfo> weatherByCity(WeatherService weatherService){
        return weatherService::getCityWeather;
    }
}

@Service
public class WeatherService {

  private String URL = "***";
  private RestClient restClient;

  public WeatherService() {
    this.restClient = RestClient.create(URL);
  }

  public record City(String city){}
  public record WeatherInfo(String city,Data data){}
  public record Data(String date,Condition night,String high,String low){}
  public record Condition(String type,String fengli){}


  public WeatherInfo getCityWeather(City cityName) {
    System.out.println("request is :"+cityName);
    WeatherInfo response = restClient.get()
        .uri("/weather?city={city}", cityName.city())
        .retrieve()
        .body(WeatherInfo.class);
    System.out.println("response is :"+response);
    return response;
  }
}

Logger

java.lang.IllegalStateException: No function callback found for name: weatherByCity
    at org.springframework.ai.chat.model.AbstractToolCallSupport.resolveFunctionCallbacks(AbstractToolCallSupport.java:197) ~[spring-ai-core-1.0.0-20241224.161216-1116.jar:1.0.0-SNAPSHOT]
    at org.springframework.ai.openai.OpenAiChatModel.getFunctionTools(OpenAiChatModel.java:643) ~[spring-ai-openai-1.0.0-20241224.161216-1038.jar:1.0.0-SNAPSHOT]
    at org.springframework.ai.openai.OpenAiChatModel.createRequest(OpenAiChatModel.java:591) ~[spring-ai-openai-1.0.0-20241224.161216-1038.jar:1.0.0-SNAPSHOT]
    at org.springframework.ai.openai.OpenAiChatModel.internalCall(OpenAiChatModel.java:225) ~[spring-ai-openai-1.0.0-20241224.161216-1038.jar:1.0.0-SNAPSHOT]
    at org.springframework.ai.openai.OpenAiChatModel.call(OpenAiChatModel.java:220) ~[spring-ai-openai-1.0.0-20241224.161216-1038.jar:1.0.0-SNAPSHOT]
    at org.springframework.ai.chat.client.DefaultChatClient$DefaultChatClientRequestSpec$1.aroundCall(DefaultChatClient.java:675) ~[spring-ai-core-1.0.0-20241224.161216-1116.jar:1.0.0-SNAPSHOT]
    at org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain.lambda$nextAroundCall$1(DefaultAroundAdvisorChain.java:98) ~[spring-ai-core-1.0.0-20241224.161216-1116.jar:1.0.0-SNAPSHOT]
    at io.micrometer.observation.Observation.observe(Observation.java:564) ~[micrometer-observation-1.14.2.jar:1.14.2]
    at org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain.nextAroundCall(DefaultAroundAdvisorChain.java:98) ~[spring-ai-core-1.0.0-20241224.161216-1116.jar:1.0.0-SNAPSHOT]
    at org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec.doGetChatResponse(DefaultChatClient.java:488) ~[spring-ai-core-1.0.0-20241224.161216-1116.jar:1.0.0-SNAPSHOT]
    at org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec.lambda$doGetObservableChatResponse$1(DefaultChatClient.java:477) ~[spring-ai-core-1.0.0-20241224.161216-1116.jar:1.0.0-SNAPSHOT]
    at io.micrometer.observation.Observation.observe(Observation.java:564) ~[micrometer-observation-1.14.2.jar:1.14.2]
    at org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec.doGetObservableChatResponse(DefaultChatClient.java:477) ~[spring-ai-core-1.0.0-20241224.161216-1116.jar:1.0.0-SNAPSHOT]
    at org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec.doGetChatResponse(DefaultChatClient.java:461) ~[spring-ai-core-1.0.0-20241224.161216-1116.jar:1.0.0-SNAPSHOT]
    at org.springframework.ai.chat.client.DefaultChatClient$DefaultCallResponseSpec.content(DefaultChatClient.java:511) ~[spring-ai-core-1.0.0-20241224.161216-1116.jar:1.0.0-SNAPSHOT]
    at com.ling.controller.ChatController.chat(ChatController.java:44) ~[classes/:na]
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:257) ~[spring-web-6.2.1.jar:6.2.1]
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:190) ~[spring-web-6.2.1.jar:6.2.1]
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.2.1.jar:6.2.1]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:986) ~[spring-webmvc-6.2.1.jar:6.2.1]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:891) ~[spring-webmvc-6.2.1.jar:6.2.1]
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.2.1.jar:6.2.1]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1088) ~[spring-webmvc-6.2.1.jar:6.2.1]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:978) ~[spring-webmvc-6.2.1.jar:6.2.1]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.2.1.jar:6.2.1]
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903) ~[spring-webmvc-6.2.1.jar:6.2.1]
    at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564) ~[tomcat-embed-core-10.1.34.jar:6.0]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.2.1.jar:6.2.1]
    at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.34.jar:6.0]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.34.jar:10.1.34]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.2.1.jar:6.2.1]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.1.jar:6.2.1]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.2.1.jar:6.2.1]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.1.jar:6.2.1]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.2.1.jar:6.2.1]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.1.jar:6.2.1]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]

Comment From: LingLambda

sorry, the issue was mistakenly closed earlier.

Comment From: LingLambda

In short, when I manually configure ChatClient or ChatClient.Builder as a Bean and use them to call the function I created, the function cannot be found. However, this issue does not occur when using autoconfiguration. During debugging, I traced the issue to line 186 of org.springframework.ai.chat.model.AbstractToolCallSupport, where functionCallbackResolver is null, causing this exception to be thrown.

Manual Configuration : {4048BC59-47C1-407B-A24F-EF69324F01CF}

Auto Configuration {C8B9F2D3-1575-402A-9D01-0C2EFCAC54EB}

Comment From: ThomasVitale

The problem seems to be when building the ChatModel instance. If you want to use the function calling capabilities, the OpenAiChatModel needs to have a FunctionCallbackResolver instance. You can either autowire one (there's an instance auto-configured by the Spring AI OpenAI Spring Boot Starter) or instantiate your own (from DefaultFunctionCallbackResolver).

@Bean
public ChatClient myClient(FunctionCallbackResolver functionCallbackResolver) {
  ...
  OpenAiChatModel chatModel = new OpenAiChatModel(openAiApi, openAiChatOptions, functionCallbackResolver, RetryUtils.DEFAULT_RETRY_TEMPLATE);
  ...
}

Comment From: LingLambda

问题似乎出在构建 ChatModel 实例时。如果要使用函数调用功能, OpenAiChatModel 需要有一个 FunctionCallbackResolver 实例。您可以自动连接一个实例(Spring AI OpenAI Spring Boot Starter 自动配置了一个实例),也可以实例化您自己的实例(从 DefaultFunctionCallbackResolver 开始)。

@Bean public ChatClient myClient(FunctionCallbackResolver functionCallbackResolver) { ... OpenAiChatModel chatModel = new OpenAiChatModel(openAiApi, openAiChatOptions, functionCallbackResolver, RetryUtils.DEFAULT_RETRY_TEMPLATE); ... }

Thank you so much for your help and explanation!