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 1fb80364e9d0 camel-openai: handle null finish_reason from
OpenAI-compatible servers
1fb80364e9d0 is described below
commit 1fb80364e9d09a50ac7c8662d22ede7016787415
Author: Croway <[email protected]>
AuthorDate: Wed Apr 29 13:54:15 2026 +0200
camel-openai: handle null finish_reason from OpenAI-compatible servers
Some OpenAI-compatible servers (e.g. mlx-lm) return null finish_reason
when tools are defined but the model responds without calling any.
Use the SDK's null-safe _finishReason() accessor instead of the
throwing finishReason() to avoid NPE.
---
.../camel/component/openai/OpenAIProducer.java | 23 ++++++++++++++++++----
1 file changed, 19 insertions(+), 4 deletions(-)
diff --git
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java
index 1ff143d51e13..b2366ffa5a0f 100644
---
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java
+++
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java
@@ -30,6 +30,7 @@ import java.util.Set;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.openai.core.JsonField;
import com.openai.core.JsonValue;
import com.openai.core.http.StreamResponse;
import com.openai.models.ResponseFormatJsonSchema;
@@ -378,7 +379,7 @@ public class OpenAIProducer extends DefaultAsyncProducer {
exchange.setProperty(OpenAIConstants.RESPONSE, response);
}
- if
(response.choices().get(0).finishReason().equals(ChatCompletion.Choice.FinishReason.TOOL_CALLS))
{
+ if (isToolCallsFinishReason(response.choices().get(0))) {
exchange.getMessage().setBody(response.choices().get(0).message().toolCalls());
} else {
String content =
response.choices().get(0).message().content().orElse("");
@@ -409,10 +410,10 @@ public class OpenAIProducer extends DefaultAsyncProducer {
ChatCompletion response =
getEndpoint().getClient().chat().completions().create(paramsBuilder.build());
ChatCompletion.Choice choice = response.choices().get(0);
- if
(!choice.finishReason().equals(ChatCompletion.Choice.FinishReason.TOOL_CALLS)) {
+ if (!isToolCallsFinishReason(choice)) {
// Final LLM response
LOG.debug("Agentic loop completed after {} iterations, finish
reason: {}", iteration,
- choice.finishReason());
+ getFinishReasonString(choice));
String content = choice.message().content().orElse("");
exchange.getMessage().setBody(content);
setResponseHeaders(exchange.getMessage(), response);
@@ -566,13 +567,27 @@ public class OpenAIProducer extends DefaultAsyncProducer {
}
+ private static boolean isToolCallsFinishReason(ChatCompletion.Choice
choice) {
+ JsonField<ChatCompletion.Choice.FinishReason> field =
choice._finishReason();
+ return field.asKnown()
+ .map(r ->
r.equals(ChatCompletion.Choice.FinishReason.TOOL_CALLS))
+ .orElse(false);
+ }
+
+ private static String getFinishReasonString(ChatCompletion.Choice choice) {
+ JsonField<ChatCompletion.Choice.FinishReason> field =
choice._finishReason();
+ return field.asKnown()
+ .map(ChatCompletion.Choice.FinishReason::toString)
+ .orElse("stop");
+ }
+
private void setResponseHeaders(Message message, ChatCompletion response) {
message.setHeader(OpenAIConstants.RESPONSE_ID, response.id());
message.setHeader(OpenAIConstants.RESPONSE_MODEL, response.model());
if (!response.choices().isEmpty()) {
ChatCompletion.Choice choice = response.choices().get(0);
- message.setHeader(OpenAIConstants.FINISH_REASON,
choice.finishReason().toString());
+ message.setHeader(OpenAIConstants.FINISH_REASON,
getFinishReasonString(choice));
}
if (response.usage().isPresent()) {