ToolCallParser 0.7.0

dotnet add package ToolCallParser --version 0.7.0
                    
NuGet\Install-Package ToolCallParser -Version 0.7.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ToolCallParser" Version="0.7.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ToolCallParser" Version="0.7.0" />
                    
Directory.Packages.props
<PackageReference Include="ToolCallParser" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ToolCallParser --version 0.7.0
                    
#r "nuget: ToolCallParser, 0.7.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ToolCallParser@0.7.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ToolCallParser&version=0.7.0
                    
Install as a Cake Addin
#tool nuget:?package=ToolCallParser&version=0.7.0
                    
Install as a Cake Tool

ToolCallParser

NuGet License: MIT .NET

Multi-provider tool call parsing and normalization for LLM applications.

Packages

Package Purpose
ToolCallParser Parses LLM tool calls from any supported provider's response JSON into one ToolCall shape, and formats tool results back to that provider's format

Features

No registration or configuration is needed — every entry point is a static method or a public class you construct.

  • Auto-detected parsing - ToolCallParserFactory.Parse(string | JsonElement) detects the format and returns IReadOnlyList<ToolCall>; when detection fails it tries each parser in turn. ToolCallParserFactory.HasToolCalls(...) answers whether a response contains any calls
  • Provider detection - ToolCallParserFactory.DetectProvider(string | JsonElement) returns the detected Provider (Provider.Auto when nothing matches)
  • 5 provider parsers - OpenAIToolCallParser, AnthropicToolCallParser, GoogleToolCallParser, CohereToolCallParser, BedrockToolCallParser (namespace ToolCallParser.Parsers), reachable via ToolCallParserFactory.GetParser(Provider) / TryGetParser(Provider, out ...) or by new. They cover OpenAI, Azure OpenAI, xAI, Mistral, DeepSeek, Claude, Gemini, Command R and Bedrock. Two of them read more than one wire format: OpenAI (Chat Completions + Responses) and Google (generateContent + Interactions). GetParser(Provider.Auto) throws — use Parse/DetectProvider for detection
  • Self-hosted / compatible endpoints - any OpenAI-compatible server (Ollama, vLLM, LM Studio, LocalAI, TGI, GpuStack, Qwen, …) via Provider.OpenAICompatible; Anthropic-compatible endpoints via Provider.AnthropicCompatible
  • Tool calls written as text - ToolCallMarkup.Read(text) reads the calls an open model wrote in its chat template's markup when the serving endpoint's parser missed them — Hermes / Qwen 2.5 <tool_call>{json}</tool_call> and Qwen3-Coder <function=Name><parameter=x>… (wrapped or not) — as name + arguments, plus the prose before them and where a block the text ends inside begins; ToolCallMarkup.Scan(text) tells a streaming reader whether to hold output (None / Partial / Found)
  • Typed argument access - ToolCall.GetArgument<T>(name), GetArguments<T>() (property names match case-insensitively by default — ToolCall.DefaultArgumentOptions), HasArgument(name), GetArgumentsAsJson()
  • One answer per input - detection, HasToolCalls and Parse agree: text that is not JSON (a plain-text answer) has no tool calls; a call whose arguments are not a JSON object throws FormatException naming the tool; a streaming chunk (choices[].delta) counts only for calls complete within it — accumulate the stream first
  • Result formatting - IToolCallParser.FormatResults(IEnumerable<ToolCallResult>) with ToolCallResult.Success(...) / Failure(...); for Gemini's Interactions surface use GoogleToolCallParser.FormatResults(results, GoogleSurface.Interactions)
  • Format categories - Provider.GetFormat(), IsOpenAICompatible(), IsAnthropicCompatible(), GetDocumentationUrl(); ToolCallParserFactory.GetRegisteredProviders() / GetOpenAICompatibleProviders() list what the factory serves
  • Extensible - implement IToolCallParser (override CanParse to control recognition; it defaults to HasToolCalls) and call it directly — the factory is a fixed set, not a registry

Installation

dotnet add package ToolCallParser

Quick Start

Auto-Detection Parsing

using ToolCallParser;

// Parse tool calls with automatic provider detection
var response = """
{
  "choices": [{
    "message": {
      "tool_calls": [{
        "id": "call_abc123",
        "type": "function",
        "function": {
          "name": "get_weather",
          "arguments": "{\"location\":\"Tokyo\",\"unit\":\"celsius\"}"
        }
      }]
    }
  }]
}
""";

var toolCalls = ToolCallParserFactory.Parse(response);

foreach (var call in toolCalls)
{
    Console.WriteLine($"Tool: {call.Name}");
    Console.WriteLine($"ID: {call.Id}");
    Console.WriteLine($"Args: {call.Arguments}");
}

Provider-Specific Parsing

// OpenAI-compatible (Mistral, xAI, DeepSeek, or any OpenAI-compatible endpoint)
var openAiParser = ToolCallParserFactory.GetParser(Provider.OpenAI);

// Anthropic Claude
var claudeParser = ToolCallParserFactory.GetParser(Provider.Anthropic);

// Google Gemini
var geminiParser = ToolCallParserFactory.GetParser(Provider.Google);

// AWS Bedrock
var bedrockParser = ToolCallParserFactory.GetParser(Provider.Bedrock);

// Cohere Command R
var cohereParser = ToolCallParserFactory.GetParser(Provider.Cohere);

Supported Providers

Commercial Cloud

Provider Format Documentation
OpenAI tool_calls (Chat Completions) + function_call output items (Responses API) docs
Azure OpenAI OpenAI-compatible docs
Anthropic (Claude) tool_use blocks docs
Google Gemini functionCall parts (generateContent) + function_call steps (Interactions API) docs
xAI (Grok) OpenAI-compatible docs
Mistral OpenAI-compatible docs
Cohere Unique format docs
DeepSeek OpenAI-compatible docs
AWS Bedrock toolUse blocks (Converse); also exposes OpenAI-compatible Chat Completions + Responses endpoints, which detect as Provider.OpenAI docs

Open Source / Self-Hosted

Self-hosted runtimes speak the OpenAI tool-calling wire format, so they are handled by a single Provider.OpenAICompatible value rather than a per-runtime enum member:

// Ollama, vLLM, LM Studio, LocalAI, TGI, GpuStack, Qwen, and any other OpenAI-compatible server
var parser = ToolCallParserFactory.GetParser(Provider.OpenAICompatible);

Auto-detection (ToolCallParserFactory.Parse) also classifies these responses as the OpenAI format, so an explicit provider is only needed when you skip detection.

Format Categories

// Check format compatibility
var provider = Provider.OpenAICompatible;

if (provider.IsOpenAICompatible())
{
    Console.WriteLine("Uses OpenAI tool_calls format");
}

if (provider.IsAnthropicCompatible())
{
    Console.WriteLine("Uses Anthropic tool_use format");
}

// Get documentation URL
var docUrl = provider.GetDocumentationUrl();

Response Format Examples

OpenAI Format (Most Common)

{
  "choices": [{
    "message": {
      "tool_calls": [{
        "id": "call_abc123",
        "type": "function",
        "function": {
          "name": "get_weather",
          "arguments": "{\"location\":\"Tokyo\"}"
        }
      }]
    }
  }]
}

Anthropic Format

{
  "content": [
    {"type": "text", "text": "I'll check the weather."},
    {
      "type": "tool_use",
      "id": "toolu_01XYZ",
      "name": "get_weather",
      "input": {"location": "Paris"}
    }
  ],
  "stop_reason": "tool_use"
}

Google Gemini Format

{
  "candidates": [{
    "content": {
      "parts": [{
        "functionCall": {
          "name": "get_weather",
          "args": {"location": "London"}
        }
      }]
    }
  }]
}

AWS Bedrock Format

{
  "output": {
    "message": {
      "content": [{
        "toolUse": {
          "toolUseId": "tooluse_abc",
          "name": "get_weather",
          "input": {"location": "NYC"}
        }
      }]
    }
  },
  "stopReason": "tool_use"
}

Tool Calls Written as Text

var text = """
    Saving it.
    <tool_call>
    {"name": "write_file", "arguments": {"path": "a.txt"}}
    </tool_call>
    """;
var read = ToolCallMarkup.Read(text);

Console.WriteLine(read.Prefix); // "Saving it."
foreach (var block in read.Blocks.Where(b => b.IsWellFormed))
{
    // A JSON block gives Arguments (a JSON object); a Qwen3-Coder function block gives Parameters as written.
    Console.WriteLine($"{block.Name}: {block.Arguments ?? string.Join(", ", block.Parameters.Select(p => p.Key))}");
}

Whether a block is a call to run — the tool is one you offered, calls are allowed — is your decision, as is typing a function block's parameter text against the tool's schema.

Working with Arguments

var toolCall = toolCalls[0];

// Get a specific argument
var location = toolCall.GetArgument<string>("location");

// Get all arguments as a typed object
var weather = toolCall.GetArguments<WeatherArgs>();

// Check if argument exists
if (toolCall.HasArgument("unit"))
{
    var unit = toolCall.GetArgument<string>("unit");
}

public record WeatherArgs(string Location, string Unit = "celsius");

Formatting Results

// Create tool results
var results = new[]
{
    ToolCallResult.Success("call_abc123", "Temperature: 22°C", "get_weather"),
    ToolCallResult.Failure("call_def456", "Location not found", "get_weather")
};

// Format for OpenAI
var openAiParser = ToolCallParserFactory.GetParser(Provider.OpenAI);
var openAiResults = openAiParser.FormatResults(results);

// Format for Anthropic
var anthropicParser = ToolCallParserFactory.GetParser(Provider.Anthropic);
var claudeResults = anthropicParser.FormatResults(results);

// Format for Google Gemini (generateContent)
var geminiParser = ToolCallParserFactory.GetParser(Provider.Google);
var geminiResults = geminiParser.FormatResults(results);

Google serves tool calling through two wire formats, and a result payload carries nothing that says which one it is headed for — so for the Interactions surface the caller names it. The call_id sent back is the step id that parsing preserved:

using ToolCallParser.Parsers;

var parser = new GoogleToolCallParser();
var interactionsResults = parser.FormatResults(results, GoogleSurface.Interactions);
// [{ "type": "function_result", "call_id": "...", "name": "...", "result": {...}, "is_error": false }]

Provider Detection

// Auto-detect provider from response
var provider = ToolCallParserFactory.DetectProvider(jsonResponse);

switch (provider)
{
    case Provider.OpenAI:
        Console.WriteLine("OpenAI format detected");
        break;
    case Provider.Anthropic:
        Console.WriteLine("Anthropic format detected");
        break;
    case Provider.Google:
        Console.WriteLine("Gemini format detected");
        break;
    case Provider.Bedrock:
        Console.WriteLine("AWS Bedrock format detected");
        break;
    case Provider.Cohere:
        Console.WriteLine("Cohere format detected");
        break;
    case Provider.Auto:
        Console.WriteLine("Could not auto-detect");
        break;
}

Custom Parsers

Implement IToolCallParser and use it directly. The built-in ToolCallParserFactory exposes a fixed set of parsers and is not a mutable registry — a custom parser is just a class you own:

public class CustomProviderParser : IToolCallParser
{
    public Provider Provider => Provider.OpenAICompatible;
    public IReadOnlyList<ToolCall> Parse(string response) => throw new NotImplementedException(); // your parsing
    public IReadOnlyList<ToolCall> Parse(JsonElement element) => throw new NotImplementedException(); // your parsing
    public bool HasToolCalls(string response) => throw new NotImplementedException(); // your parsing
    public bool HasToolCalls(JsonElement element) => throw new NotImplementedException(); // your parsing
    // Optional: override CanParse for auto-detection; defaults to HasToolCalls.
    public bool CanParse(JsonElement element) => throw new NotImplementedException(); // your parsing
    public string FormatResults(IEnumerable<ToolCallResult> results) => throw new NotImplementedException(); // your parsing
}

// Use it directly
var parser = new CustomProviderParser();
var toolCalls = parser.Parse(response);

API Reference

ToolCall

public sealed record ToolCall
{
    public required string Id { get; init; }
    public required string Name { get; init; }
    public required string Arguments { get; init; }  // JSON string

    public JsonDocument GetArgumentsAsJson();
    public T? GetArguments<T>(JsonSerializerOptions? options = null);
    public T? GetArgument<T>(string name, JsonSerializerOptions? options = null);
    public bool HasArgument(string name);
}

ToolCallResult

public sealed record ToolCallResult
{
    public required string ToolCallId { get; init; }
    public string? ToolName { get; init; }
    public required string Content { get; init; }
    public bool IsSuccess { get; init; } = true;
    public string? ErrorMessage { get; init; }

    public static ToolCallResult Success(string toolCallId, string content, string? toolName = null);
    public static ToolCallResult Failure(string toolCallId, string errorMessage, string? toolName = null);
}

ProviderExtensions

public static class ProviderExtensions
{
    public static ProviderFormat GetFormat(this Provider provider);
    public static bool IsOpenAICompatible(this Provider provider);
    public static bool IsAnthropicCompatible(this Provider provider);
    public static string? GetDocumentationUrl(this Provider provider);
}

Requirements

  • .NET 10.0
  • System.Text.Json (included in .NET)

Documentation

License

MIT License - see LICENSE for details.

Contributing

Contributions welcome! Especially for adding support for new providers.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/new-provider)
  3. Add parser in src/ToolCallParser/Parsers/
  4. Add tests in tests/ToolCallParser.Tests/
  5. Update Provider enum and factory
  6. Submit a Pull Request

See docs/tool-call-format-guide.md for detailed instructions.


Made with care by iyulab

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.

NuGet packages (3)

Showing the top 3 NuGet packages that depend on ToolCallParser:

Package Downloads
IronHive.Agent

IronHive Agent - Reusable agent layer for AI-powered CLI tools

IronHive.Cli.Core

IronHive CLI Core - Agent loop, tools, session management, and provider integrations for building AI-powered CLI tools

IronHive.Host.Core

IronHive Host Core - Agent loop, tools, session management, and provider integrations for building reusable AI agent hosts (CLI, server, embedded)

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.7.0 1,396 10/19/2026
0.6.5 129 9/30/2026
0.6.4 117 9/30/2026
0.6.3 109 9/28/2026
0.6.2 123 9/17/2026
0.6.1 136 9/9/2026
0.6.0 190 8/2/2026
0.5.0 139 8/2/2026
0.4.0 237 7/21/2026
0.3.1 142 7/6/2026
0.3.0 141 7/6/2026
0.2.1 1,633 2/20/2026
0.1.0 198 1/28/2026