alibaba/spring-ai-alibaba · warning
The model call returned an exception message: {}
Error message
The model call returned an exception message: {} What it means
ModelRetryInterceptor.interceptModel inspects the model's returned message text and, if it starts with 'Exception:' (an exception previously captured and serialized by AgentLlmNode instead of thrown), logs this warning and treats it as a failure: if attempts remain and isRetryableExceptionMessage() accepts the text, it records a synthetic RuntimeException and retries after the current backoff delay. It exists because some paths deliver model exceptions as response text rather than thrown exceptions.
Source
Thrown at spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/interceptor/modelretry/ModelRetryInterceptor.java:95
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
if (attempt > 1) {
log.info("Retry model call, on the {}th attempt (out of {} attempts).", attempt, maxAttempts);
}
ModelResponse modelResponse = handler.call(request);
Object messagePayload = modelResponse.getMessage();
if (messagePayload instanceof Flux<?> responseFlux) {
return ModelResponse.of(withStreamingRetry(request, handler, castChatResponseFlux(responseFlux), attempt, currentDelay));
}
if (!(messagePayload instanceof Message message)) {
return modelResponse;
}
// Check if the response contains any exception information (exceptions captured from AgentLlmNode).
if (message != null && message.getText() != null && message.getText().startsWith("Exception:")) {
String exceptionText = message.getText();
log.warn("The model call returned an exception message: {}", exceptionText);
// Extract anomaly information from the text and determine whether a retry is possible.
if (attempt < maxAttempts && isRetryableExceptionMessage(exceptionText)) {
lastException = new RuntimeException(exceptionText);
// Wait and try again
if (currentDelay > 0) {
try {
log.info("Retry after {} ms", currentDelay);
Thread.sleep(currentDelay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Retry interrupted", e);
}
}
// Calculate the next delay time (exponential backoff)
currentDelay = Math.min((long) (currentDelay * backoffMultiplier), maxDelay);
continue;
} else if (attempt >= maxAttempts) {View on GitHub (pinned to f82da0b50f)
Solutions
- Inspect the logged exceptionText to identify the real upstream error and fix its root cause (auth, rate limit, context length)
- Customize the retryable predicate so only genuinely transient exception messages trigger retries
- Reduce total attempts/backoff if the underlying error is permanent (it will never succeed on retry)
- Ensure AgentLlmNode-side errors are surfaced as exceptions where possible so retry logic sees real failure signals
Example fix
// before
ModelRetryInterceptor.of(model); // default predicate retries on everything transient-looking
// after
ModelRetryInterceptor interceptor = ModelRetryInterceptor.builder()
.chatModel(model)
.maxAttempts(3)
.retryablePredicate(msg -> msg.startsWith("Exception: rate limit")
|| msg.startsWith("Exception: timeout"))
.build(); Defensive patterns
Strategy: retry
Validate before calling
String text = message.getText();
boolean embeddedException = text != null && text.startsWith("Exception:");
boolean retryable = embeddedException && isTransient(text); Type guard
boolean isEmbeddedException(org.springframework.ai.chat.messages.AssistantMessage m) {
return m != null && m.getText() != null && m.getText().startsWith("Exception:");
} Try / catch
try { return interceptor.interceptModel(request, chain); }
catch (RuntimeException e) { throw new ModelInvocationException("Retries exhausted on embedded exception", e); } Prevention
- Tune the retryable predicate to only transient messages (rate limit, timeout)
- Cap maxAttempts and backoff so permanent errors fail fast
- Eliminate upstream paths that serialize exceptions into message text
- Alert on this warning's frequency — repeated embedded exceptions indicate a broken provider path
When it happens
Trigger: A model call 'succeeds' but its output text is an 'Exception: ...' string produced upstream (AgentLlmNode captured the exception), and the retry policy decides whether it is retryable; logs each time such an embedded exception is detected.
Common situations: Provider returning error payloads that get flattened into message text; rate-limit or timeout messages surfaced as text from a wrapped client; streaming responses containing serialized error bodies; prompts exceeding context limits reported as text.
Understand the failure class
Background: "API request failed": what wrapped HTTP errors from external APIs mean and how to find the real cause — this error's family across 29 libraries.
Related errors
- Model call failed (attempted {}/{}): {}
- Primary model failed: {}
- Fallback model {} failed: {}
- Exceptions cannot be retried and are thrown immediately: {}
- Tool '{}' failed (attempt {}/{}), retrying in {}ms: {}
AI-assisted analysis of alibaba/spring-ai-alibaba@f82da0b50f (2026-09-09).
Data as JSON: /api/errors/b21278ff8dea477c.
Report an issue: GitHub.