Recursive AI

Stream an LLM response in C#

OneLLM is a free, open-source LLM and agent library for .NET from Recursive AI. MIT or Apache-2.0, no native dependencies.

Print response text as chunks arrive with await foreach. A live request needs your provider’s API key.

Terminal
dotnet add package RecursiveAI.OneLlm

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

Set OPENAI_API_KEY to your own OpenAI credential in your process environment, outside source code (key setup). Then stream a live response:

C#
using OneLlm;
 
await foreach (var piece in Llm.StreamTextAsync(
    "gpt-6-luna", "Say hello"))
    Console.Write(piece);
Console.WriteLine();

Prints text as the model generates it. gpt-6-luna comes from OneLLM’s shipped model catalog, which maps it to OpenAI’s model and provider configuration.

The download runs offline, no key needed.

Unzip the example, then dotnet run.

The live snippet compiled but was not run against a paid API. The printed output below comes only from the offline run.

Variations

Run it offline or in tests

The download runs the scripted provider without a key or network request. The provider supplies three chunks: “Hello”, “ from” and “ the stream!”. The model name selects the catalog route, but the registered provider answers offline; this is not a live model response. Exceptions can occur while iterating, so catch them around the await foreach loop, not only around creation of the enumerable.

C#
using OneLlm;
using OneLlm.Testing;
 
var provider = new ScriptedProvider("openai")
    .Enqueue(ScriptStep.Text("Hello", " from", " the stream!"));
var runtime = new Runtime();
runtime.Providers.Set("openai", provider);
Llm.Default = runtime;
await foreach (string piece in Llm.StreamTextAsync("gpt-6-luna", "Say hello"))
    Console.Write(piece);
Console.WriteLine();
Text
Hello from the stream!

Append this snippet to the program above to test cancellation and a provider failure without a service. RateLimitException is OneLLM’s typed rate-limit error in the OneLlm namespace, already imported above:

C#
provider.Enqueue(ScriptStep.Text("not printed"));
using var stop = new CancellationTokenSource();
stop.Cancel();
try
{
    await foreach (string piece in Llm.StreamTextAsync(
        "gpt-6-luna", "Say hello", stop.Token))
        Console.Write(piece);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Canceled");
}
provider.Enqueue(ScriptStep.Fail(
    new RateLimitException("openai", "Demo rate limit")));
try
{
    await foreach (string piece in Llm.StreamTextAsync(
        "gpt-6-luna", "Say hello"))
        Console.Write(piece);
}
catch (RateLimitException)
{
    Console.WriteLine("Rate limited");
}
Text
Canceled
Rate limited

The short answer

Iterate Llm.StreamTextAsync(model, prompt) with await foreach and write each returned string without adding a newline. Add the final newline after enumeration completes.

How do I stream an LLM response in C#?

StreamTextAsync returns an IAsyncEnumerable<string>, not a task containing the final answer. Each iteration gives you another piece of response text. Write or append that piece as it arrives instead of waiting to collect the entire answer before updating your output.

The example uses Console.Write(piece) because WriteLine would insert a line break after every chunk. A chunk is not necessarily a sentence or a line. The scripted answer deliberately includes leading spaces in its second and third pieces, so concatenating them produces the exact text shown. Do not trim each piece: removing boundary spaces can join words together.

How can I test an LLM streaming response?

Queue ScriptStep.Text with the exact pieces you want your application to receive. ScriptedProvider replays those pieces and records requests. It is part of the published package and is documented in OneLLM’s happy-path guide. This lets you test concatenation and output handling without relying on a changing generated answer.

Test failure and cancellation as well as the successful path. The variation queues a response, passes an already-canceled token, and then queues a typed provider error for a separate request. Neither branch contacts a service. An application may have already displayed partial text when a live stream fails; keep that possibility distinct from a completed response in your own UI.

The Runtime holds the provider registry, and Llm.Default makes it available to the static API. Registering the scripted provider as "openai" means that this catalog model’s route resolves to the offline implementation. The model name does not make this a real request to a service. No real key is read or needed by this program.

The sample changes the process-wide Llm.Default. Tests doing that should not run in parallel with other tests that also change it. Use an isolated runtime arrangement when multiple independently configured operations share a process.

How do I cancel a streaming response?

Pass a CancellationToken to StreamTextAsync and catch OperationCanceledException around enumeration. The token controls the request or wait; simply abandoning your displayed text is not the same as asking the operation to stop. The error documentation describes cancellation and the library’s typed failures.

Catch provider errors in the same place. The variation demonstrates RateLimitException; real applications can decide whether to show a retry option or stop. Do not automatically append a second attempt to partial text as if it were one uninterrupted answer.

Text streaming and conversation persistence are separate tasks. StreamTextAsync yields text pieces only. If your caller needs all stream events and the final result, OneLLM also exposes StreamAsync and its result task. If it needs a saved conversation to resume later, implement that state separately rather than treating console output as a chat store.

All onellm recipes