Recursive AI

IChatClient for Claude and Gemini 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.

Use Microsoft.Extensions.AI’s IChatClient with Anthropic (Claude) and Gemini.

Terminal
dotnet add package \
    RecursiveAI.OneLlm.Extensions.AI

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

C#
using Microsoft.Extensions.AI;
using OneLlm;
using OneLlm.Extensions.AI;
var runtime = new Runtime();
using var ai = runtime.AsChatClient(
    "claude-sonnet");
var reply = await ai.GetResponseAsync("Hi");
Console.WriteLine(reply.Text);

Calls Claude using ANTHROPIC_API_KEY.

Download this example (.zip): unzip, then dotnet run.

The full program

The download runs offline with canned replies, no API key needed.

claude-sonnet and gemini-flash are shipped catalog aliases: the pinned catalog maps them to claude-sonnet-5-5 and gemini-3.8-flash, respectively (model names).

The download registers both scripted providers before creating the client. The default catalog model is claude-sonnet; a per-request ChatOptions.ModelId selects gemini-flash. These are catalog aliases, not a claim that the two providers accept identical options or return identical answers.

C#
using Microsoft.Extensions.AI;
using OneLlm;
using OneLlm.Extensions.AI;
using OneLlm.Testing;
 
var runtime = new Runtime();
var claude = new ScriptedProvider("anthropic");
var gemini = new ScriptedProvider("gemini");
runtime.Providers.Set("anthropic", claude);
runtime.Providers.Set("gemini", gemini);
claude.EnqueueText("Hello from scripted Claude.");
gemini.EnqueueText("Hello from scripted Gemini.");
 
using IChatClient client = runtime.AsChatClient("claude-sonnet");
Console.WriteLine((await client.GetResponseAsync("Say hello.")).Text);
var options = new ChatOptions { ModelId = "gemini-flash" };
Console.WriteLine((await client.GetResponseAsync("Say hello.", options)).Text);
Console.WriteLine($"Anthropic requests: {claude.Requests.Count}");
Console.WriteLine($"Gemini requests: {gemini.Requests.Count}");

dotnet run prints:

Text
Hello from scripted Claude.
Hello from scripted Gemini.
Anthropic requests: 1
Gemini requests: 1

An unknown model raises ModelNotFoundException; the scripted provider also throws if the program makes an extra request with no reply queued.

With a real model

The pinned getting-started docs name ANTHROPIC_API_KEY for Anthropic and GEMINI_API_KEY for Gemini. Set those environment variables to your own credentials outside source code. To use only Claude, set ANTHROPIC_API_KEY and omit the Gemini call.

In the full program, remove both ScriptedProvider declarations, both runtime.Providers.Set(...) registrations, and both EnqueueText calls. Also remove the two final Requests.Count print statements: those inspect the stand-ins. The runtime setup becomes:

C#
using OneLlm;
var runtime = new Runtime();

Keep the AsChatClient, GetResponseAsync and ChatOptions.ModelId calls unchanged. The built-in providers read their environment keys on the first request, as the docs describe. The snippet at the top uses this real-provider setup, not the canned replies.

This live-model path was not run here: no API key was used. The output shown above comes from the scripted providers, not from Anthropic or Gemini.

Variations

Stream text through the same interface. Queue a streamed reply and replace the response call with:

C#
claude.Enqueue(ScriptStep.Text("Hel", "lo"));
await foreach (var update in client.GetStreamingResponseAsync("Say hello."))
    Console.Write(update.Text);
Console.WriteLine();

After the full program’s original calls have consumed their replies, this prints Hello.


The short answer

Install RecursiveAI.OneLlm.Extensions.AI and call runtime.AsChatClient("claude-sonnet"). The returned IChatClient uses OneLLM’s model catalog and provider registry. Use ChatOptions.ModelId to select another catalog model for one request, as the download does with gemini-flash.

The runnable example exercises the bridge path without contacting either service. Its replies explicitly say “scripted,” and its counters show one request going to each registered provider. That avoids confusing an interface demonstration with a live Anthropic or Gemini integration test.

How do I use the Anthropic SDK in C#?

For the IChatClient interface shown here, install RecursiveAI.OneLlm.Extensions.AI, set ANTHROPIC_API_KEY, and call new Runtime().AsChatClient("claude-sonnet"); OneLLM supplies the Anthropic provider behind that interface.

With OneLLM’s bridge, application code can consume IChatClient while OneLLM handles the provider-facing request. The package references the core library and Microsoft.Extensions.AI.Abstractions. Its bridge documentation describes both directions: exposing a OneLLM model as a client, and registering an existing client as a OneLLM provider.

For a live application, remove the scripted-provider registrations and configure the real provider credentials outside source code. The download never reads a key. OneLLM’s release results say conformance uses recorded fixtures and Anthropic was not run live; this page does not extend those claims. Follow the provider setup documentation for the connection you intend to use.

A shared interface does not erase provider differences. The catalog adapts requests to the chosen model, and dropped options appear as warnings. Read OneLlmProperties.Warnings when you depend on an option’s effect. Do not assume every setting on ChatOptions is forwarded unchanged: the docs list fields that are not carried, including ConversationId.

Use Gemini through the same interface

Select the Gemini model through the same runtime and client abstraction. The full program overrides ModelId for the second call, leaving the client’s default Claude model intact. Creating a separate runtime.AsChatClient("gemini-flash") is also appropriate when different services should have different defaults.

Keep conversation messages in your application when it owns the history. A single GetResponseAsync("Say hello.") call does not make every later call share a conversation. Pass the messages you want to send, and choose one history policy rather than assuming that switching model IDs also transfers server-side conversation state.

The bridge carries text, reasoning and tool calls through the corresponding content types. For streaming, consume GetStreamingResponseAsync to its end if you need the complete result and final usage. The docs say streaming ends with one update carrying the finish reason and one usage item, so aggregation counts usage once.

Tools need a separate execution decision. This client does not run functions merely because a response contains a function call. The bridge docs recommend function-invoking middleware or a OneLLM tool loop when execution is desired. Treat that as an explicit application design choice, not an incidental feature of changing the model name. For this recipe’s text-only request, neither provider is given tools.

All onellm recipes