LlmTornado 3.5.25

There is a newer version of this package available.
See the version list below for details.
dotnet add package LlmTornado --version 3.5.25
                    
NuGet\Install-Package LlmTornado -Version 3.5.25
                    
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="LlmTornado" Version="3.5.25" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="LlmTornado" Version="3.5.25" />
                    
Directory.Packages.props
<PackageReference Include="LlmTornado" />
                    
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 LlmTornado --version 3.5.25
                    
#r "nuget: LlmTornado, 3.5.25"
                    
#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.
#addin nuget:?package=LlmTornado&version=3.5.25
                    
Install as a Cake Addin
#tool nuget:?package=LlmTornado&version=3.5.25
                    
Install as a Cake Tool

LlmTornado LlmTornado.Toolkit LlmTornado.Mcp LlmTornado.Contrib

🌪️ LLM Tornado

Build AI agents and multi-agent systems in minutes with one toolkit. Use any Provider, switch without refactoring anything. Deploy today.

Key Features:

  • Use any model from any Provider: All you need to know is the model's name; we handle the rest. Built-in: Anthropic, Azure, Cohere, DeepInfra, DeepSeek, Google, Groq, Mistral, Ollama, OpenAI, OpenRouter, Perplexity, Voyage, vLLM, xAI. Check the full Feature Matrix here.
  • Multi-Agent Systems: Toolkit for the orchestration of multiple collaborating specialist agents.
  • Maximize request success rate: We keep track of which parameters are supported by which models, how long the reasoning context can be, etc., and silently modify your requests to comply with rules enforced by a diverse set of Providers.
  • Leverage unique capabilities: Non-standard features from all major Providers are carefully mapped, documented, and ready to use via strongly-typed code.
  • Fully multimodal: Text, image, video, document, URL, and audio inputs are supported.
  • MCP compatible: Seamlessly integrate Model Context Protocol using the official .NET SDK and LlmTornado.Mcp adapter.
  • Enterprise ready: Observability as a first-class citizen. Preview any request before committing to it. Streamlined usage information as a monitoring backbone. Flexibility with multilingual apps in mind.

⭐ With a few lines of code you can:

... and a lot more! Now, instead of relying on one LLM provider, you can combine the unique strengths of many.

⚡Getting Started

Install LLM Tornado via NuGet:

dotnet add package LlmTornado.Toolkit

Optional addons:

dotnet add package LlmTornado.Mcp # Model Context Protocol (MCP) integration
dotnet add package LlmTornado.Contrib # productivity, quality of life enhancements

🪄 Quick Inference

Inferencing across multiple providers is as easy as changing the ChatModel argument. Tornado instance can be constructed with multiple API keys, the correct key is then used based on the model automatically:

TornadoApi api = new TornadoApi([
    // note: delete lines with providers you won't be using
    new (LLmProviders.OpenAi, "OPEN_AI_KEY"),
    new (LLmProviders.Anthropic, "ANTHROPIC_KEY"),
    new (LLmProviders.Cohere, "COHERE_KEY"),
    new (LLmProviders.Google, "GOOGLE_KEY"),
    new (LLmProviders.Groq, "GROQ_KEY"),
    new (LLmProviders.DeepSeek, "DEEP_SEEK_KEY"),
    new (LLmProviders.Mistral, "MISTRAL_KEY"),
    new (LLmProviders.XAi, "XAI_KEY"),
    new (LLmProviders.Perplexity, "PERPLEXITY_KEY"),
    new (LLmProviders.Voyage, "VOYAGE_KEY"),
    new (LLmProviders.DeepInfra, "DEEP_INFRA_KEY"),
    new (LLmProviders.OpenRouter, "OPEN_ROUTER_KEY")
]);

// this sample iterates a bunch of models, gives each the same task, and prints results.
List<ChatModel> models = [
    ChatModel.OpenAi.O3.Mini, ChatModel.Anthropic.Claude37.Sonnet,
    ChatModel.Cohere.Command.RPlus, ChatModel.Google.Gemini.Gemini2Flash001,
    ChatModel.Groq.Meta.Llama370B, ChatModel.DeepSeek.Models.Chat,
    ChatModel.Mistral.Premier.MistralLarge, ChatModel.XAi.Grok.Grok2241212,
    ChatModel.Perplexity.Sonar.Default
];

foreach (ChatModel model in models)
{
    string? response = await api.Chat.CreateConversation(model)
        .AppendSystemMessage("You are a fortune teller.")
        .AppendUserInput("What will my future bring?")
        .GetResponse();

    Console.WriteLine(response);
}

💡 Instead of passing in a strongly typed model, you can pass a string instead: await api.Chat.CreateConversation("gpt-4o"), Tornado will automatically resolve the provider.

❄️ Vendor Extensions

Tornado has a powerful concept of VendorExtensions which can be applied to various endpoints and are strongly typed. Many Providers offer unique/niche APIs, often enabling use cases otherwise unavailable. For example, let's set a reasoning budget for Anthropic's Claude 3.7:

public static async Task AnthropicSonnet37Thinking()
{
    Conversation chat = Program.Connect(LLmProviders.Anthropic).Chat.CreateConversation(new ChatRequest
    {
        Model = ChatModel.Anthropic.Claude37.Sonnet,
        VendorExtensions = new ChatRequestVendorExtensions(new ChatRequestVendorAnthropicExtensions
        {
            Thinking = new AnthropicThinkingSettings
            {
                BudgetTokens = 2_000,
                Enabled = true
            }
        })
    });
    
    chat.AppendUserInput("Explain how to solve differential equations.");

    ChatRichResponse blocks = await chat.GetResponseRich();

    if (blocks.Blocks is not null)
    {
        foreach (ChatRichResponseBlock reasoning in blocks.Blocks.Where(x => x.Type is ChatRichResponseBlockTypes.Reasoning))
        {
            Console.ForegroundColor = ConsoleColor.DarkGray;
            Console.WriteLine(reasoning.Reasoning?.Content);
            Console.ResetColor();
        }

        foreach (ChatRichResponseBlock reasoning in blocks.Blocks.Where(x => x.Type is ChatRichResponseBlockTypes.Message))
        {
            Console.WriteLine(reasoning.Message);
        }
    }
}

🔮 Self-Hosted/Custom Providers

Instead of consuming commercial APIs, one can easily roll their inference servers with a plethora of available tools. Here is a simple demo for streaming response with Ollama, but the same approach can be used for any custom provider:

public static async Task OllamaStreaming()
{
    TornadoApi api = new TornadoApi(new Uri("http://localhost:11434")); // default Ollama port, API key can be passed in the second argument if needed
    
    await api.Chat.CreateConversation(new ChatModel("falcon3:1b")) // <-- replace with your model
        .AppendUserInput("Why is the sky blue?")
        .StreamResponse(Console.Write);
}

If you need more control over requests, for example, custom headers, you can create an instance of a built-in Provider. This is useful for custom deployments like Amazon Bedrock, Vertex AI, etc.

TornadoApi tornadoApi = new TornadoApi(new AnthropicEndpointProvider
{
    Auth = new ProviderAuthentication("ANTHROPIC_API_KEY"),
    UrlResolver = (endpoint, url) => "https://api.anthropic.com/v1/{0}{1}",
    RequestResolver = (request, data, streaming) =>
    {
        // by default, providing a custom request resolver omits beta headers
        // request is HttpRequestMessage, data contains the payload
    }
});

https://github.com/user-attachments/assets/de62f0fe-93e0-448c-81d0-8ab7447ad780

🔎 Advanced Inference

Streaming

Tornado offers three levels of abstraction, trading more details for more complexity. The simple use cases where only plaintext is needed can be represented in a terse format:

await api.Chat.CreateConversation(ChatModel.Anthropic.Claude3.Sonnet)
    .AppendSystemMessage("You are a fortune teller.")
    .AppendUserInput("What will my future bring?")
    .StreamResponse(Console.Write);

The levels of abstraction are:

  • Response (string for chat, float[] for embeddings, etc.)
  • ResponseRich (tools, modalities, metadata such as usage)
  • ResponseRichSafe (same as level 2, guaranteed not to throw on network level, for example, if the provider returns an internal error or doesn't respond at all)

Streaming with Rich content (tools, images, audio..)

When plaintext is insufficient, switch to StreamResponseRich or GetResponseRich() APIs. Tools requested by the model can be resolved later and never returned to the model. This is useful in scenarios where we use the tools without intending to continue the conversation:

//Ask the model to generate two images, and stream the result:
public static async Task GoogleStreamImages()
{
    Conversation chat = api.Chat.CreateConversation(new ChatRequest
    {
        Model = ChatModel.Google.GeminiExperimental.Gemini2FlashImageGeneration,
        Modalities = [ ChatModelModalities.Text, ChatModelModalities.Image ]
    });
    
    chat.AppendUserInput([
        new ChatMessagePart("Generate two images: a lion and a squirrel")
    ]);
    
    await chat.StreamResponseRich(new ChatStreamEventHandler
    {
        MessagePartHandler = async (part) =>
        {
            if (part.Text is not null)
            {
                Console.Write(part.Text);
                return;
            }

            if (part.Image is not null)
            {
                // In our tests this executes Chafa to turn the raw base64 data into Sixels
                await DisplayImage(part.Image.Url);
            }
        },
        BlockFinishedHandler = (block) =>
        {
            Console.WriteLine();
            return ValueTask.CompletedTask;
        },
        OnUsageReceived = (usage) =>
        {
            Console.WriteLine();
            Console.WriteLine(usage);
            return ValueTask.CompletedTask;
        }
    });
}

Tools with immediate resolve

Tools requested by the model can be resolved and the results returned immediately. This has the benefit of automatically continuing the conversation:

Conversation chat = api.Chat.CreateConversation(new ChatRequest
{
    Model = ChatModel.OpenAi.Gpt4.O,
    Tools =
    [
        new Tool(new ToolFunction("get_weather", "gets the current weather", new
        {
            type = "object",
            properties = new
            {
                location = new
                {
                    type = "string",
                    description = "The location for which the weather information is required."
                }
            },
            required = new List<string> { "location" }
        }))
    ]
})
.AppendSystemMessage("You are a helpful assistant")
.AppendUserInput("What is the weather like today in Prague?");

ChatStreamEventHandler handler = new ChatStreamEventHandler
{
  MessageTokenHandler = (x) =>
  {
      Console.Write(x);
      return Task.CompletedTask;
  },
  FunctionCallHandler = (calls) =>
  {
      calls.ForEach(x => x.Result = new FunctionResult(x, "A mild rain is expected around noon.", null));
      return Task.CompletedTask;
  },
  AfterFunctionCallsResolvedHandler = async (results, handler) => { await chat.StreamResponseRich(handler); }
};

await chat.StreamResponseRich(handler);

Tools with deferred resolve

Instead of resolving the tool call, we can postpone/quit the conversation. This is useful for extractive tasks, where we care only for the tool call:

Conversation chat = api.Chat.CreateConversation(new ChatRequest
{
    Model = ChatModel.OpenAi.Gpt4.Turbo,
    Tools = new List<Tool>
    {
        new Tool
        {
            Function = new ToolFunction("get_weather", "gets the current weather")
        }
    },
    ToolChoice = new OutboundToolChoice(OutboundToolChoiceModes.Required)
});

chat.AppendUserInput("Who are you?"); // user asks something unrelated, but we force the model to use the tool
ChatRichResponse response = await chat.GetResponseRich(); // the response contains one block of type Function

GetResponseRichSafe() API is also available, which is guaranteed not to throw on the network level. The response is wrapped in a network-level wrapper, containing additional information. For production use cases, either use try {} catch {} on all the HTTP request-producing Tornado APIs, or use the safe APIs.

🌐 MCP

To use Model Context Protocol, install LlmTornado.Mcp adapter. After that, new interop methods become available on ModelContextProtocol types:

// your clientTransport, for example StdioClientTransport
await using IMcpClient mcpClient = await McpClientFactory.CreateAsync(clientTransport);

// 1. fetch tools
List<Tool> tools = await mcpClient.ListTornadoToolsAsync();

// 2. create a conversation, pass available tools
TornadoApi api = new TornadoApi(LLmProviders.OpenAi, apiKeys.OpenAi);
Conversation conversation = api.Chat.CreateConversation(new ChatRequest
{
    Model = ChatModel.OpenAi.Gpt41.V41,
    Tools = tools
});

// 3. let model pick a tool, resolve the result
await conversation
    .AddSystemMessage("You are a helpful assistant")
    .AddUserMessage("What is the weather like in Prague?")
    .GetResponseRich(calls =>
    {
        foreach (FunctionCall call in calls)
        {
            call.Resolve(new
            {
                weather = "heavy rain is expected"
            });
        }

        return Task.CompletedTask;
    });

// 4. stream the response
await conversation.StreamResponse(Console.Write);

A complete example with Client & Server is available here.

🧰 Toolkit

Tornado includes powerful abstractions in the LlmTornado.Toolkit package, allowing rapid development of applications, while avoiding many design pitfalls. Scalability and tuning-friendly code design are at the core of these abstractions.

ToolkitChat

ToolkitChat is a primitive for graph-based workflows, where edges move data and nodes execute functions. ToolkitChat supports streaming, rich responses, and chaining tool calls. Tool calls are provided via ChatFunction or ChatPlugin (an envelope with multiple tools). Many overloads accept a primary and a secondary model acting as a backup, this zig-zag strategy overcomes temporary downtime in APIs better than simple retrying of the same model. All tool calls are strongly typed and strict by default. For providers, where a strict JSON schema is not supported (Anthropic, for example), prefill with { is used as a fallback. Call can be marked as non-strict by simply changing a parameter.

class DemoAggregatedItem
{
    public string Name { get; set; }
    public string KnownName { get; set; }
    public int Quantity { get; set; }
}

string sysPrompt = "aggregate items by type";
string userPrompt = "three apples, one cherry, two apples, one orange, one orange";

await ToolkitChat.GetSingleResponse(api, ChatModel.Google.Gemini.Gemini2Flash001, ChatModel.OpenAi.Gpt41.V41Mini, sysPrompt, new ChatFunction([
    new ChatFunctionParam("items", new ChatFunctionTypeListTypedObject("aggregated items", true, [
        new ChatFunctionParam("name", "name of the item", true, ChatFunctionAtomicParamTypes.String),
        new ChatFunctionParam("quantity", "aggregated quantity", true, ChatFunctionAtomicParamTypes.Int),
        new ChatFunctionParam("known_name", new ChatFunctionTypeEnum("known name of the item", true, [ "apple", "cherry", "orange", "other" ]))
    ]))
], async (args, ctx) =>
{
    if (!args.ParamTryGet("items", out List<DemoAggregatedItem>? items) || items is null)
    {
        return new ChatFunctionCallResult(ChatFunctionCallResultParameterErrors.MissingRequiredParameter, "items");
    }
    
    Console.WriteLine("Aggregated items:");

    foreach (DemoAggregatedItem item in items)
    {
        Console.WriteLine($"{item.Name}: {item.Quantity}");
    }
    
    return new ChatFunctionCallResult();
}), userPrompt); // temp defaults to 0, output length to 8k

/*
Aggregated items:
apple: 5
cherry: 1
orange: 2
*/

👉 Why Tornado?

  • 50,000+ installs on NuGet (previous names Lofcz.Forks.OpenAI, OpenAiNg).
  • Used in award-winning commercial projects, processing > 100B tokens monthly.
  • Covered by 250+ tests.
  • Great performance.
  • The license will never change.

Star History Chart

📚 Contributing

PRs are welcome! We are accepting new Provider implementations, contributions towards a 100 % green Feature Matrix, and, after public discussion, new abstractions.

License

This library is licensed under the MIT license. 💜

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 is compatible.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (6)

Showing the top 5 NuGet packages that depend on LlmTornado:

Package Downloads
LlmTornado.Contrib

Provides extra functionality to LlmTornado.

LlmTornado.Toolkit

Package Description

LlmTornado.Internal.OpenRouter

Package Description

LlmTornado.Demo

Package Description

LlmTornado.Mcp

Package Description

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.6.0 14 6/28/2025
3.5.26 16 6/28/2025
3.5.25 17 6/27/2025
3.5.24 85 6/26/2025
3.5.23 123 6/25/2025
3.5.22 129 6/24/2025
3.5.21 225 6/20/2025
3.5.20 137 6/19/2025
3.5.19 174 6/18/2025
3.5.18 187 6/16/2025
3.5.17 133 6/15/2025
3.5.16 132 6/15/2025
3.5.15 138 6/15/2025
3.5.14 444 6/11/2025
3.5.13 322 6/10/2025
3.5.12 108 6/6/2025
3.5.11 77 6/6/2025
3.5.10 100 6/6/2025
3.5.9 104 6/6/2025
3.5.8 166 6/4/2025
3.5.7 163 6/2/2025
3.5.6 156 6/2/2025
3.5.5 145 5/31/2025
3.5.4 255 5/23/2025
3.5.3 150 5/22/2025
3.5.2 172 5/21/2025
3.5.1 517 5/13/2025
3.5.0 584 5/9/2025
3.4.22 297 5/8/2025
3.4.21 149 5/6/2025
3.4.20 270 4/28/2025
3.4.19 262 4/24/2025
3.4.18 214 4/23/2025
3.4.17 343 4/19/2025
3.4.16 107 4/19/2025
3.4.15 207 4/16/2025
3.4.14 215 4/16/2025
3.4.13 186 4/16/2025
3.4.12 253 4/15/2025
3.4.11 213 4/13/2025
3.4.10 175 4/10/2025
3.4.9 131 4/5/2025
3.4.8 1,952 3/27/2025
3.4.7 4,328 3/21/2025
3.4.6 138 3/21/2025
3.4.5 146 3/20/2025
3.4.4 562 3/17/2025
3.4.3 145 3/15/2025
3.4.2 73 3/15/2025
3.4.1 67 3/15/2025
3.4.0 69 3/15/2025
3.3.2 292 3/9/2025
3.3.1 278 3/7/2025
3.3.0 252 3/2/2025
3.2.8 119 2/28/2025
3.2.7 124 2/26/2025
3.2.6 1,185 2/25/2025
3.2.5 141 2/20/2025
3.2.4 138 2/17/2025
3.2.3 541 2/10/2025
3.2.2 180 2/7/2025
3.2.1 98 2/6/2025
3.2.0 119 2/4/2025
3.1.35 103 2/2/2025
3.1.34 102 1/31/2025
3.1.33 181 1/24/2025
3.1.32 121 1/23/2025
3.1.31 130 1/22/2025
3.1.30 141 1/17/2025
3.1.29 170 12/19/2024
3.1.28 101 12/19/2024
3.1.27 154 12/14/2024
3.1.26 446 11/22/2024
3.1.25 103 11/22/2024
3.1.24 116 11/21/2024
3.1.23 115 11/20/2024
3.1.22 120 11/20/2024
3.1.21 125 11/18/2024
3.1.20 108 11/18/2024
3.1.19 110 11/17/2024
3.1.18 103 11/16/2024
3.1.17 172 11/5/2024
3.1.16 108 11/4/2024
3.1.15 237 10/22/2024
3.1.14 366 9/14/2024
3.1.13 189 9/1/2024
3.1.12 193 8/20/2024
3.1.11 142 8/18/2024
3.1.10 136 8/6/2024
3.1.9 116 8/6/2024
3.1.8 124 7/24/2024
3.1.7 97 7/24/2024
3.1.6 100 7/23/2024
3.1.5 135 7/19/2024
3.1.4 117 7/19/2024
3.1.3 178 6/23/2024
3.1.2 148 6/15/2024
3.1.1 123 6/15/2024
3.1.0 111 6/15/2024
3.0.17 123 6/8/2024
3.0.16 114 6/8/2024
3.0.15 174 5/21/2024
3.0.14 151 5/21/2024
3.0.13 147 5/20/2024
3.0.11 129 5/18/2024
3.0.10 137 5/15/2024
3.0.9 154 5/15/2024
3.0.8 133 5/9/2024
3.0.7 157 5/5/2024
3.0.6 121 5/2/2024
3.0.5 121 5/1/2024
3.0.4 130 5/1/2024
3.0.3 114 5/1/2024
3.0.2 111 5/1/2024
3.0.1 137 4/27/2024
3.0.0 149 4/27/2024