Tool Calling
Nehanda CLI supports two tool calling paths: native (provider handles tool selection and returns structured tool_calls) and rescue (tool schemas are injected into the system prompt, and the engine parses tool calls from model text). The rescue path exists to work around a specific vLLM limitation.
The Problem
The Nehanda vLLM deployment runs with two flags:
--tool-call-parser qwen3_xml— tells vLLM to intercept Qwen-style XML tool calls--enable-auto-tool-choice— enables automatic tool selection
However, the nehandaMlProxy Lambda function strips the tools and tool_choice arrays from requests before forwarding to vLLM. This is necessary to work around a Qwen3 chat template bug where the presence of a tools array causes system message ordering errors.
The consequence: the model never receives native tool definitions, so it can’t use the standard OpenAI tool calling protocol. The engine needs a way to get tool calls out of the model anyway.
The Rescue Path
1. System prompt injection
Tool schemas injected as [TOOL_CALL]...[/TOOL_CALL] blocks
into the system prompt, with example invocations
2. Late directive injection
A [SYSTEM DIRECTIVE] appended to the final user message
to defeat token recency bias on reasoning models
3. Model generation
The model emits tool calls in [TOOL_CALL] format within its response text.
No native tool_calls are returned by the provider.
4. Local parsing & execution
parseXmlToolCalls() extracts JSON tool calls from the text.
executeBuiltinTool() runs them.
Results are appended to the transcript.
The loop continues.
Why [TOOL_CALL] Not <tool_call> XML?
The vLLM --tool-call-parser qwen3_xml flag registers <tool_call> XML tags as stop tokens. When the model emits them in plain text:
- vLLM terminates generation mid-sentence
- vLLM’s native parser takes over
- The native parser returns nothing (the plain-text path never populates
message.tool_calls) - The result: truncated responses containing only thinking traces
Switching to [TOOL_CALL]...[/TOOL_CALL] delimiters completely bypasses vLLM’s stop-token interception. The model completes its full generation, and the engine parses the tool calls locally.
Parser Details
parseXmlToolCalls(text, knownToolNames)
Extracts tool calls from model output. The parser:
- Finds
[TOOL_CALL]...[/TOOL_CALL]blocks - Extracts JSON from within the delimiters
- Strips any leaked XML tags (
<arguments>,<tool_call>) - Repairs a common shorthand:
{"ToolName", "arguments": {...}}→{"name":"ToolName", "arguments": {...}} - Validates tool names against
knownToolNames(ignores unknown tools) - Returns an array of
tool_useobjects
parseManualToolCalls(text)
A fallback parser that looks for JSON tool calls in fenced code blocks:
```json
{"tool": "ToolName", "input": {"key": "value"}}
```
This is used when [TOOL_CALL] blocks are not found but the model still tries to call tools.
When Is Rescue Mode Active?
Rescue mode is active when either of these is true:
- The provider is configured with
force_manual_tools: truein settings - The provider is
nehanda(the Nehanda Cloud endpoint) - The provider is
ollamaand native tool support was not detected - An Ollama provider returned a “does not support tools” error (triggers automatic fallback)
Ollama Tool Support Detection
For Ollama providers, the engine proactively checks whether the model supports native tools by calling /api/show:
{
"template": "... .Tools ...",
"capabilities": ["tools"]
}
If the template contains .Tools or capabilities include tools, native tool calling is used. Otherwise, rescue mode is activated. Results are cached per model to avoid repeated checks.
Native Tool Calling
When native tool calling is available (Claude, LM Studio with tool support, Ollama with tool support), the engine:
- Sends the
toolsarray in the API request - Receives structured
tool_callsin the response - Executes them directly — no text parsing needed
The two paths are transparent to the rest of the engine. Both produce the same tool_use objects that flow into the tool execution pipeline.
omitToolChoice Behavior
Some providers (notably Nehanda) require tool_choice to be omitted from requests. The omitToolChoice(provider) function in modelConfig.mjs handles this. When tool_choice is omitted, the model relies on the [SYSTEM DIRECTIVE] late injection to decide whether to use tools.