spring-projects / spring-projects/spring-ai

Cannot retrieve the called tool from ChatClientResponse, but the return value inside ChatClientResponse contains the toolCalls field

Open
#3,405 2 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

status: waiting-for-triage
Dominant language
Java
Stars
9.5k
Forks
2.9k
Avg merge
1d 7h
Merged PRs (30d)
6

Description

I implemented a custom log listener through a custom Advisor and added the Advisor to the currently used ChatClient using the ChatClient. builder (chatmodel). defaultAdvisors() method.
Subsequently, two tool methods were declared using @ Tool and encapsulated into a class. The class was declared as a Bean using @ Component and automatically injected into my ChatClient calling tool using @ Autowired

This is the Advisor I wrote
`
@Slf4j
@Component
public class CustomLoggerAdvisor implements CallAdvisor, StreamAdvisor {
@Override
public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {
logRequest(chatClientRequest);
ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(chatClientRequest);
logResponse(chatClientResponse);
return chatClientResponse;
}

@Override
public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain) {
    logRequest(chatClientRequest);
    Flux<ChatClientResponse> chatClientResponses = streamAdvisorChain.nextStream(chatClientRequest);

    return new ChatClientMessageAggregator().aggregateChatClientResponse(chatClientResponses, this::logResponse);
}

private void logRequest(ChatClientRequest request) {
    System.out.println("CustomLoggerAdvisor get to request");
    String system = request.prompt().getSystemMessage().getText();
    List<String> userList = request.prompt().getUserMessages().stream().map(g -> StrUtil.isNotBlank(g.getText()) ? g.getText() : "无响应内容").toList();
    StringBuilder sb = new StringBuilder();
    for (String s : userList) {
        sb.append(s).append("\n");
    }
    OpenAiChatOptions copy = request.prompt().getOptions().copy();
    List<ToolCallback> toolCallbacks = copy.getToolCallbacks();
    if (toolCallbacks != null && !toolCallbacks.isEmpty()) {
        List<String> list = toolCallbacks.stream().map(t -> t.getToolDefinition().name()).toList();
        log.warn("Existence Tool Set: {}", list);
    }
    log.warn("System prompt word: {}", system);
    log.warn("User Input: {}", sb);
}

private void logResponse(ChatClientResponse chatClientResponse) {
    System.out.println("CustomLoggerAdvisor get response");
    String model = chatClientResponse.chatResponse().getMetadata().getModel();

    Integer promptTokens = chatClientResponse.chatResponse().getMetadata().getUsage().getPromptTokens();
    Integer completionTokens = chatClientResponse.chatResponse().getMetadata().getUsage().getCompletionTokens();
    Integer totalTokens = chatClientResponse.chatResponse().getMetadata().getUsage().getTotalTokens();

    ChatResponse chatResponse = chatClientResponse.chatResponse();

    //Unable to get the tool called
    List<Generation> results = chatResponse.getResults();
    log.error("Model Response Results:{}", results);

    log.warn("response model: {}", model);
    log.warn("Token consumption data: Request consumption:{},Response consumption:{},Total consumption:{}", promptTokens, completionTokens, totalTokens);
    List<String> list = chatClientResponse.chatResponse().getResults().stream().map(g -> StrUtil.isNotBlank(g.getOutput().getText()) ? g.getOutput().getText() : "No responsive content").toList();
    StringBuilder sb = new StringBuilder();
    for (String s : list) {
        sb.append(s).append("\n");
    }
    log.warn("Model response content:{}", sb);
}

@Override
public String getName() {
    return this.getClass().getName();
}

@Override
public int getOrder() {
    return 10000;
}

}

This is the Tool class I wrote
`
@Slf4j
@Component
public class calculateTool {

@Tool(name = "addOperation", description = "addition operation")
public Integer addOperation(@ToolParam(description = "Add") Integer a, @ToolParam(description = "Agend") Integer b) {
    log.warn("Addition tool called");
    return a + b;
}

@Tool(name = "mulOperation", description = "multiplication operation")
public Integer mulOperation(@ToolParam(description = "Multiplier") Integer a, @ToolParam(description = "Multiplier") Integer b) {
    log.warn("The multiplication tool is called");
    return a * b;
}

}`

This is the way to declare the use of Advisor
ChatClient.builder(chatmodel).defaultAdvisors(customLoggerAdvisor).build();

This is the way to declare the use of Tools
ChatResponse response = chatClient.prompt().system(prompts.testPrompt).user("Please calculate" + a + "+" + b + "=").tools(calculateTool).call().chatResponse();

This is the log of key positions
`
2025-06-01T11:14:24.017+08:00 INFO 13520 --- [advisor] [)-192.168.1.103] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
2025-06-01T11:14:34.797+08:00 DEBUG 13520 --- [advisor] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : GET "/advisor/mul?a=0&b=0", parameters={masked}
2025-06-01T11:14:34.801+08:00 DEBUG 13520 --- [advisor] [nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.example.advisor.controller.advisorController#mul(int, int)
CustomLoggerAdvisor get to request
2025-06-01T11:14:34.892+08:00 WARN 13520 --- [advisor] [nio-8080-exec-1] o.e.advisor.advisor.CustomLoggerAdvisor : Existence Tool Set: [addOperation, mulOperation]
2025-06-01T11:14:34.892+08:00 WARN 13520 --- [advisor] [nio-8080-exec-1] o.e.advisor.advisor.CustomLoggerAdvisor : System prompt word: You are a proxy calculator please answer user questions correctly.

2025-06-01T11:14:34.892+08:00 WARN 13520 --- [advisor] [nio-8080-exec-1] o.e.advisor.advisor.CustomLoggerAdvisor : User Input: Please calculate0x0=

2025-06-01T11:14:34.990+08:00 DEBUG 13520 --- [advisor] [nio-8080-exec-1] o.s.web.client.DefaultRestClient : Writing [ChatCompletionRequest[messages=[ChatCompletionMessage[rawContent=You are a proxy calculator please answer user questions correctly.
, role=SYSTEM, name=null, toolCallId=null, toolCalls=null, refusal=null, audioOutput=null, annotations=null], ChatCompletionMessage[rawContent=Please calculate0x0=, role=USER, name=null, toolCallId=null, toolCalls=null, refusal=null, audioOutput=null, annotations=null]], model=deepseek-reasoner, store=null, metadata=null, frequencyPenalty=null, logitBias=null, logprobs=null, topLogprobs=null, maxTokens=null, maxCompletionTokens=null, n=null, outputModalities=null, audioParameters=null, presencePenalty=null, responseFormat=null, seed=null, serviceTier=null, stop=null, stream=false, streamOptions=null, temperature=0.7, topP=null, tools=[org.springframework.ai.openai.api.OpenAiApi$FunctionTool@6b869f45, org.springframework.ai.openai.api.OpenAiApi$FunctionTool@32e34c5e], toolChoice=null, parallelToolCalls=null, user=null, reasoningEffort=null, webSearchOptions=null]] as "application/json" with org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
2025-06-01T11:14:43.730+08:00 DEBUG 13520 --- [advisor] [nio-8080-exec-1] o.s.web.client.DefaultRestClient : Reading to [org.springframework.ai.openai.api.OpenAiApi$ChatCompletion]
2025-06-01T11:14:43.745+08:00 WARN 13520 --- [advisor] [nio-8080-exec-1] o.example.advisor.tools.calculateTool : The multiplication tool is called
2025-06-01T11:14:43.752+08:00 DEBUG 13520 --- [advisor] [nio-8080-exec-1] o.s.web.client.DefaultRestClient : Writing [ChatCompletionRequest[messages=[ChatCompletionMessage[rawContent=You are a proxy calculator please answer user questions correctly.
, role=SYSTEM, name=null, toolCallId=null, toolCalls=null, refusal=null, audioOutput=null, annotations=null], ChatCompletionMessage[rawContent=Please calculate0x0=, role=USER, name=null, toolCallId=null, toolCalls=null, refusal=null, audioOutput=null, annotations=null], ChatCompletionMessage[rawContent=I will calculate the expression "0x0" for you. Since "x" often represents multiplication in mathematical contexts, I'll interpret this as 0 multiplied by 0., role=ASSISTANT, name=null, toolCallId=null, toolCalls=[ToolCall[index=null, id=call_0_75a4f53f-7a49-48ca-bf05-97450a732eec, type=function, function=ChatCompletionFunction[name=mulOperation, arguments={"a": 0, "b": 0}]]], refusal=null, audioOutput=null, annotations=null], ChatCompletionMessage[rawContent=0, role=TOOL, name=mulOperation, toolCallId=call_0_75a4f53f-7a49-48ca-bf05-97450a732eec, toolCalls=null, refusal=null, audioOutput=null, annotations=null]], model=deepseek-reasoner, store=null, metadata=null, frequencyPenalty=null, logitBias=null, logprobs=null, topLogprobs=null, maxTokens=null, maxCompletionTokens=null, n=null, outputModalities=null, audioParameters=null, presencePenalty=null, responseFormat=null, seed=null, serviceTier=null, stop=null, stream=false, streamOptions=null, temperature=0.7, topP=null, tools=[org.springframework.ai.openai.api.OpenAiApi$FunctionTool@5004df99, org.springframework.ai.openai.api.OpenAiApi$FunctionTool@73b883ce], toolChoice=null, parallelToolCalls=null, user=null, reasoningEffort=null, webSearchOptions=null]] as "application/json" with org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
2025-06-01T11:14:51.829+08:00 DEBUG 13520 --- [advisor] [nio-8080-exec-1] o.s.web.client.DefaultRestClient : Reading to [org.springframework.ai.openai.api.OpenAiApi$ChatCompletion]
CustomLoggerAdvisor get response
2025-06-01T11:32:54.386+08:00 ERROR 13520 --- [advisor] [nio-8080-exec-1] o.e.advisor.advisor.CustomLoggerAdvisor : Model Response Results:[Generation[assistantMessage=AssistantMessage [messageType=ASSISTANT, toolCalls=[], textContent=The result of multiplying 0 by 0 is:

0 × 0 = 0, metadata={role=ASSISTANT, messageType=ASSISTANT, finishReason=STOP, refusal=, index=0, annotations=[], id=4986adcf-4814-4ce8-99ef-864c0dfa1eb4}], chatGenerationMetadata=DefaultChatGenerationMetadata[finishReason='STOP', filters=0, metadata=0]]]
2025-06-01T11:32:54.386+08:00 WARN 13520 --- [advisor] [nio-8080-exec-1] o.e.advisor.advisor.CustomLoggerAdvisor : response model: deepseek-reasoner
2025-06-01T11:32:54.386+08:00 WARN 13520 --- [advisor] [nio-8080-exec-1] o.e.advisor.advisor.CustomLoggerAdvisor : Token consumption data: Request consumption:747,Response consumption:234,Total consumption:981
2025-06-01T11:32:54.394+08:00 WARN 13520 --- [advisor] [nio-8080-exec-1] o.e.advisor.advisor.CustomLoggerAdvisor : Model response content:The result of multiplying 0 by 0 is:

0 × 0 = 0

2025-06-01T11:32:54.413+08:00 DEBUG 13520 --- [advisor] [nio-8080-exec-1] m.m.a.RequestResponseBodyMethodProcessor : Using 'text/html', given [text/html, application/json, application/xhtml+xml, application/xml;q=0.9, /;q=0.8] and supported [text/plain, /, application/json, application/*+json]
2025-06-01T11:32:54.414+08:00 DEBUG 13520 --- [advisor] [nio-8080-exec-1] m.m.a.RequestResponseBodyMethodProcessor : Writing ["The result of multiplying 0 by 0 is:0 × 0 = 0"]
2025-06-01T11:32:54.420+08:00 DEBUG 13520 --- [advisor] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed 200 OK
`

This is the chatResponse. getResults () value under the logResponse method of the Customs LoggerAdvisor class in debug mode

Image

The size of the toolCalls list is 0, but according to the log printed by the console, it is not 0

After the model sends a request to the application to call the tool, the application accepts and executes the tool to return the corresponding response value. This process is all handled by the DefaultRestClient. I think this tool call should be recorded, but in chatResponse, it seems that only the toolCalls field is present but there is no value inside

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by tracing the tool-call flow from ChatClientResponse.chatResponse().getResults() through DefaultRestClient, comparing the intermediate response shown in the logs with the final ChatResponse observed by CustomLoggerAdvisor. Examine how toolCalls are represented across the response cycle, and verify the fix by reproducing the addOperation or mulOperation example and confirming the tool call remains available in the advisor response.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, spring
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.