---
title: "Tool/function calling"
manual: "LLM Foundation"
version: "0.42"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:developer-tool-calling@0.42"
source: "Developer/ToolCalling.rst"
rendered: "2026-11-01T11:38:21+00:00"
---

# Tool/function calling {#developer-tool-calling}

Tool calling (also known as function calling) allows the LLM to request
execution of functions you define. The model decides when to call a tool
based on the conversation context.

![Sequence: the application sends a chat request with tool definitions, the model answers with tool calls, the tool gate admits or denies each call, the application executes the admitted ones and returns their results, and the model produces the final answer.](../Images/diagram-tool-calling-flow.svg)

## Defining tools {#defining-tools}

**Example: Tool/function calling**

```php
$tools = [
    [
        'type' => 'function',
        'function' => [
            'name' => 'get_weather',
            'description' => 'Get current weather for a location',
            'parameters' => [
                'type' => 'object',
                'properties' => [
                    'location' => [
                        'type' => 'string',
                        'description' => 'City name',
                    ],
                    'unit' => [
                        'type' => 'string',
                        'enum' => ['celsius', 'fahrenheit'],
                    ],
                ],
                'required' => ['location'],
            ],
        ],
    ],
];
```

## Executing tool calls {#executing-tool-calls}

`CompletionResponse::$toolCalls` is a list of
`NetresearchNrLlmDomainValueObjectToolCall` value objects —
`$toolCall->arguments` is already a JSON-decoded associative array,
so no manual `json_decode()` is needed. The two follow-up turns are
built with the `ChatMessage` factories:
`ChatMessage::assistantToolCalls()` echoes the assistant turn that
carries the tool calls, and `ChatMessage::toolResult()` answers one
call by its id.

**Example: Handling tool call responses**

```php
use Netresearch\NrLlm\Domain\ValueObject\ChatMessage;

$response = $this->llmManager->chatWithTools($messages, $tools);

if ($response->hasToolCalls()) {
    // Echo the assistant turn (with all its tool calls) back first
    $messages[] = ChatMessage::assistantToolCalls(
        $response->toolCalls,
        $response->content,
        $response->metadata['nrllm_provider_items'] ?? null,
    );

    foreach ($response->toolCalls as $toolCall) {
        // Execute your function — $toolCall->arguments is a decoded array
        $result = match ($toolCall->name) {
            'get_weather' => $this->getWeather($toolCall->arguments['location']),
            default => throw new \RuntimeException("Unknown function: {$toolCall->name}"),
        };

        // Answer the call by its id
        $messages[] = ChatMessage::toolResult($toolCall->id, json_encode($result, JSON_THROW_ON_ERROR));
    }

    // Ask the model to answer with the tool results in context
    $response = $this->llmManager->chat($messages);
}
```

Providers that implement [ToolCapableInterface](https://docs.typo3.org/permalink/netresearch/nr-llm:api-provider-tools@0.42) support
tool calling.

## Provider-owned conversation context {#provider-owned-conversation-context}

Gemini generateContent and OpenAI Responses need native assistant context
between tool steps. The built-in tool loop carries it automatically, including
through stored suspension and resume ([ADR-222](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-222@0.42)). Manual loops
pass the response's `nrllm_provider_items` metadata to the assistant factory
as shown above. Store `ChatMessage::toTranscriptArray()` and restore it with
`ChatMessage::fromArray()`; the ordinary wire array and JSON serialization
omit this internal context.

Populated Gemini native function-call IDs additionally use an internal
per-turn binding to the public tool-call ID ([ADR-226](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-226@0.42)).
Corresponding results echo the native ID; reordered identical parallel calls
retain their own association after stored approval resume. ID-less calls keep
the existing result shape. Older owned state with native IDs is accepted only
when its decoded name and arguments identify a unique association; malformed
or ambiguous owned bindings are refused before contact. Integer and decimal
representations match only when conversion preserves the numeric value in
both directions; strings and booleans remain distinct. These bindings stay
inside the opaque carrier and do not become OpenAI payload fields.

Gemini preserves the original native parts and thought signatures. It uses an
explicit ownership marker so a fallback to OpenAI Responses reconstructs
ordinary visible text and tool calls. Historical OpenAI items keep their own
replay semantics. A malformed or ambiguous recognized Gemini capsule is
refused before provider contact. Switching providers cannot guarantee native
reasoning continuity on a provider or model that cannot consume that context.
New streamed-response replay state is outside this contract.

## Running the tool loop without an approval step {#running-the-tool-loop-without-an-approval-step}

`ToolLoopServiceInterface::runLoop()` suspends a run when the model
calls a tool that needs a human approval, and throws
`ToolApprovalRequiredException`. A caller that has no approval step,
such as an editor dialog, offers the model only the tools that never
suspend: first the tools the policy allows, then those of them that run
without an approval and without asking for typed input
([ADR-210](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-210@0.42)).

**Example: offering only tools that run without approval**

```php
use Netresearch\NrLlm\Service\Tool\ToolCallPolicyInterface;
use Netresearch\NrLlm\Service\Tool\UnattendedToolFilterInterface;

$offerable = $this->toolCallPolicy->filterOfferable(null, $configuration, $backendUser);
$tools = $this->unattendedToolFilter->unattended($offerable);

$result = $this->toolLoopService->runLoop($messages, $configuration, $context, $tools);
```

An empty list offers no tools at all; pass it as is rather than `null`,
which would mean "every enabled tool".

The filter applies the same rule as an agent run. A remote (MCP) tool is
judged on the operator's declaration on its server record, not on its
effect, so an undeclared remote tool counts as running without approval
here too.
