Store Chat Messages & State Without Managing Infrastructure.Check Out DialogueDB
Skip to content

Tool Calling Executor

To take advantage of tool calling with OpenAI, Anthropic, and other providers that support it, you can use createLlmFunctionExecutor or the LlmExecutorWithFunctions class directly. It works exactly like a regular llm executor — it extends the class and adds options with some additional type constraints.

Deprecated Export

LlmExecutorOpenAiFunctions is deprecated and will be removed in a future major version. Use LlmExecutorWithFunctions or createLlmFunctionExecutor instead — they support tool calling across all providers, not just OpenAI.

Basic Example

ts
const llm = useLlm("openai.gpt-4o-mini");
const instruction = `You are walking through a maze.
You must take one step at a time.
Pick a direction to move.`;

const prompt = createChatPrompt(instruction);

// Using the factory function (recommended)
const executor = createLlmFunctionExecutor({
  llm,
  prompt,
})

// Or using the class directly
// const executor = new LlmExecutorWithFunctions({ llm, prompt })

const functions = [{
    name: "move_left",
    description: "move one block to the left",
    parameters: {/* options, as JSON Schema */}
},{
    name: "move_right",
    description: "move one block to the right",
    parameters: {/* options, as JSON Schema */}
}]

const response = await executor.execute({
  input: "Hello!"
}, {
  functionCall: "auto",
  functions: functions,
})

Execute Options

The second argument to execute() controls tool calling for that call.

OptionTypeDefaultDescription
functionsArray<{ name, description, parameters? }>undefinedThe tools the LLM is allowed to call. parameters is a JSON Schema object describing the tool's arguments.
functionCall"auto" | "none" | "any" | { name: string }undefinedHow the LLM should choose. "auto" lets it decide, "none" forbids tool calls, and "any" forces it to call some tool — these three work on every provider. { name } (force one specific tool) is currently mapped only for Anthropic (direct and Bedrock; note Bedrock rejects a forced tool_choice combined with adaptive thinking). On Google it falls back to "auto", and on OpenAI-compatible endpoints it is passed through unmapped, which the provider rejects.
functionCallStrictInputbooleanfalseEnables strict schema adherence on providers that support it (OpenAI-compatible endpoints). Ignored elsewhere.
jsonSchemaRecord<string, any>undefinedOptional JSON Schema for structured output. Mapped for OpenAI-compatible endpoints (OpenAI, xAI, Deepseek) only; silently ignored elsewhere.

TIP

Tool definitions are normalized internally, so the same functions array works across OpenAI, Anthropic, Google, xAI, and other providers that support tool calling.

Handling the Response

The tool-calling executor returns one of two things, depending on what the LLM did:

  • The LLM called one or more tools — you get the normalized content array, OutputResultContent[]. Each tool call is an object with type: "function_use", plus name, input, and functionId.
  • The LLM replied with text instead — you get the output of the parser you passed in (a string by default, since the executor falls back to the string parser).

Use the exported guards.isFunctionCall type guard to tell them apart without hand-writing checks:

ts
import { guards } from "llm-exe";

const response = await executor.execute({ input: "What's the weather in Denver?" }, {
  functionCall: "auto",
  functions,
})

if (guards.hasFunctionCall(response)) {
  for (const item of response) {
    if (guards.isFunctionCall(item)) {
      // item.name      -> "getWeather"
      // item.input     -> { latitude: 39.7392, longitude: -104.9903 }
      // item.functionId -> provider-assigned call id, use it when replying
      const result = await callYourTool(item.name, item.input);
    }
  }
} else {
  // plain text response, already run through your parser. hasFunctionCall
  // (not Array.isArray) is the discriminator: a parser like listToArray also
  // returns an array on the text path, and it must land here, not above.
  console.log(response);
}

WARNING

A parser passed to createLlmFunctionExecutor only applies to the text path. When the LLM calls a tool, the raw normalized function_use content is returned so you can dispatch on it — the parser is not applied to tool arguments. Validate item.input yourself if the tool schema isn't enough.

Replying With Tool Results

Tool calling is a loop: run the tool, add the result back to the conversation, and call the executor again. functionId is what ties a result back to the call that produced it — see Dialogue for storing the running history, and addFromHistory for replaying it into the next prompt.

See Also