Skip to content

Inline tool calls

Some models put tool calls in ordinary content as explicit text, commonly wrapped in <tool_call>...</tool_call>. llm-chat can parse those blocks and feed them through the normal tool-call loop.

Inline tool-call parsing is opt-in and disabled by default. Tool calls can have side effects, and the same marker text can be ordinary content for a different model. Enable it only for models and prompts trusted to emit tool calls in this format.

Configuration

import { ChatServiceConfiguration, OpenAIChatService } from "@johannes.latzel/llm-chat";

const config = new ChatServiceConfiguration();
config.parseInlineToolCalls = true;

// Optional. These are the defaults.
config.inlineToolCallMarkers = {
    open: "<tool_call>",
    close: "</tool_call>"
};

const service = OpenAIChatService.builder()
    .model("your-model")
    .withServiceConfig(config)
    .create();
Property Env var Default Description
parseInlineToolCalls LLM_CHAT_PARSE_INLINE_TOOL_CALLS false Parse explicit inline tool-call blocks in streamed content
inlineToolCallMarkers — <tool_call> / </tool_call> Marker pair used when parsing is enabled

This is independent of parseInlineReasoning. Both can be enabled together. Their opening markers must differ; an ambiguous configuration throws when the request is sent.

Accepted payloads

A block must contain JSON. The following shapes are supported:

Shape Example
Name + arguments {"name":"get_weather","arguments":{"city":"Berlin"}}
Tool + parameters {"tool":"get_weather","parameters":{"city":"Berlin"}}
Function wrapper {"function":{"name":"get_weather","arguments":{"city":"Berlin"}}}
Array of calls [{"name":"first"},{"name":"second","arguments":{}}]

arguments / parameters may be an object or a JSON string representing an object. When omitted, empty, or null, the arguments are {}. Other argument types are rejected and the block remains content.

Each call is assigned a generated id. Any id supplied by the model is ignored, so duplicate or malformed model ids cannot collide in the conversation history.

Streaming and completion

A tool-call block is buffered until its closing marker is seen. Splitting the opening marker, closing marker, or JSON payload across arbitrary chunks is supported. Partial blocks are never executed.

Once at least one complete inline call has been parsed, a provider finish of stop (or another non-aborted finish) is treated as tool_calls so the existing tool loop runs and executes the call. An explicit provider tool_calls finish is unchanged. FinishReason.Aborted is never overridden.

If a block is malformed, has an unsupported shape, is empty, or is still open at end of stream, the original text remains ordinary content and no tool runs.

Structured and inline calls together

Structured delta.tool_calls from the provider remain fully supported. If a response contains both structured deltas and inline blocks, both sets are executed in arrival order.

There is no deduplication between the two forms. If a provider emits the same call both as delta.tool_calls and as <tool_call>, it will run twice. Configure a provider to use one form.

Inline calls detected inside a reasoning block are reasoning text, not tool calls. Text-based tool-call forms other than the configured marker pair are not parsed.

Trust warning

Inline tool calls are model-generated output. Enabling them lets model content invoke registered tools. Treat the model and its prompt as trusted input for any tool with side effects.