ManagedCode.CodexSharpSDK is an open-source .NET SDK for driving the Codex CLI from C#.
It is a CLI-first .NET 10 / C# 14 SDK aligned with real codex runtime behavior, with:
- thread-based API (
start/resume) - streamed JSONL events
- structured output schema support
- image attachments
--configflattening to TOML- NativeAOT-friendly implementation and tests on TUnit
All consumer usage examples are documented in this README; this repository intentionally does not keep standalone sample projects.
dotnet add package ManagedCode.CodexSharpSDKBefore using this SDK, you must have:
codexCLI installed and available inPATH- an already authenticated Codex session (
codex login)
Quick check:
codex --version
codex loginusing ManagedCode.CodexSharpSDK;
using var client = new CodexClient();
var thread = client.StartThread(new ThreadOptions
{
Model = CodexModels.Gpt54,
ModelReasoningEffort = ModelReasoningEffort.Medium,
});
var turn = await thread.RunAsync("Diagnose failing tests and propose a fix");
Console.WriteLine(turn.FinalResponse);
Console.WriteLine($"Items: {turn.Items.Count}");AutoStart is enabled by default, so StartThread() works immediately.
using var client = new CodexClient(new CodexClientOptions
{
CodexOptions = new CodexOptions
{
// Override only when `codex` is not discoverable via npm/PATH.
CodexExecutablePath = "/custom/path/to/codex",
},
});
var thread = client.StartThread(new ThreadOptions
{
Model = CodexModels.Gpt54,
ModelReasoningEffort = ModelReasoningEffort.High,
SandboxMode = SandboxMode.WorkspaceWrite,
});ThreadOptions supports full codex exec control.
var thread = client.StartThread(new ThreadOptions
{
Profile = "strict",
UseOss = true,
LocalProvider = OssProvider.LmStudio,
FullAuto = true,
Ephemeral = true,
Color = ExecOutputColor.Auto,
EnabledFeatures = ["multi_agent"],
DisabledFeatures = ["steer"],
AdditionalCliArguments = ["--some-future-flag", "custom-value"],
});using var client = new CodexClient();
var metadata = client.GetCliMetadata();
Console.WriteLine($"Installed codex-cli: {metadata.InstalledVersion}");
Console.WriteLine($"Default model: {metadata.DefaultModel ?? "(not set)"}");
foreach (var model in metadata.Models.Where(model => model.IsListed))
{
Console.WriteLine(model.Slug);
}GetCliMetadata() reads:
- installed CLI version from
codex --version - default model from
~/.codex/config.toml - model catalog from
~/.codex/models_cache.json
var update = client.GetCliUpdateStatus();
if (update.IsUpdateAvailable)
{
Console.WriteLine(update.UpdateMessage);
Console.WriteLine(update.UpdateCommand);
}GetCliUpdateStatus() compares installed CLI version with latest published @openai/codex npm version and returns an update command matched to your install context (bun or npm).
When thread-level web search options are omitted, SDK does not emit a web_search override and leaves your existing CLI/config value as-is.
CodexClientis safe for concurrent use from multiple threads.StartAsync()is idempotent and guarded.StopAsync()cleanly disconnects client state.Dispose()transitions client toDisposed.- A single
CodexThreadinstance serializes turns (RunAsyncandRunStreamedAsync) to prevent race conditions in shared conversation state.
var streamed = await thread.RunStreamedAsync("Implement the fix");
await foreach (var evt in streamed.Events)
{
switch (evt)
{
case ItemCompletedEvent completed:
Console.WriteLine($"Item: {completed.Item.Type}");
break;
case TurnCompletedEvent done:
Console.WriteLine($"Output tokens: {done.Usage.OutputTokens}");
break;
}
}using System.Text.Json.Serialization;
public sealed record RepositorySummary(string Summary, string Status);
[JsonSerializable(typeof(RepositorySummary))]
internal sealed partial class AppJsonContext : JsonSerializerContext;
var schema = StructuredOutputSchema.Map<RepositorySummary>(
additionalProperties: false,
(response => response.Summary, StructuredOutputSchema.PlainText()),
(response => response.Status, StructuredOutputSchema.PlainText()));
var result = await thread.RunAsync<RepositorySummary>(
"Summarize repository status",
schema,
AppJsonContext.Default.RepositorySummary);
Console.WriteLine(result.TypedResponse.Status);
Console.WriteLine(result.TypedResponse.Summary);For advanced options (for example cancellation), use the TurnOptions overload:
using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var result = await thread.RunAsync<RepositorySummary>(
"Summarize repository status",
AppJsonContext.Default.RepositorySummary,
new TurnOptions
{
OutputSchema = schema,
CancellationToken = cancellation.Token,
});RunAsync<TResponse> always requires OutputSchema (direct parameter or TurnOptions.OutputSchema).
For AOT/trimming-safe typed deserialization, pass JsonTypeInfo<TResponse> from a source-generated context.
Overloads without JsonTypeInfo<TResponse> are explicitly marked with RequiresDynamicCode and RequiresUnreferencedCode.
using Microsoft.Extensions.Logging;
public sealed class ConsoleCodexLogger : ILogger
{
public IDisposable BeginScope<TState>(TState state)
where TState : notnull
{
return NullScope.Instance;
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception? exception,
Func<TState, Exception?, string> formatter)
{
Console.WriteLine($"[{logLevel}] {formatter(state, exception)}");
if (exception is not null)
{
Console.WriteLine(exception);
}
}
private sealed class NullScope : IDisposable
{
public static NullScope Instance { get; } = new();
public void Dispose() { }
}
}
using var client = new CodexClient(new CodexOptions
{
Logger = new ConsoleCodexLogger(),
});using var imageStream = File.OpenRead("./photo.png");
var result = await thread.RunAsync(
[
new TextInput("Describe these images"),
new LocalImageInput("./ui.png"),
new LocalImageInput(new FileInfo("./diagram.jpg")),
new LocalImageInput(imageStream, "photo.png"),
]);var resumed = client.ResumeThread("thread_123");
await resumed.RunAsync("Continue from previous plan");An optional adapter package lets you use CodexSharpSDK through the standard IChatClient interface from Microsoft.Extensions.AI.
dotnet add package ManagedCode.CodexSharpSDK.Extensions.AIusing Microsoft.Extensions.AI;
using ManagedCode.CodexSharpSDK.Extensions.AI;
IChatClient client = new CodexChatClient(new CodexChatClientOptions
{
DefaultModel = CodexModels.Gpt54,
});
var response = await client.GetResponseAsync("Diagnose failing tests and propose a fix");
Console.WriteLine(response.Text);using ManagedCode.CodexSharpSDK.Extensions.AI.Extensions;
builder.Services.AddCodexChatClient(options =>
{
options.DefaultModel = CodexModels.Gpt54;
});
// Then inject IChatClient anywhere:
app.MapGet("/ask", async (IChatClient client) =>
{
var response = await client.GetResponseAsync("Summarize the repo");
return response.Text;
});using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using ManagedCode.CodexSharpSDK.Models;
using ManagedCode.CodexSharpSDK.Extensions.AI.Extensions;
var services = new ServiceCollection();
services.AddCodexChatClient(options =>
{
options.DefaultModel = CodexModels.Gpt54;
});
using var provider = services.BuildServiceProvider();
var chatClient = provider.GetRequiredService<IChatClient>();Keyed registration is also supported:
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using ManagedCode.CodexSharpSDK.Models;
using ManagedCode.CodexSharpSDK.Extensions.AI.Extensions;
var services = new ServiceCollection();
services.AddKeyedCodexChatClient("codex-main", options =>
{
options.DefaultModel = CodexModels.Gpt54;
});
using var provider = services.BuildServiceProvider();
var keyedChatClient = provider.GetRequiredKeyedService<IChatClient>("codex-main");await foreach (var update in client.GetStreamingResponseAsync("Implement the fix"))
{
Console.Write(update.Text);
}var options = new ChatOptions
{
ModelId = CodexModels.Gpt54,
AdditionalProperties = new AdditionalPropertiesDictionary
{
["codex:sandbox_mode"] = "workspace-write",
["codex:reasoning_effort"] = "high",
},
};
var response = await client.GetResponseAsync("Refactor the auth module", options);Codex-specific output items (commands, file changes, MCP tool calls, web searches) are preserved as typed AIContent subclasses:
foreach (var content in response.Messages.SelectMany(m => m.Contents))
{
switch (content)
{
case CommandExecutionContent cmd:
Console.WriteLine($"Command: {cmd.Command} (exit {cmd.ExitCode})");
break;
case FileChangeContent file:
Console.WriteLine($"File changes: {file.Changes.Count}");
break;
}
}See docs/Features/meai-integration.md and ADR 003 for full details.
dotnet build ManagedCode.CodexSharpSDK.slnx -c Release -warnaserror
dotnet test --solution ManagedCode.CodexSharpSDK.slnx -c Release