LlmClient.Create
C# · package Toolnexus · SPEC §8
public sealed class LlmClient{ public static LlmClient Create(Options opts);
public sealed class Options { public string BaseUrl { get; set; } = ""; public string Style { get; set; } = "openai"; // "openai" | "anthropic" public string Model { get; set; } = ""; public string? ApiKey { get; set; } // from env if null public IDictionary<string, string>? Headers { get; set; } public string? SystemPrompt { get; set; } public int? MaxTurns { get; set; } // default 10 public Hooks? Hooks { get; set; }
// Resilience — see LlmClient.ErrorInfo public int? Retries { get; set; } // default 2 public IReadOnlyCollection<int>? RetryableStatuses { get; set; } public int? RetryBaseMs { get; set; } // default 500 public long? TimeoutMs { get; set; } // whole-run deadline; null = none public Func<ErrorInfo, Tier>? OnError { get; set; }
// Memory — see LlmClient.Conversation / IConversationStore public IConversationStore? Store { get; set; }
// Observability — see MetricEvent public Action<MetricEvent>? OnMetric { get; set; }
// Suspension (§10) — see LlmClient.WaitFor public Func<Request, Task<Answer>>? WaitFor { get; set; }
// Request shaping (§8 Gap 1/2) public IReadOnlyDictionary<string, object?>? RequestParams { get; set; } public Func<IDictionary<string, object?>, IDictionary<string, object?>?>? BodyTransform { get; set; } public HttpClient? HttpClient { get; set; } public HttpMessageHandler? HttpHandler { get; set; }
// Multimodal parts (§1B / §8A) public long? MaxPartBytes { get; set; } public string? OnUnsupportedPart { get; set; } }}The unified client: system prompt, skills injection, parallel and chained tool calls, retries, memory.
LlmClient.Create is the one entry point that turns a BaseUrl + Style + Model into a client
that can run a Toolkit to completion — everything else on
Options is either required wiring (the four fields above) or an opt-in override of a documented
default, with its own page for the deep dive.
When to use it
Section titled “When to use it”Reach for LlmClient.Create whenever you need to talk to an LLM through the tool-calling loop —
which is almost always, since RunAsync and
StreamAsync both hang off the client it returns. Point it at any
OpenAI-shape endpoint (OpenAI itself, OpenRouter, a local vLLM/Ollama server) with
Style = "openai", or at Anthropic’s Messages API with Style = "anthropic" — the client speaks
both wire formats from the same options shape, so switching providers is a two-field change, not a
rewrite.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. The smallest useful call — required fields only
Section titled “1. The smallest useful call — required fields only”using Toolnexus;
var client = LlmClient.Create(new LlmClient.Options{ BaseUrl = "https://openrouter.ai/api/v1", Style = "openai", // or "anthropic" Model = "anthropic/claude-3.5-sonnet", ApiKey = Environment.GetEnvironmentVariable("OPENROUTER_API_KEY"),});
await using var tk = await Toolkit.CreateAsync(new Toolkit.Options());var res = await client.RunAsync("List the files in the current folder, then summarise the README.", tk);Console.WriteLine(res.Text);2. The realistic case — a bounded, observable run
Section titled “2. The realistic case — a bounded, observable run”using Toolnexus;
var client = LlmClient.Create(new LlmClient.Options{ BaseUrl = "https://api.openai.com/v1", Style = "openai", Model = "gpt-4o-mini", ApiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY"), SystemPrompt = "You are a careful, concise coding assistant.", MaxTurns = 6, Retries = 3, TimeoutMs = 30_000, OnMetric = ev => Console.Error.WriteLine($"[metric] {ev.Kind} {ev.Model}"),});
await using var tk = await Toolkit.CreateAsync(new Toolkit.Options{ McpConfig = "mcp.json", SkillsDir = new[] { "skills" },});
var res = await client.RunAsync("Triage the open issues and propose a fix plan.", tk);Console.WriteLine(res.Text);3. The full surface — every optional slot named
Section titled “3. The full surface — every optional slot named”using Toolnexus;
var client = LlmClient.Create(new LlmClient.Options{ BaseUrl = "https://api.anthropic.com/v1", Style = "anthropic", Model = "claude-sonnet-4-5", ApiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY"), Headers = new Dictionary<string, string> { ["anthropic-version"] = "2023-06-01" }, SystemPrompt = "You are the on-call triage agent.", MaxTurns = 12,
// resilience — see LlmClient.ErrorInfo Retries = 3, RetryBaseMs = 500, RetryableStatuses = new[] { 520, 521 }, // widen the default 429/500/502/503/504/529 set TimeoutMs = 60_000, OnError = info => info.Status == 402 ? LlmClient.Tier.Fail : LlmClient.Tier.Retry,
// memory — see LlmClient.Conversation / IConversationStore Store = new MyDurableStore(),
// observability — see MetricEvent OnMetric = ev => Metrics.Record(ev),
// suspension (§10) — see LlmClient.WaitFor WaitFor = async request => await MyApprovalQueue.ResolveAsync(request),
// request shaping — see the "See also" pages below RequestParams = new Dictionary<string, object?> { ["max_tokens"] = 8192 }, BodyTransform = body => { body["metadata"] = new { team = "triage" }; return body; }, HttpClient = MySharedHttpClient,
// multimodal parts (§1B/§8A) MaxPartBytes = 5 * 1024 * 1024, OnUnsupportedPart = "text",});
await using var tk = await Toolkit.CreateAsync(new Toolkit.Options { McpConfig = "mcp.json" });var res = await client.RunAsync("go", tk);Console.WriteLine(res.Text);See also
Section titled “See also”Toolkit.CreateAsync— Collapse MCP servers, skills, built-ins, A2A agents and your own functions into one tool list — thetkevery example above passes toRunAsync.LlmClient.RunAsync— Send a prompt, let the loop call tools until the model stops, get a RunResult.LlmClient.StreamAsync— The streaming loop: text deltas, tool-call events, and suspension events as they happen.LlmClient.ErrorInfo— Classify an LLM error into retry/fail, bound a run with a deadline, and cancel cleanly.LlmClient.IConversationStore— Swap in-memory history for your own store so a conversation survives a process restart.LlmClient.Conversation— Keep a transcript across turns so the model remembers what it already did.MetricEvent— Emit one metric event per model call and per tool call: tokens, latency, outcome.LlmClient.Hooks— Intercept before/after model calls and tool calls: audit, redact, veto, or rewrite.ProviderException— The typed error a failed provider call raises, including the port-specificRetryAfterMs(milliseconds).