spring-projects/spring-ai · error · ToolCallLimitExceededException

Tool call limit exceeded (dynamic message: tool name and lim

Error message

Tool call limit exceeded (dynamic message: tool name and limit)

What it means

Spring AI's DefaultToolCallingManager enforces a per-tool call limit (ToolCallingChatOptions maxCalls). When ToolCallLimitBehavior.THROW_EXCEPTION is configured and a tool exceeds its allowed number of invocations, the manager throws ToolCallLimitExceededException carrying the tool name, the limit, and a partial ToolExecutionResult built from the conversation history up to that point. It exists to stop runaway LLM tool-calling loops.

Source

Thrown at spring-ai-model/src/main/java/org/springframework/ai/model/tool/DefaultToolCallingManager.java:262

			totalToolCallCount++;
			int toolCallCount = toolCallCounts.merge(toolName, 1, Integer::sum);

			ToolCallLimits.Breach limitBreach = this.toolCallLimits.check(toolName, toolCallCount, totalToolCallCount);
			if (limitBreach != null) {
				toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), toolName, limitBreach.message()));

				if (this.toolCallLimits.onLimitExceeded() == ToolCallLimitBehavior.THROW) {
					ToolResponseMessage partialToolResponseMessage = ToolResponseMessage.builder()
						.responses(toolResponses)
						.build();
					List<Message> partialConversationHistory = buildConversationHistoryAfterToolExecution(
							prompt.getInstructions(), assistantMessage, partialToolResponseMessage);
					ToolExecutionResult partialToolExecutionResult = ToolExecutionResult.builder()
						.conversationHistory(partialConversationHistory)
						.returnDirect(Objects.requireNonNullElse(returnDirect, false))
						.build();
					throw new ToolCallLimitExceededException(limitBreach.toolName(), limitBreach.limit(),
							partialToolExecutionResult);
				}

				// ToolCallLimitBehavior.RETURN_ERROR_RESPONSE: skip invoking this tool
				// call but keep processing the rest of the batch.
				continue;
			}

			String toolInputArguments = toolCall.arguments();

			// Handle the possible null parameter situation in streaming mode.
			final String finalToolInputArguments;
			if (!StringUtils.hasText(toolInputArguments)) {
				if (logger.isWarnEnabled()) {
					logger.warn("Tool call arguments are null or empty for tool: " + toolName
							+ ". Using empty JSON object as default.");
				}
				finalToolInputArguments = "{}";

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Raise the per-tool call limit (e.g. toolCallLimit("myTool", 10)) so legitimate workflows complete
  2. Change the limit behavior to ToolCallLimitBehavior.RETURN_ERROR_RESPONSE so the model receives an error response instead of an exception
  3. Catch ToolCallLimitExceededException and use getResult().conversationHistory() from the partial result to continue or summarize
  4. Improve tool responses/descriptions so the model stops re-invoking the tool

Example fix

// before
ChatOptions options = ToolCallingChatOptions.builder()
    .toolCallbacks(tools)
    .toolCallLimit("search", 2, ToolCallLimitBehavior.THROW_EXCEPTION)
    .build();
// after
ChatOptions options = ToolCallingChatOptions.builder()
    .toolCallbacks(tools)
    .toolCallLimit("search", 10, ToolCallLimitBehavior.RETURN_ERROR_RESPONSE)
    .build();
Defensive patterns

Strategy: try-catch

Validate before calling

// Before calling the model, ensure limits are sensible
ToolCallingChatOptions opts = ...;
if (opts.getToolCallLimit() != null && opts.getToolCallLimit() < expectedMaxToolCalls) {
    throw new IllegalArgumentException("tool call limit too low for this workflow");
}

Try / catch

try {
    ChatResponse resp = chatModel.call(prompt);
} catch (ToolCallLimitExceededException e) {
    // inspect e.getToolName(), e.getLimit(); continue from e.getResult().conversationHistory()
    logger.warn("Tool {} hit call limit {}", e.getToolName(), e.getLimit());
}

Prevention

When it happens

Trigger: Calling ChatClient/ChatModel with ToolCallingChatOptions where a tool limit is registered via toolCallLimit(toolName, maxCalls) with behavior THROW_EXCEPTION, and the model then requests that tool more than maxCalls times during internalToolExecutionResult processing; executeToolCall detects the breach via limitBreach and throws.

Common situations: LLMs entering tool-calling loops (repeatedly calling a search or fetch tool because results don't satisfy them), setting maxCalls too low for legitimate multi-step workflows, or agents with recursive/self-referencing tools.

Related errors


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