Tool Calling with IPWorks LLM


Requirements:
IPWorks LLM

Introduction

The LLMClient component lets applications offer operations, such as a weather lookup, as tools that a model can request. The model does not run application code directly: the application validates each request, performs the operation, and returns the result so the model can continue.

This guide shows how to declare a tool, handle its calls, and continue the conversation until the model produces a final reply.

Tip: Download the latest version of IPWorks LLM to follow along. Set up a first request as shown in Getting Started with IPWorks LLM.

Tool calling depends on the selected model and endpoint. Confirm that both support it before using this workflow.

Step 1: Declare a Tool

For a tool with simple parameters, call AddToolParam for each parameter, followed by AddTool with an empty InputSchema:


llmclient.AddToolParam(
  "location",
  "string",
  "The city and state, for example Raleigh, NC.",
  true);
llmclient.AddTool(
  "get_weather",
  "Look up the current weather at a city and state.",
  "");

AddToolParam collects parameter definitions until AddTool is called. If InputSchema is empty, AddTool builds an object schema from those parameters and marks the required ones. Otherwise, it uses the supplied schema. The parameter definitions are cleared after the tool is added.

Supply a complete schema when the parameter builder cannot express the input you need, such as an enum, array, nested object, or other constraint. This alternative declaration adds an optional units choice:


llmclient.AddTool(
  "get_weather",
  "Look up the current weather at a city and state.",
  "{\"type\":\"object\",\"properties\":{" +
  "\"location\":{\"type\":\"string\",\"description\":\"City and state, for example Raleigh, NC.\"}," +
  "\"units\":{\"type\":\"string\",\"enum\":[\"celsius\",\"fahrenheit\"]}" +
  "},\"required\":[\"location\"]}");

Use one of these declarations for get_weather. The name and descriptions are sent to the provider with the schema and help the model choose an operation and arguments. The application must still validate the arguments when a call arrives.

Step 2: Handle a Tool Call

When the model requests a tool, LLMClient raises ToolCall. The event provides the tool name, arguments as JSON, and a call ID. Pass that ID to AddToolResult so the result matches the request, including repeated calls to the same tool.

The handler returns a failure for an unknown tool, invalid arguments, or a failed lookup. TryReadLocation, shown under Validate Tool Requests, checks for a nonempty string argument.


using System.Text.Json;
llmclient.OnToolCall += (s, e) =>
{
  string result;
  bool failed = false;
  if (e.Name != "get_weather")
  {
    result = "Unknown tool requested.";
    failed = true;
  }
  else if (!TryReadLocation(e.ArgumentsJson, out string location))
  {
    result = "A nonempty string location is required.";
    failed = true;
  }
  else
  {
    try { result = JsonSerializer.Serialize(GetWeather(location)); }
    catch (Exception)
    {
      result = "The weather lookup failed.";
      failed = true;
    }
  }
  llmclient.AddToolResult(e.Id, result, failed);
};

This sample GetWeather method returns fixed, illustrative data for any location. Replace it with your application's lookup code to return current weather:


static object GetWeather(string location)
{
  return new
  {
    location,
    temperature = 72,
    units = "fahrenheit",
    conditions = "Sunny"
  };
}

ToolCall fires once per requested invocation, including during streaming. AddToolResult takes the call ID, result content, and an IsError Boolean. Give every requested call a result before generating again, including failed operations. The component checks that the ID matches a call in the conversation, but does not check that every call has been answered.

Step 3: Continue Until the Final Reply

Generate appends the assistant's reply and tool calls to the conversation before raising ToolCall. The handler above adds results after that assistant turn. The next Generate sends both calls and results. A response containing only tool calls has an empty ResponseText; a tool-call response can also contain text, so use FinishReason to decide whether to continue.


llmclient.AddMessage("user", "What is the weather in Raleigh?");
const int maxGenerationRounds = 5;
for (int round = 0; round < maxGenerationRounds; round++)
{
  llmclient.Generate();
  if (llmclient.FinishReason == LLMClientFinishReasons.frStop)
  {
    Console.WriteLine(llmclient.ResponseText);
    break;
  }
  if (llmclient.FinishReason != LLMClientFinishReasons.frToolCalls)
    throw new InvalidOperationException(
      "The model did not complete its reply: " + llmclient.FinishReason);
  if (round == maxGenerationRounds - 1)
    throw new InvalidOperationException("The generation-round limit was reached.");
}

The limit counts Generate calls, not individual tool invocations: several ToolCall events can occur in one round. The handler supplies results even in the last permitted round. If that response still requests tools, the loop stops without another generation. A length limit, content filter, or other finish reason is not treated as a completed answer.

Validate Tool Requests

ArgumentsJson comes from the model, even when the application supplied a schema. Parse and validate it before performing the operation. TryReadLocation rejects malformed JSON, a missing location, and values that are not nonempty strings:


static bool TryReadLocation(string json, out string location)
{
  location = null;
  try
  {
    using JsonDocument args = JsonDocument.Parse(json);
    if (args.RootElement.ValueKind != JsonValueKind.Object ||
        !args.RootElement.TryGetProperty("location", out JsonElement value) ||
        value.ValueKind != JsonValueKind.String)
      return false;
    location = value.GetString();
    return !String.IsNullOrWhiteSpace(location);
  }
  catch (JsonException) { return false; }
}

The application must also check whether the requested operation is permitted:

  • Allow only explicitly registered tool names.
  • Parse and validate every argument, including ranges, formats, and resource identifiers.
  • Apply the user's permissions to the specific data or action. For consequential changes, ask for confirmation when the application requires it; confirmation does not grant missing permissions.
  • Do not pass model output to a command shell, SQL statement, file path, or outbound request without validation and appropriate restrictions.
  • Return a concise error through AddToolResult instead of hiding the failure from the model.

Keep secrets and sensitive implementation details out of tool descriptions and results. Tool definitions are sent to the selected provider with the conversation.

Provider Differences

The tool-calling methods and event are the same across providers. The component sends the retained conversation in the format of the API the provider uses. The OpenAI Chat Completions format also applies to Ollama, LM Studio, Groq, OpenRouter, Together AI, Fireworks AI, and spCustomOpenAI. The Anthropic and Gemini formats also apply to their corresponding custom provider values:

API Calls and results Failure result
OpenAI Chat Completions Assistant tool_calls; one role tool message per result, linked by tool_call_id No separate error flag is sent; the content must describe the failure.
Anthropic Messages Assistant tool_use blocks; tool_result blocks in a user message, linked by tool_use_id IsError sends is_error: true.
Google Gemini Model functionCall parts; user functionResponse parts, identified by function name and an ID when supplied by the provider An object supplied as successful content is sent as an object. Other successful content is sent as a string under result; failure content is sent as a string under error.

Pass the Id from ToolCall to AddToolResult for every provider. Note: If Gemini omits a call ID, LLMClient creates one locally for this match; it does not send that ID to Gemini. Test the schemas and operations your application actually uses, since model tool choices and arguments can vary.

With spOllama, LLMClient sends tool definitions and handles calls from a model that supports them. Ollama's OpenAI-compatible Chat Completions endpoint accepts tools but does not honor tool_choice. To avoid silently ignoring a requested choice, LLMClient omits that field when ToolChoice is empty or auto; any other value causes Generate to fail with error 804.

Conclusion

Tool calls keep the conversation open until the application returns their results and generates again. Bound that loop, and use FinishReason to recognize the final reply. For saving conversations that contain tool calls and results, see Managing IPWorks LLM Conversations.

We appreciate your feedback. If you have any questions, comments, or suggestions about this article please contact our support team at support@nsoftware.com.