Adapters.ToOpenAI
C# · package Toolnexus · SPEC §4 · Adapters.cs
public static List<Dictionary<string, object?>> ToOpenAI(IEnumerable<ITool> tools)Turns tools into the tools array an OpenAI-shaped chat completion expects. This is the bridge
between “toolnexus knows about these tools” and “the model can call them”.
It takes an IEnumerable<ITool>, so a LINQ query or a filtered tk.Tools() passes straight in
without materialising a list first.
When to use it
Section titled “When to use it”When you are driving the LLM call yourself and need schema to put in the request body. Every OpenAI-compatible endpoint takes this shape — OpenAI, OpenRouter, Groq, Together, a local Ollama, or your own gateway.
Why this and not the client
Section titled “Why this and not the client”Examples
Section titled “Examples”1. One tool to OpenAI schema
Section titled “1. One tool to OpenAI schema”using Toolnexus;
var weather = NativeTool.Of( "get_weather", "Current weather for a city", new Dictionary<string, object?> { ["type"] = "object", ["properties"] = new Dictionary<string, object?> { ["city"] = new Dictionary<string, object?> { ["type"] = "string" } }, ["required"] = new[] { "city" }, }, (IDictionary<string, object?> a) => $"sunny in {a["city"]}");
var schema = Adapters.ToOpenAI(new ITool[] { weather });
if (schema.Count != 1) throw new Exception("expected 1 entry");if (schema[0]["type"] as string != "function") throw new Exception("type");
var fn = (IDictionary<string, object?>)schema[0]["function"]!;if (fn["name"] as string != "get_weather") throw new Exception("name");if (fn["description"] as string != "Current weather for a city") throw new Exception("description");
Console.WriteLine($"ok: {fn["name"]}");Note the nesting: OpenAI wraps each tool in {"type":"function","function":{...}}. The
InputSchema on an ITool becomes function.parameters — the key is renamed.
2. Feeding it straight into a request body
Section titled “2. Feeding it straight into a request body”The output is plain dictionaries, so System.Text.Json serializes it with no converter.
using System.Text.Json;using Toolnexus;
ITool Mk(string name, string desc) => NativeTool.Of( name, desc, new Dictionary<string, object?> { ["type"] = "object", ["properties"] = new Dictionary<string, object?>() }, (IDictionary<string, object?> a) => name);
var tools = new[] { Mk("search", "Search the docs"), Mk("ping", "Health check") };
var body = new Dictionary<string, object?>{ ["model"] = "gpt-4o-mini", ["messages"] = new[] { new Dictionary<string, object?> { ["role"] = "user", ["content"] = "search for adapters" } }, ["tools"] = Adapters.ToOpenAI(tools),};
var entries = (List<Dictionary<string, object?>>)body["tools"]!;if (entries.Count != 2) throw new Exception("expected 2 tools");
// Order is preserved.var names = entries .Select(e => (string)((IDictionary<string, object?>)e["function"]!)["name"]!) .ToList();if (names[0] != "search" || names[1] != "ping") throw new Exception($"order: {string.Join(",", names)}");
// Plain JSON — no custom converter needed.if (JsonSerializer.Serialize(body).Length == 0) throw new Exception("serialize");
Console.WriteLine($"ok: {string.Join(", ", names)}");3. Round-tripping a call back to the tool
Section titled “3. Round-tripping a call back to the tool”Schema out, tool call in. The Name the model returns is the same Name you look up.
using System.Text.Json;using Toolnexus;
var weather = NativeTool.Of( "get_weather", "Current weather for a city", new Dictionary<string, object?> { ["type"] = "object", ["properties"] = new Dictionary<string, object?> { ["city"] = new Dictionary<string, object?> { ["type"] = "string" } }, ["required"] = new[] { "city" }, }, (IDictionary<string, object?> a) => $"sunny in {a["city"]}");
var tools = new ITool[] { weather };
// What a model would send back. OpenAI encodes arguments as a JSON STRING.const string rawArgs = """{"city":"Chennai"}""";const string calledName = "get_weather";
var parsed = JsonSerializer.Deserialize<Dictionary<string, object?>>(rawArgs)!;// NB: `args` is already the implicit parameter of a top-level program — pick another name.var callArgs = parsed.ToDictionary(kv => kv.Key, kv => (object?)kv.Value?.ToString());
var called = tools.FirstOrDefault(t => t.Name == calledName) ?? throw new Exception("the advertised name should resolve back to the tool");
var res = await called.ExecuteAsync(callArgs);if (res.IsError || res.Output != "sunny in Chennai") throw new Exception(res.Output);
// An empty tool list is valid — it just means "no tools this turn".if (Adapters.ToOpenAI(Array.Empty<ITool>()).Count != 0) throw new Exception("expected empty");
Console.WriteLine($"ok: {calledName} -> {res.Output}");| Path | From | Notes |
|---|---|---|
[]["type"] |
— | Always the literal "function". |
[]["function"]["name"] |
ITool.Name |
What the model calls back with. |
[]["function"]["description"] |
ITool.Description |
|
[]["function"]["parameters"] |
ITool.InputSchema |
Renamed — InputSchema → parameters. |
See also
Section titled “See also”Adapters.ToAnthropic·Adapters.ToGeminiToolkit.CreateAsync—tk.ToOpenAI()is this, applied to the toolkitLlmClient.Create— calls the adapter for you