This is an automated email from the ASF dual-hosted git repository.
Croway pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 2e66cc2330c5 CAMEL-24550: Document AI component exception types for
onException handling
2e66cc2330c5 is described below
commit 2e66cc2330c57ffc56590a63884b1d88ae94ae31
Author: Karol <[email protected]>
AuthorDate: Sat Aug 29 15:51:18 2026 +0200
CAMEL-24550: Document AI component exception types for onException handling
Document the SDK exception types that camel-openai and
camel-langchain4j-chat
propagate unchanged, so routes can retry rate limits and 5xx while failing
fast
on validation and auth errors. Adds the interaction between the SDK's own
retry
layer (maxRetries) and Camel redelivery, which multiply rather than replace
each
other, and a cross-component overview in the LLM Integration Guide.
Co-authored-by: Claude <[email protected]>
---
.../catalog/docs/langchain4j-chat-component.adoc | 58 +++++++++++++++
.../camel/catalog/docs/openai-component.adoc | 87 +++++++++++++++++++++-
.../src/main/docs/langchain4j-chat-component.adoc | 58 +++++++++++++++
.../src/main/docs/openai-component.adoc | 87 +++++++++++++++++++++-
.../src/main/docs/ai-llm-integration-guide.adoc | 41 ++++++++++
5 files changed, 329 insertions(+), 2 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-chat-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-chat-component.adoc
index c71b3d6e9d5d..cbe442b39008 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-chat-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-chat-component.adoc
@@ -325,3 +325,61 @@ List<Content> contents = List.of(augmentedContent);
String response = template.requestBodyAndHeader("direct:send-multiple",
messages, LangChain4jChatHeaders.AUGMENTED_DATA, contents, String.class);
----
+
+== Error Handling
+
+The component does not wrap what the chat model throws. LangChain4j maps
provider HTTP errors onto its own hierarchy in `dev.langchain4j.exception`, and
that exception reaches the exchange unchanged, so `onException` can match it by
type.
+
+The hierarchy already splits along the axis that matters for retries:
+
+[cols="2,3"]
+|===
+| Exception | Meaning
+
+| `RetriableException` | Base type for failures worth another attempt
+| `RateLimitException` | Provider rejected the call with a rate limit
+| `InternalServerException` | Provider-side failure
+| `TimeoutException` | The call did not complete within the model's timeout
+| `NonRetriableException` | Base type for failures that will fail the same way
again
+| `InvalidRequestException` | Malformed request, including context-length
errors
+| `ContentFilteredException` | Content rejected by the provider's moderation,
a subclass of `InvalidRequestException`
+| `AuthenticationException` | Bad or missing API key
+| `ModelNotFoundException` | Unknown model name
+| `UnresolvedModelServerException` | The model server address could not be
resolved
+|===
+
+Because the two base classes carry the decision, a route rarely has to
enumerate the concrete types:
+
+[source, java]
+----
+onException(RetriableException.class)
+ .maximumRedeliveries(3)
+ .redeliveryDelay(2000)
+ .backOffMultiplier(2)
+ .useExponentialBackOff();
+
+onException(NonRetriableException.class)
+ .maximumRedeliveries(0)
+ .handled(true)
+ .to("direct:rejected");
+
+from("direct:chat")
+ .to("langchain4j-chat:my-chat");
+----
+
+IMPORTANT: The mapping is performed by the LangChain4j model implementation,
not by Camel. `OpenAiChatModel` and `OllamaChatModel` route their calls through
LangChain4j's exception mapper, but a model that does not will propagate the
raw exception of its own HTTP client instead. Verify what your provider throws
before relying on the type.
+
+=== Model retries vs Camel redelivery
+
+Most LangChain4j builders expose `maxRetries`, defaulting to `2`, which means
one call already makes up to three attempts before any exception reaches Camel.
Adding `maximumRedeliveries(3)` on top of that multiplies out to twelve
attempts against a provider that is already rate limiting you.
+
+Configure one layer. Either leave the model retries alone and skip Camel
redelivery, or build the model with `maxRetries(0)` and drive retries from
`onException` so they are visible to the Camel error handler:
+
+[source, java]
+----
+ChatModel model = OpenAiChatModel.builder()
+ .apiKey(System.getenv("OPENAI_API_KEY"))
+ .modelName("gpt-4o-mini")
+ .maxRetries(0)
+ .build();
+----
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-component.adoc
index 5d7462a31161..eea6c204ad13 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-component.adoc
@@ -1277,4 +1277,89 @@ The component may throw the following exceptions:
* `CamelExchangeException`:
** When moderation returns a number of results that does not match the
number of inputs (moderation)
** When the image response contains no images, or an image with neither
`b64_json` nor `url` (image-generation, image-edit)
-* API-specific exceptions from the OpenAI SDK for network errors,
authentication failures, rate limiting, etc.
+* API-specific exceptions from the OpenAI SDK for network errors,
authentication failures, rate limiting, etc. These are propagated unchanged, so
routes can match them by type. See
<<_targeting_sdk_exceptions_with_onexception>>.
+
+=== Targeting SDK Exceptions with onException
+
+The producer does not wrap or reclassify what the OpenAI SDK throws. The SDK
exception reaches the exchange as-is, so `onException` can match the concrete
type and treat a transient failure differently from a terminal one.
+
+Every HTTP error response maps to a subclass of
`com.openai.errors.OpenAIServiceException`:
+
+[cols="2,1,3"]
+|===
+| Exception | Status | Worth retrying
+
+| `BadRequestException` | 400 | No, the request is malformed
+| `UnauthorizedException` | 401 | No, fix the API key
+| `PermissionDeniedException` | 403 | No
+| `NotFoundException` | 404 | No, usually an unknown model or a wrong `baseUrl`
+| `UnprocessableEntityException` | 422 | No, typically a context-length or
validation error
+| `RateLimitException` | 429 | Yes, with backoff
+| `InternalServerException` | 5xx | Yes, with backoff
+| `UnexpectedStatusCodeException` | other | Depends on the provider
+|===
+
+Three further exceptions carry no status code and extend
`com.openai.errors.OpenAIException` directly: `OpenAIIoException` for connect
and read failures, `OpenAIRetryableException` when the SDK gave up after
exhausting its own retries, and `OpenAIInvalidDataException` when the response
body cannot be parsed.
+
+Retrying the transient cases while failing fast on the rest:
+
+[source,java]
+----
+onException(RateLimitException.class, InternalServerException.class,
OpenAIIoException.class)
+ .maximumRedeliveries(3)
+ .redeliveryDelay(2000)
+ .backOffMultiplier(2)
+ .useExponentialBackOff();
+
+onException(BadRequestException.class, UnprocessableEntityException.class,
+ UnauthorizedException.class)
+ .maximumRedeliveries(0)
+ .handled(true)
+ .to("direct:rejected");
+
+from("direct:ask")
+ .to("openai:chat-completion?model=gpt-4o-mini");
+----
+
+`OpenAIServiceException` carries the details of the failed call, which is what
you want for logging or for routing on the provider's own error code:
+
+[source,java]
+----
+onException(OpenAIServiceException.class)
+ .handled(true)
+ .process(exchange -> {
+ OpenAIServiceException cause
+ = exchange.getProperty(Exchange.EXCEPTION_CAUGHT,
OpenAIServiceException.class);
+ exchange.getIn().setHeader("FailureStatus", cause.statusCode());
+ exchange.getIn().setHeader("FailureCode", cause.code().orElse(null));
+ exchange.getIn().setHeader("FailureType", cause.type().orElse(null));
+ })
+ .to("direct:failed-calls");
+----
+
+`headers()` on the same exception exposes the response headers, including
`Retry-After` on a 429.
+
+=== SDK Retry vs Camel Redelivery
+
+The SDK client retries on its own before the exception ever reaches Camel. The
`maxRetries` option controls this and defaults to `2`, so one exchange already
issues up to three HTTP requests. The SDK retries 408, 409, 429 and 5xx
responses as well as connection failures, backing off exponentially and
honoring the `Retry-After` and `Retry-After-Ms` response headers.
+
+Camel redelivery multiplies with that layer rather than replacing it. With the
default `maxRetries=2` and `maximumRedeliveries(3)`, a single message can
produce twelve HTTP requests, which is rarely what you want against a
rate-limited endpoint.
+
+Pick one layer and keep the other out of the way:
+
+* Leave `maxRetries` at its default and do not add Camel redelivery for rate
limits. The SDK backoff already reads `Retry-After`, and this is the simplest
choice for a plain request/response route.
+* Set `maxRetries=0` and drive retries from `onException` when the retry has
to be visible to Camel, for example to reach a dead letter channel, apply a
per-route policy, or show up in error handler metrics.
+
+[source,java]
+----
+from("direct:ask")
+ .to("openai:chat-completion?model=gpt-4o-mini&maxRetries=0");
+----
+
+NOTE: `requestTimeout` bounds a single HTTP request, not the whole retry
sequence. At the default `maxRetries=2`, worst-case wall time is roughly three
times `requestTimeout` plus the backoff between attempts.
+
+=== Errors in Streaming Mode
+
+With `streaming=true` the producer issues the initial HTTP request and then
hands Camel an `Iterator<ChatCompletionChunk>`. Failures on that first request
(authentication, rate limiting, connection errors) are thrown by the producer
and behave like any other producer exception.
+
+A failure that occurs after the first chunk is different: it surfaces while
the iterator is consumed, so it is reported against the step doing the
consuming, usually a Split EIP, and not against the `to("openai:...")` step.
Route-scoped `onException` still applies, but an error handler attached to the
producer step alone will not see it.
diff --git
a/components/camel-ai/camel-langchain4j-chat/src/main/docs/langchain4j-chat-component.adoc
b/components/camel-ai/camel-langchain4j-chat/src/main/docs/langchain4j-chat-component.adoc
index c71b3d6e9d5d..cbe442b39008 100644
---
a/components/camel-ai/camel-langchain4j-chat/src/main/docs/langchain4j-chat-component.adoc
+++
b/components/camel-ai/camel-langchain4j-chat/src/main/docs/langchain4j-chat-component.adoc
@@ -325,3 +325,61 @@ List<Content> contents = List.of(augmentedContent);
String response = template.requestBodyAndHeader("direct:send-multiple",
messages, LangChain4jChatHeaders.AUGMENTED_DATA, contents, String.class);
----
+
+== Error Handling
+
+The component does not wrap what the chat model throws. LangChain4j maps
provider HTTP errors onto its own hierarchy in `dev.langchain4j.exception`, and
that exception reaches the exchange unchanged, so `onException` can match it by
type.
+
+The hierarchy already splits along the axis that matters for retries:
+
+[cols="2,3"]
+|===
+| Exception | Meaning
+
+| `RetriableException` | Base type for failures worth another attempt
+| `RateLimitException` | Provider rejected the call with a rate limit
+| `InternalServerException` | Provider-side failure
+| `TimeoutException` | The call did not complete within the model's timeout
+| `NonRetriableException` | Base type for failures that will fail the same way
again
+| `InvalidRequestException` | Malformed request, including context-length
errors
+| `ContentFilteredException` | Content rejected by the provider's moderation,
a subclass of `InvalidRequestException`
+| `AuthenticationException` | Bad or missing API key
+| `ModelNotFoundException` | Unknown model name
+| `UnresolvedModelServerException` | The model server address could not be
resolved
+|===
+
+Because the two base classes carry the decision, a route rarely has to
enumerate the concrete types:
+
+[source, java]
+----
+onException(RetriableException.class)
+ .maximumRedeliveries(3)
+ .redeliveryDelay(2000)
+ .backOffMultiplier(2)
+ .useExponentialBackOff();
+
+onException(NonRetriableException.class)
+ .maximumRedeliveries(0)
+ .handled(true)
+ .to("direct:rejected");
+
+from("direct:chat")
+ .to("langchain4j-chat:my-chat");
+----
+
+IMPORTANT: The mapping is performed by the LangChain4j model implementation,
not by Camel. `OpenAiChatModel` and `OllamaChatModel` route their calls through
LangChain4j's exception mapper, but a model that does not will propagate the
raw exception of its own HTTP client instead. Verify what your provider throws
before relying on the type.
+
+=== Model retries vs Camel redelivery
+
+Most LangChain4j builders expose `maxRetries`, defaulting to `2`, which means
one call already makes up to three attempts before any exception reaches Camel.
Adding `maximumRedeliveries(3)` on top of that multiplies out to twelve
attempts against a provider that is already rate limiting you.
+
+Configure one layer. Either leave the model retries alone and skip Camel
redelivery, or build the model with `maxRetries(0)` and drive retries from
`onException` so they are visible to the Camel error handler:
+
+[source, java]
+----
+ChatModel model = OpenAiChatModel.builder()
+ .apiKey(System.getenv("OPENAI_API_KEY"))
+ .modelName("gpt-4o-mini")
+ .maxRetries(0)
+ .build();
+----
diff --git
a/components/camel-ai/camel-openai/src/main/docs/openai-component.adoc
b/components/camel-ai/camel-openai/src/main/docs/openai-component.adoc
index 5d7462a31161..eea6c204ad13 100644
--- a/components/camel-ai/camel-openai/src/main/docs/openai-component.adoc
+++ b/components/camel-ai/camel-openai/src/main/docs/openai-component.adoc
@@ -1277,4 +1277,89 @@ The component may throw the following exceptions:
* `CamelExchangeException`:
** When moderation returns a number of results that does not match the
number of inputs (moderation)
** When the image response contains no images, or an image with neither
`b64_json` nor `url` (image-generation, image-edit)
-* API-specific exceptions from the OpenAI SDK for network errors,
authentication failures, rate limiting, etc.
+* API-specific exceptions from the OpenAI SDK for network errors,
authentication failures, rate limiting, etc. These are propagated unchanged, so
routes can match them by type. See
<<_targeting_sdk_exceptions_with_onexception>>.
+
+=== Targeting SDK Exceptions with onException
+
+The producer does not wrap or reclassify what the OpenAI SDK throws. The SDK
exception reaches the exchange as-is, so `onException` can match the concrete
type and treat a transient failure differently from a terminal one.
+
+Every HTTP error response maps to a subclass of
`com.openai.errors.OpenAIServiceException`:
+
+[cols="2,1,3"]
+|===
+| Exception | Status | Worth retrying
+
+| `BadRequestException` | 400 | No, the request is malformed
+| `UnauthorizedException` | 401 | No, fix the API key
+| `PermissionDeniedException` | 403 | No
+| `NotFoundException` | 404 | No, usually an unknown model or a wrong `baseUrl`
+| `UnprocessableEntityException` | 422 | No, typically a context-length or
validation error
+| `RateLimitException` | 429 | Yes, with backoff
+| `InternalServerException` | 5xx | Yes, with backoff
+| `UnexpectedStatusCodeException` | other | Depends on the provider
+|===
+
+Three further exceptions carry no status code and extend
`com.openai.errors.OpenAIException` directly: `OpenAIIoException` for connect
and read failures, `OpenAIRetryableException` when the SDK gave up after
exhausting its own retries, and `OpenAIInvalidDataException` when the response
body cannot be parsed.
+
+Retrying the transient cases while failing fast on the rest:
+
+[source,java]
+----
+onException(RateLimitException.class, InternalServerException.class,
OpenAIIoException.class)
+ .maximumRedeliveries(3)
+ .redeliveryDelay(2000)
+ .backOffMultiplier(2)
+ .useExponentialBackOff();
+
+onException(BadRequestException.class, UnprocessableEntityException.class,
+ UnauthorizedException.class)
+ .maximumRedeliveries(0)
+ .handled(true)
+ .to("direct:rejected");
+
+from("direct:ask")
+ .to("openai:chat-completion?model=gpt-4o-mini");
+----
+
+`OpenAIServiceException` carries the details of the failed call, which is what
you want for logging or for routing on the provider's own error code:
+
+[source,java]
+----
+onException(OpenAIServiceException.class)
+ .handled(true)
+ .process(exchange -> {
+ OpenAIServiceException cause
+ = exchange.getProperty(Exchange.EXCEPTION_CAUGHT,
OpenAIServiceException.class);
+ exchange.getIn().setHeader("FailureStatus", cause.statusCode());
+ exchange.getIn().setHeader("FailureCode", cause.code().orElse(null));
+ exchange.getIn().setHeader("FailureType", cause.type().orElse(null));
+ })
+ .to("direct:failed-calls");
+----
+
+`headers()` on the same exception exposes the response headers, including
`Retry-After` on a 429.
+
+=== SDK Retry vs Camel Redelivery
+
+The SDK client retries on its own before the exception ever reaches Camel. The
`maxRetries` option controls this and defaults to `2`, so one exchange already
issues up to three HTTP requests. The SDK retries 408, 409, 429 and 5xx
responses as well as connection failures, backing off exponentially and
honoring the `Retry-After` and `Retry-After-Ms` response headers.
+
+Camel redelivery multiplies with that layer rather than replacing it. With the
default `maxRetries=2` and `maximumRedeliveries(3)`, a single message can
produce twelve HTTP requests, which is rarely what you want against a
rate-limited endpoint.
+
+Pick one layer and keep the other out of the way:
+
+* Leave `maxRetries` at its default and do not add Camel redelivery for rate
limits. The SDK backoff already reads `Retry-After`, and this is the simplest
choice for a plain request/response route.
+* Set `maxRetries=0` and drive retries from `onException` when the retry has
to be visible to Camel, for example to reach a dead letter channel, apply a
per-route policy, or show up in error handler metrics.
+
+[source,java]
+----
+from("direct:ask")
+ .to("openai:chat-completion?model=gpt-4o-mini&maxRetries=0");
+----
+
+NOTE: `requestTimeout` bounds a single HTTP request, not the whole retry
sequence. At the default `maxRetries=2`, worst-case wall time is roughly three
times `requestTimeout` plus the backoff between attempts.
+
+=== Errors in Streaming Mode
+
+With `streaming=true` the producer issues the initial HTTP request and then
hands Camel an `Iterator<ChatCompletionChunk>`. Failures on that first request
(authentication, rate limiting, connection errors) are thrown by the producer
and behave like any other producer exception.
+
+A failure that occurs after the first chunk is different: it surfaces while
the iterator is consumed, so it is reported against the step doing the
consuming, usually a Split EIP, and not against the `to("openai:...")` step.
Route-scoped `onException` still applies, but an error handler attached to the
producer step alone will not see it.
diff --git a/components/camel-ai/src/main/docs/ai-llm-integration-guide.adoc
b/components/camel-ai/src/main/docs/ai-llm-integration-guide.adoc
index 74581e3a29b4..1c793a0bbcd8 100644
--- a/components/camel-ai/src/main/docs/ai-llm-integration-guide.adoc
+++ b/components/camel-ai/src/main/docs/ai-llm-integration-guide.adoc
@@ -302,6 +302,47 @@ Enable `useStreaming=true` on the platform-http endpoint
when passing large stre
| High-concurrency fan-out where Camel adds little value between HTTP and the
LLM SDK
|===
+== Error handling and retries
+
+None of the AI components wrap what the underlying SDK throws. Whatever the
model client raises lands on the exchange unchanged, so `onException` can
separate a rate limit worth retrying from a validation error that will fail
identically on every attempt. Which types you match depends on the component.
+
+[cols="2,2,4",options="header"]
+|===
+| Component | Exception package | Retryable vs terminal
+
+| xref:openai-component.adoc[OpenAI]
+| `com.openai.errors`
+| No base type per category. Treat `RateLimitException`,
`InternalServerException` and `OpenAIIoException` as retryable, and
`BadRequestException`, `UnprocessableEntityException`, `UnauthorizedException`
as terminal. All HTTP errors share `OpenAIServiceException`, which exposes
`statusCode()`, `code()` and `headers()`
+
+| xref:langchain4j-chat-component.adoc[LangChain4j Chat], Agent, Embeddings
+| `dev.langchain4j.exception`
+| The hierarchy encodes it. `RetriableException` and `NonRetriableException`
are the only two types most routes need
+
+| xref:spring-ai-chat-component.adoc[Spring AI Chat]
+| Depends on the `ChatModel` bean
+| Spring AI 2.0 builds its OpenAI model on the same `openai-java` SDK, so an
OpenAI-backed `ChatModel` throws `com.openai.errors.*`. Other model
implementations throw their own types, so check before matching
+|===
+
+A minimum viable policy, written against the LangChain4j hierarchy because it
is the clearest:
+
+[source,java]
+----
+onException(RetriableException.class)
+ .maximumRedeliveries(3)
+ .redeliveryDelay(2000)
+ .backOffMultiplier(2)
+ .useExponentialBackOff();
+
+onException(NonRetriableException.class)
+ .maximumRedeliveries(0)
+ .handled(true)
+ .to("direct:dead-letter");
+----
+
+WARNING: These SDKs retry internally before Camel sees the failure, twice by
default, and Camel redelivery multiplies with that layer instead of replacing
it. Three SDK attempts under `maximumRedeliveries(3)` means twelve requests to
a provider that is already rate limiting you. Either turn the SDK layer off
(`maxRetries=0` on the OpenAI component, `maxRetries(0)` on a LangChain4j model
builder) so the retry is visible to the Camel error handler, or leave it on and
skip Camel redelivery [...]
+
+For the per-component detail see
xref:openai-component.adoc#_targeting_sdk_exceptions_with_onexception[OpenAI
error handling] and
xref:langchain4j-chat-component.adoc#_error_handling[LangChain4j Chat error
handling].
+
== End-to-end example: document extraction pipeline
A typical resume-processing pipeline (the use case from community feedback):