Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,75 @@ YAML::
----
====

=== Parallel Tool Execution

A model can request several tool calls in a single response. By default, the component executes them one after another,
so the batch takes as long as the sum of its tool calls. Because tool calls emitted in the same assistant message are
independent by design, `parallelToolExecution=true` dispatches them concurrently instead, reducing the batch to the
duration of its slowest tool:

[tabs]
====
Java::
+
[source,java]
----
from("direct:chat")
.to("openai:chat-completion?model=gpt-4"
+ "&parallelToolExecution=true"
+ "&parallelToolTimeout=30000"
+ "&mcpServer.api.transportType=streamableHttp"
+ "&mcpServer.api.url=http://localhost:9090/mcp");
----

XML::
+
[source,xml]
----
<route>
<from uri="direct:chat"/>
<to uri="openai:chat-completion?model=gpt-4&amp;parallelToolExecution=true&amp;parallelToolTimeout=30000&amp;mcpServer.api.transportType=streamableHttp&amp;mcpServer.api.url=http://localhost:9090/mcp"/>
</route>
----

YAML::
+
[source,yaml]
----
- route:
from:
uri: direct:chat
steps:
- to:
uri: openai:chat-completion
parameters:
model: gpt-4
parallelToolExecution: true
parallelToolTimeout: 30000
mcpServer.api.transportType: streamableHttp
mcpServer.api.url: http://localhost:9090/mcp
----
====

The option applies to both the agentic loop and the `openai:tool-execution` operation.

Notes:

* Results are always fed back to the model in the order the model requested the tools, as required by the OpenAI API,
regardless of the order in which the tool calls complete.
* A batch containing a single tool call is executed inline, without a thread hand-off.
* `parallelToolTimeout` (milliseconds, default `0` = disabled) bounds the batch as a whole, so that one slow tool cannot
block it. A tool call that exceeds it is cancelled and then handled according to `toolExecutionErrorStrategy`. Leaving
it disabled is reasonable, because `mcpTimeout` already bounds each individual MCP request.
* With `toolExecutionErrorStrategy=failExchange`, the sibling tool calls that were already dispatched are allowed to
complete before the exchange fails. Sequential execution instead abandons the remaining calls at the first failure.
* The thread pool comes from Camel's `ExecutorServiceManager` and follows the default thread pool profile, so it is
visible over JMX and shut down with the `CamelContext`. When virtual threads are enabled
(`camel.main.virtualThreadsEnabled=true`), the pool automatically becomes a thread-per-task virtual thread executor and
the profile's pool sizes no longer apply — concurrency is then bounded by the profile's `maxQueueSize` instead.
* `stdio` MCP servers multiplex all calls over a single child process, so the achievable parallelism there depends on
the server implementation. HTTP-based transports (`streamableHttp`, `sse`) benefit the most.

=== Manual Tool Loop with `tool-execution` Operation

When `autoToolExecution=false`, you can implement your own tool loop using the `openai:tool-execution` operation and Camel's `loopDoWhile` EIP. This gives you full control to add logging, filtering, retry logic, or custom routing between tool calls — without writing any Java code.
Expand Down Expand Up @@ -524,6 +593,68 @@ YAML::
----
====

=== Runtime Tool Refresh

MCP servers may add, remove or change tools while they are running, and announce it with a `tools/list_changed`
notification. When `mcpToolRefresh=true` (the default), the component subscribes to that notification and refreshes the
tool list it advertises to the model, so a long-running route picks up server-side changes without being restarted.

Set `mcpToolRefresh=false` to pin the tool list to whatever was listed when the endpoint started, for deployments that
require a deterministic set of tools:

[tabs]
====
Java::
+
[source,java]
----
from("direct:chat")
.to("openai:chat-completion?model=gpt-4"
+ "&mcpToolRefresh=false"
+ "&mcpServer.api.transportType=streamableHttp"
+ "&mcpServer.api.url=http://localhost:9090/mcp");
----

XML::
+
[source,xml]
----
<route>
<from uri="direct:chat"/>
<to uri="openai:chat-completion?model=gpt-4&amp;mcpToolRefresh=false&amp;mcpServer.api.transportType=streamableHttp&amp;mcpServer.api.url=http://localhost:9090/mcp"/>
</route>
----

YAML::
+
[source,yaml]
----
- route:
from:
uri: direct:chat
steps:
- to:
uri: openai:chat-completion
parameters:
model: gpt-4
mcpToolRefresh: false
mcpServer.api.transportType: streamableHttp
mcpServer.api.url: http://localhost:9090/mcp
----
====

Notes:

* A refresh replaces only the tools of the server that sent the notification. Tools from the other configured servers are
left untouched.
* The per-server `toolNames` include list is re-applied on every refresh, so a server cannot widen its exposed tool set
by announcing new tools.
* Duplicate tool names are resolved the same way as at startup: the server that registered the name first keeps it.
* A tool call already in flight when a refresh lands runs to completion. If the model then calls a tool that the refresh
removed, it is handled as a hallucinated tool name according to `hallucinatedToolNameStrategy`.
* `returnDirect` flags set programmatically with `addReturnDirectTool` / `removeReturnDirectTool` survive a refresh and a
reconnect.

=== MCP Connection Recovery

When `mcpReconnect=true` (the default), the component automatically recovers from MCP server connection failures. If a tool call fails with a transport error, the component:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,17 @@ public boolean configure(CamelContext camelContext, Object obj, String name, Obj
case "mcpServer": target.getConfiguration().setMcpServer(property(camelContext, java.util.Map.class, value)); return true;
case "mcptimeout":
case "mcpTimeout": target.getConfiguration().setMcpTimeout(property(camelContext, int.class, value)); return true;
case "mcptoolrefresh":
case "mcpToolRefresh": target.getConfiguration().setMcpToolRefresh(property(camelContext, boolean.class, value)); return true;
case "model": target.getConfiguration().setModel(property(camelContext, java.lang.String.class, value)); return true;
case "oauthprofile":
case "oauthProfile": target.getConfiguration().setOauthProfile(property(camelContext, java.lang.String.class, value)); return true;
case "outputclass":
case "outputClass": target.getConfiguration().setOutputClass(property(camelContext, java.lang.String.class, value)); return true;
case "paralleltoolexecution":
case "parallelToolExecution": target.getConfiguration().setParallelToolExecution(property(camelContext, boolean.class, value)); return true;
case "paralleltooltimeout":
case "parallelToolTimeout": target.getConfiguration().setParallelToolTimeout(property(camelContext, long.class, value)); return true;
case "previousresponseid":
case "previousResponseId": target.getConfiguration().setPreviousResponseId(property(camelContext, java.lang.String.class, value)); return true;
case "requesttimeout":
Expand Down Expand Up @@ -221,11 +227,17 @@ public Class<?> getOptionType(String name, boolean ignoreCase) {
case "mcpServer": return java.util.Map.class;
case "mcptimeout":
case "mcpTimeout": return int.class;
case "mcptoolrefresh":
case "mcpToolRefresh": return boolean.class;
case "model": return java.lang.String.class;
case "oauthprofile":
case "oauthProfile": return java.lang.String.class;
case "outputclass":
case "outputClass": return java.lang.String.class;
case "paralleltoolexecution":
case "parallelToolExecution": return boolean.class;
case "paralleltooltimeout":
case "parallelToolTimeout": return long.class;
case "previousresponseid":
case "previousResponseId": return java.lang.String.class;
case "requesttimeout":
Expand Down Expand Up @@ -353,11 +365,17 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
case "mcpServer": return target.getConfiguration().getMcpServer();
case "mcptimeout":
case "mcpTimeout": return target.getConfiguration().getMcpTimeout();
case "mcptoolrefresh":
case "mcpToolRefresh": return target.getConfiguration().isMcpToolRefresh();
case "model": return target.getConfiguration().getModel();
case "oauthprofile":
case "oauthProfile": return target.getConfiguration().getOauthProfile();
case "outputclass":
case "outputClass": return target.getConfiguration().getOutputClass();
case "paralleltoolexecution":
case "parallelToolExecution": return target.getConfiguration().isParallelToolExecution();
case "paralleltooltimeout":
case "parallelToolTimeout": return target.getConfiguration().getParallelToolTimeout();
case "previousresponseid":
case "previousResponseId": return target.getConfiguration().getPreviousResponseId();
case "requesttimeout":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class OpenAIEndpointUriFactory extends org.apache.camel.support.component
private static final Set<String> ENDPOINT_IDENTITY_PROPERTY_NAMES;
private static final Map<String, String> MULTI_VALUE_PREFIXES;
static {
Set<String> props = new HashSet<>(65);
Set<String> props = new HashSet<>(68);
props.add("additionalBodyProperty");
props.add("additionalHeader");
props.add("additionalResponseHeader");
Expand Down Expand Up @@ -59,10 +59,13 @@ public class OpenAIEndpointUriFactory extends org.apache.camel.support.component
props.add("mcpReconnect");
props.add("mcpServer");
props.add("mcpTimeout");
props.add("mcpToolRefresh");
props.add("model");
props.add("oauthProfile");
props.add("operation");
props.add("outputClass");
props.add("parallelToolExecution");
props.add("parallelToolTimeout");
props.add("previousResponseId");
props.add("requestTimeout");
props.add("speechInstructions");
Expand Down
Loading