https://github.com/managedcode/geminisharpsdk
CLI-first .NET 10 / C# SDK for Google Gemini CLI with typed thread API, streamed JSONL events, structured outputs
https://github.com/managedcode/geminisharpsdk
csharp dotnet gemini gemini-cli gemini-sdk-csharp google sdk
Last synced: about 7 hours ago
JSON representation
CLI-first .NET 10 / C# SDK for Google Gemini CLI with typed thread API, streamed JSONL events, structured outputs
- Host: GitHub
- URL: https://github.com/managedcode/geminisharpsdk
- Owner: managedcode
- License: mit
- Created: 2026-03-18T07:20:44.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2026-04-09T19:26:46.000Z (3 months ago)
- Last Synced: 2026-06-22T13:05:22.325Z (1 day ago)
- Topics: csharp, dotnet, gemini, gemini-cli, gemini-sdk-csharp, google, sdk
- Language: C#
- Homepage:
- Size: 196 KB
- Stars: 2
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Agents: AGENTS.md
Awesome Lists containing this project
README
# ManagedCode.GeminiSharpSDK
[](https://github.com/managedcode/GeminiSharpSDK/actions/workflows/ci.yml)
[](https://github.com/managedcode/GeminiSharpSDK/actions/workflows/release.yml)
[](https://github.com/managedcode/GeminiSharpSDK/actions/workflows/codeql.yml)
[](https://www.nuget.org/packages/ManagedCode.GeminiSharpSDK)
[](https://github.com/managedcode/GeminiSharpSDK/blob/main/LICENSE)
`ManagedCode.GeminiSharpSDK` is an open-source .NET SDK for driving the Gemini CLI from C#.
It is a CLI-first `.NET 10 / C# 14` SDK aligned with real `gemini` runtime behavior, with:
- thread-based API (`start` / `resume`)
- streamed JSONL events
- structured output schema support
- image attachments
- `--config` flattening 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.
## Installation
```bash
dotnet add package ManagedCode.GeminiSharpSDK
```
## Prerequisites
Before using this SDK, you must have:
- `gemini` CLI installed and available in `PATH`
- an already authenticated Gemini session (`gemini login`)
Quick check:
```bash
gemini --version
gemini login
```
## Quickstart
```csharp
using ManagedCode.GeminiSharpSDK;
using var client = new GeminiClient();
var thread = client.StartThread(new ThreadOptions
{
Model = GeminiModels.Gemini25Pro,
});
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.
## Advanced Configuration (Optional)
```csharp
using var client = new GeminiClient(new GeminiClientOptions
{
GeminiOptions = new GeminiOptions
{
// Override only when `gemini` is not discoverable via npm/PATH.
GeminiExecutablePath = "/custom/path/to/gemini",
},
});
var thread = client.StartThread(new ThreadOptions
{
Model = GeminiModels.Gemini25Pro,
ApprovalPolicy = ApprovalMode.Default,
AdditionalDirectories = ["/workspace/shared"],
});
```
## Extended CLI Options
`ThreadOptions` maps to the current headless Gemini CLI surface (`--prompt`, `--output-format stream-json`, `--resume`, `--approval-mode`, `--include-directories`, and sandbox toggle). Unsupported legacy flags fail fast.
Fresh SDK-started runs persist per Gemini project/working directory and are visible via `gemini --list-sessions` for that same project.
```csharp
var thread = client.StartThread(new ThreadOptions
{
Model = GeminiModels.AutoGemini3,
ApprovalPolicy = ApprovalMode.Default,
AdditionalDirectories = ["/workspace/shared"],
SandboxMode = SandboxMode.WorkspaceWrite,
AdditionalCliArguments = ["--some-future-flag", "custom-value"],
});
```
## Gemini CLI Metadata
```csharp
using var client = new GeminiClient();
var metadata = client.GetCliMetadata();
Console.WriteLine($"Installed gemini-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 `gemini --version`
- default model from `~/.gemini/config.toml`
- model catalog from `~/.gemini/models_cache.json`
```csharp
var update = client.GetCliUpdateStatus();
if (update.IsUpdateAvailable)
{
Console.WriteLine(update.UpdateMessage);
Console.WriteLine(update.UpdateCommand);
}
```
`GetCliUpdateStatus()` compares installed CLI version with latest published `/gemini-cli` 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.
## Client Lifecycle and Thread Safety
- `GeminiClient` is safe for concurrent use from multiple threads.
- `StartAsync()` is idempotent and guarded.
- `StopAsync()` cleanly disconnects client state.
- `Dispose()` transitions client to `Disposed`.
- A single `GeminiThread` instance serializes turns (`RunAsync` and `RunStreamedAsync`) to prevent race conditions in shared conversation state.
## Streaming
```csharp
var streamed = await thread.RunStreamedAsync("Implement the fix");
await foreach (var evt in streamed.Events)
{
switch (evt)
{
case InitEvent init:
Console.WriteLine($"Session: {init.SessionId}");
break;
case MessageEvent { Role: "assistant" } message:
Console.Write(message.Content);
break;
case ResultEvent { Usage: not null } result:
Console.WriteLine($"Output tokens: {result.Usage.OutputTokens}");
break;
}
}
```
## Structured Output
```csharp
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(
additionalProperties: false,
(response => response.Summary, StructuredOutputSchema.PlainText()),
(response => response.Status, StructuredOutputSchema.PlainText()));
var result = await thread.RunAsync(
"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:
```csharp
using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var result = await thread.RunAsync(
"Summarize repository status",
AppJsonContext.Default.RepositorySummary,
new TurnOptions
{
OutputSchema = schema,
CancellationToken = cancellation.Token,
});
```
`RunAsync` always requires `OutputSchema` (direct parameter or `TurnOptions.OutputSchema`).
For AOT/trimming-safe typed deserialization, pass `JsonTypeInfo` from a source-generated context.
Overloads without `JsonTypeInfo` are explicitly marked with `RequiresDynamicCode` and `RequiresUnreferencedCode`.
## Diagnostics Logging (Optional)
```csharp
using Microsoft.Extensions.Logging;
public sealed class ConsoleGeminiLogger : ILogger
{
public IDisposable BeginScope(TState state)
where TState : notnull
{
return NullScope.Instance;
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public void Log(
LogLevel logLevel,
EventId eventId,
TState state,
Exception? exception,
Func 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 GeminiClient(new GeminiOptions
{
Logger = new ConsoleGeminiLogger(),
});
```
## Images + Text Input
```csharp
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"),
]);
```
## Resume an Existing GeminiThread
```csharp
var resumed = client.ResumeThread("thread_123");
await resumed.RunAsync("Continue from previous plan");
```
## Microsoft.Extensions.AI Integration
An optional adapter package lets you use GeminiSharpSDK through the standard `IChatClient` interface from `Microsoft.Extensions.AI`.
```bash
dotnet add package ManagedCode.GeminiSharpSDK.Extensions.AI
```
### Basic usage
```csharp
using Microsoft.Extensions.AI;
using ManagedCode.GeminiSharpSDK.Extensions.AI;
IChatClient client = new GeminiChatClient(new GeminiChatClientOptions
{
DefaultModel = GeminiModels.Gemini25Pro,
});
var response = await client.GetResponseAsync("Diagnose failing tests and propose a fix");
Console.WriteLine(response.Text);
```
### DI registration
```csharp
using ManagedCode.GeminiSharpSDK.Extensions.AI.Extensions;
builder.Services.AddGeminiChatClient(options =>
{
options.DefaultModel = GeminiModels.Gemini25Pro;
});
// Then inject IChatClient anywhere:
app.MapGet("/ask", async (IChatClient client) =>
{
var response = await client.GetResponseAsync("Summarize the repo");
return response.Text;
});
```
### Resolve `IChatClient` from `IServiceProvider`
```csharp
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using ManagedCode.GeminiSharpSDK.Models;
using ManagedCode.GeminiSharpSDK.Extensions.AI.Extensions;
var services = new ServiceCollection();
services.AddGeminiChatClient(options =>
{
options.DefaultModel = GeminiModels.Gemini25Pro;
});
using var provider = services.BuildServiceProvider();
var chatClient = provider.GetRequiredService();
```
Keyed registration is also supported:
```csharp
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using ManagedCode.GeminiSharpSDK.Models;
using ManagedCode.GeminiSharpSDK.Extensions.AI.Extensions;
var services = new ServiceCollection();
services.AddKeyedGeminiChatClient("gemini-main", options =>
{
options.DefaultModel = GeminiModels.Gemini25Pro;
});
using var provider = services.BuildServiceProvider();
var keyedChatClient = provider.GetRequiredKeyedService("gemini-main");
```
### Streaming
```csharp
await foreach (var update in client.GetStreamingResponseAsync("Implement the fix"))
{
Console.Write(update.Text);
}
```
### Gemini-specific options via ChatOptions
```csharp
var options = new ChatOptions
{
ModelId = GeminiModels.Gemini25Pro,
AdditionalProperties = new AdditionalPropertiesDictionary
{
["gemini:sandbox_mode"] = "workspace-write",
["gemini:reasoning_effort"] = "high",
},
};
var response = await client.GetResponseAsync("Refactor the auth module", options);
```
### Rich content types
Gemini-specific output items (commands, file changes, MCP tool calls, web searches) are preserved as typed `AIContent` subclasses:
```csharp
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](https://github.com/managedcode/GeminiSharpSDK/blob/main/docs/Features/meai-integration.md) and [ADR 003](https://github.com/managedcode/GeminiSharpSDK/blob/main/docs/ADR/003-microsoft-extensions-ai-integration.md) for full details.
## Microsoft Agent Framework Integration
An optional adapter package lets you use GeminiSharpSDK with Microsoft Agent Framework `AIAgent`.
```bash
dotnet add package ManagedCode.GeminiSharpSDK.Extensions.AgentFramework
```
This package now ships on the same stable version track as the core SDK because `Microsoft.Agents.AI` is stable.
### Basic usage
```csharp
using ManagedCode.GeminiSharpSDK.Extensions.AI;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
IChatClient chatClient = new GeminiChatClient();
AIAgent agent = chatClient.AsAIAgent(
name: "GeminiAssistant",
instructions: "You are a helpful coding assistant.");
AgentResponse response = await agent.RunAsync("Summarize the repository");
Console.WriteLine(response);
```
### DI registration
```csharp
using ManagedCode.GeminiSharpSDK.Extensions.AgentFramework.Extensions;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
builder.Services.AddGeminiAIAgent(
configureAgent: options =>
{
options.Name = "GeminiAssistant";
options.ChatOptions = new ChatOptions
{
Instructions = "You are a helpful coding assistant."
};
});
app.MapGet("/agent", async (AIAgent agent) =>
{
var response = await agent.RunAsync("Summarize the repository");
return response.ToString();
});
```
### Keyed DI registration
```csharp
using ManagedCode.GeminiSharpSDK.Extensions.AgentFramework.Extensions;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddKeyedGeminiAIAgent(
"gemini-main",
configureAgent: options =>
{
options.Name = "GeminiAssistant";
options.ChatOptions = new ChatOptions
{
Instructions = "You are a helpful coding assistant."
};
});
using var provider = services.BuildServiceProvider();
var keyedAgent = provider.GetRequiredKeyedService("gemini-main");
```
This package builds on the existing `IChatClient` adapter, so the canonical MAF path remains `IChatClient.AsAIAgent(...)`; the new package adds a supported Gemini-specific package boundary and DI convenience methods.
See [docs/Features/agent-framework-integration.md](https://github.com/managedcode/GeminiSharpSDK/blob/main/docs/Features/agent-framework-integration.md) and [ADR 004](https://github.com/managedcode/GeminiSharpSDK/blob/main/docs/ADR/004-microsoft-agent-framework-integration.md) for full details.
## Build and Test
```bash
dotnet build ManagedCode.GeminiSharpSDK.slnx -c Release -warnaserror
dotnet test --solution ManagedCode.GeminiSharpSDK.slnx -c Release
```