{"record":{"id":"b21278ff8dea477c","repo":"alibaba/spring-ai-alibaba","slug":"the-model-call-returned-an-exception-message","errorCode":null,"errorMessage":"The model call returned an exception message: {}","messagePattern":"The model call returned an exception message: (.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/interceptor/modelretry/ModelRetryInterceptor.java","lineNumber":95,"sourceCode":"\t\tfor (int attempt = 1; attempt <= maxAttempts; attempt++) {\n\t\t\ttry {\n\t\t\t\tif (attempt > 1) {\n\t\t\t\t\tlog.info(\"Retry model call, on the {}th attempt (out of {} attempts).\", attempt, maxAttempts);\n\t\t\t\t}\n\n\t\t\t\tModelResponse modelResponse = handler.call(request);\n\t\t\t\tObject messagePayload = modelResponse.getMessage();\n\t\t\t\tif (messagePayload instanceof Flux<?> responseFlux) {\n\t\t\t\t\treturn ModelResponse.of(withStreamingRetry(request, handler, castChatResponseFlux(responseFlux), attempt, currentDelay));\n\t\t\t\t}\n\t\t\t\tif (!(messagePayload instanceof Message message)) {\n\t\t\t\t\treturn modelResponse;\n\t\t\t\t}\n\n\t\t\t\t// Check if the response contains any exception information (exceptions captured from AgentLlmNode).\n\t\t\t\tif (message != null && message.getText() != null && message.getText().startsWith(\"Exception:\")) {\n\t\t\t\t\tString exceptionText = message.getText();\n\t\t\t\t\tlog.warn(\"The model call returned an exception message: {}\", exceptionText);\n\n\t\t\t\t\t// Extract anomaly information from the text and determine whether a retry is possible.\n\t\t\t\t\tif (attempt < maxAttempts && isRetryableExceptionMessage(exceptionText)) {\n\t\t\t\t\t\tlastException = new RuntimeException(exceptionText);\n\t\t\t\t\t\t// Wait and try again\n\t\t\t\t\t\tif (currentDelay > 0) {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tlog.info(\"Retry after {} ms\", currentDelay);\n\t\t\t\t\t\t\t\tThread.sleep(currentDelay);\n\t\t\t\t\t\t\t} catch (InterruptedException e) {\n\t\t\t\t\t\t\t\tThread.currentThread().interrupt();\n\t\t\t\t\t\t\t\tthrow new RuntimeException(\"Retry interrupted\", e);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Calculate the next delay time (exponential backoff)\n\t\t\t\t\t\tcurrentDelay = Math.min((long) (currentDelay * backoffMultiplier), maxDelay);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t} else if (attempt >= maxAttempts) {","sourceCodeStart":77,"sourceCodeEnd":113,"githubUrl":"https://github.com/alibaba/spring-ai-alibaba/blob/f82da0b50f35744c13968191be2b1cd2452ef550/spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/interceptor/modelretry/ModelRetryInterceptor.java#L77-L113","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nModelRetryInterceptor.of(model); // default predicate retries on everything transient-looking\n// after\nModelRetryInterceptor interceptor = ModelRetryInterceptor.builder()\n    .chatModel(model)\n    .maxAttempts(3)\n    .retryablePredicate(msg -> msg.startsWith(\"Exception: rate limit\")\n        || msg.startsWith(\"Exception: timeout\"))\n    .build();","handlingStrategy":"retry","validationCode":"String text = message.getText();\nboolean embeddedException = text != null && text.startsWith(\"Exception:\");\nboolean retryable = embeddedException && isTransient(text);","typeGuard":"boolean isEmbeddedException(org.springframework.ai.chat.messages.AssistantMessage m) {\n    return m != null && m.getText() != null && m.getText().startsWith(\"Exception:\");\n}","tryCatchPattern":"try { return interceptor.interceptModel(request, chain); }\ncatch (RuntimeException e) { throw new ModelInvocationException(\"Retries exhausted on embedded exception\", e); }","preventionTips":["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"],"tags":["model","retry","interceptor","embedded-exception"],"backgroundTag":"api-request-failed","analyzedSha":"f82da0b50f35744c13968191be2b1cd2452ef550","analyzedAt":"2026-09-09T15:32:42.421Z","contentChangedAt":"2026-09-09T15:32:42.421Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}