diff --git a/.github/workflows/server-sdk-ai.yml b/.github/workflows/server-sdk-ai.yml
new file mode 100644
index 0000000..c5013f6
--- /dev/null
+++ b/.github/workflows/server-sdk-ai.yml
@@ -0,0 +1,43 @@
+name: server-sdk-ai
+on:
+ schedule:
+ # * is a special character in YAML so you have to quote this string
+ - cron: '0 9 * * *'
+ push:
+ branches: [ main, 'feat/**' ]
+ paths:
+ - 'server-sdk-ai/**'
+ - '.github/workflows/server-sdk-ai.yml'
+ pull_request:
+ branches: [ main, 'feat/**' ]
+ paths:
+ - 'server-sdk-ai/**'
+ - '.github/workflows/server-sdk-ai.yml'
+
+jobs:
+ build:
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest]
+ fail-fast: false
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+
+ - name: Setup dotnet build tools
+ uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
+ with:
+ dotnet-version: 8.0
+
+ # Build-only: these examples call external model providers and need
+ # provider credentials to run, so CI just compiles them to catch build errors.
+ - name: Build server-sdk-ai examples
+ shell: bash
+ run: |
+ set -e
+ for proj in $(find server-sdk-ai -name '*.csproj' | sort); do
+ echo "::group::Building $proj"
+ dotnet build "$proj"
+ echo "::endgroup::"
+ done
diff --git a/CODEOWNERS b/CODEOWNERS
index 3c1c6e1..e76d4e9 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -1,2 +1,5 @@
# Repository Maintainers
* @launchdarkly/team-sdk-net
+
+# AI examples
+/server-sdk-ai/ @launchdarkly/team-ai-agents @launchdarkly/team-ai-evals
diff --git a/README.md b/README.md
index 5e903b0..2cdc8bc 100644
--- a/README.md
+++ b/README.md
@@ -7,9 +7,10 @@ or the [.NET SDK reference guide](https://docs.launchdarkly.com/sdk/server-side/
## SDKs
-| SDK | Package | Examples |
-|-----------------|--------------------------|--------------------------------|
-| .NET Server SDK | `LaunchDarkly.ServerSdk` | [`server-sdk/`](./server-sdk/) |
+| SDK | Package | Examples |
+|--------------------|-----------------------------|------------------------------------------|
+| .NET Server SDK | `LaunchDarkly.ServerSdk` | [`server-sdk/`](./server-sdk/) |
+| .NET Server AI SDK | `LaunchDarkly.ServerSdk.Ai` | [`server-sdk-ai/`](./server-sdk-ai/) |
## Requirements
diff --git a/server-sdk-ai/README.md b/server-sdk-ai/README.md
new file mode 100644
index 0000000..38867ca
--- /dev/null
+++ b/server-sdk-ai/README.md
@@ -0,0 +1,26 @@
+# LaunchDarkly .NET Server AI SDK examples
+
+Examples for the [`LaunchDarkly.ServerSdk.Ai`](https://www.nuget.org/packages/LaunchDarkly.ServerSdk.Ai) package.
+
+For more comprehensive instructions, you can visit the [AI Configs Quickstart](https://docs.launchdarkly.com/home/ai-configs/quickstart) or the [.NET AI SDK reference guide](https://docs.launchdarkly.com/sdk/ai/dotnet).
+
+Each example is a self-contained .NET console application you can run independently.
+
+## Getting Started
+
+These examples show how to integrate LaunchDarkly AI with different providers.
+
+| Provider | Example | Description |
+|----------|----------------------------------------------------------|------------------------------------------------------------------|
+| OpenAI | [Chat Completions](./frameworks/openai/chat-completions/) | `CompletionConfig` with OpenAI, automatic metrics tracking |
+| Bedrock | [Converse](./frameworks/bedrock/converse/) | `CompletionConfig` with the AWS Bedrock Converse API, metrics tracking |
+
+## Features
+
+These examples focus on the LaunchDarkly AI SDK itself. They are **provider-agnostic** — they retrieve and resolve AI Configs, then exercise the tracker API with synthetic operations rather than calling any model provider.
+
+| Example | Description |
+|------------------------------------------------------|-----------------------------------------------------------------------------------------|
+| [Completion Config](./features/completion-config/) | Default values, Mustache variables, model parameter extraction, tool enumeration, and `TrackMetricsOf` |
+| [Agent Config](./features/agent-config/) | Agent instructions and tools; `TrackMetricsOf` and `TrackToolCall` |
+| [Judge Config](./features/judge-config/) | Judge config retrieval and `TrackJudgeResult` |
diff --git a/server-sdk-ai/features/agent-config/.env.example b/server-sdk-ai/features/agent-config/.env.example
new file mode 100644
index 0000000..a4c08ca
--- /dev/null
+++ b/server-sdk-ai/features/agent-config/.env.example
@@ -0,0 +1,5 @@
+# Your LaunchDarkly server-side SDK key
+LAUNCHDARKLY_SDK_KEY=
+
+# Override to use a different AI Config key
+LAUNCHDARKLY_AGENT_KEY=sample-agent
diff --git a/server-sdk-ai/features/agent-config/AgentConfig.csproj b/server-sdk-ai/features/agent-config/AgentConfig.csproj
new file mode 100644
index 0000000..3ec26a6
--- /dev/null
+++ b/server-sdk-ai/features/agent-config/AgentConfig.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
diff --git a/server-sdk-ai/features/agent-config/NOTICE b/server-sdk-ai/features/agent-config/NOTICE
new file mode 100644
index 0000000..9357970
--- /dev/null
+++ b/server-sdk-ai/features/agent-config/NOTICE
@@ -0,0 +1,4 @@
+LaunchDarkly .NET Server AI SDK Agent Config example
+Copyright 2026 Catamorphic, Co.
+
+This product includes software developed at LaunchDarkly (https://launchdarkly.com/).
diff --git a/server-sdk-ai/features/agent-config/Program.cs b/server-sdk-ai/features/agent-config/Program.cs
new file mode 100644
index 0000000..3e69b2f
--- /dev/null
+++ b/server-sdk-ai/features/agent-config/Program.cs
@@ -0,0 +1,152 @@
+using DotNetEnv;
+using LaunchDarkly.Sdk;
+using LaunchDarkly.Sdk.Server;
+using LaunchDarkly.Sdk.Server.Ai;
+using LaunchDarkly.Sdk.Server.Ai.Adapters;
+using LaunchDarkly.Sdk.Server.Ai.Config;
+using LaunchDarkly.Sdk.Server.Ai.Tracking;
+
+Env.TraversePath().Load();
+
+var sdkKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_SDK_KEY");
+if (string.IsNullOrEmpty(sdkKey))
+{
+ Console.Error.WriteLine(
+ "LaunchDarkly SDK key is required: set the LAUNCHDARKLY_SDK_KEY environment variable and try again.");
+ return;
+}
+
+// Set agentKey to the AI config key you want to evaluate.
+var agentKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_AGENT_KEY")
+ ?? "sample-agent";
+
+var ldClient = new LdClient(Configuration.Builder(sdkKey).Build());
+if (!ldClient.Initialized)
+{
+ Console.Error.WriteLine(
+ "*** SDK failed to initialize. Please check your internet connection and SDK credential for any typo.");
+ ldClient.Dispose();
+ return;
+}
+Console.WriteLine("*** SDK successfully initialized!");
+
+var aiClient = new LdAiClient(new LdClientAdapter(ldClient));
+
+// Set up the evaluation context. This context should appear on your
+// LaunchDarkly contexts dashboard soon after you run the demo.
+var context = Context.Builder(ContextKind.Of("user"), "example-user-key")
+ .Name("Sandy")
+ .Build();
+
+// Default agent config used when the AI Config is unreachable or missing.
+var defaultValue = LdAiAgentConfigDefault.New()
+ .Enable()
+ .SetModelName("gpt-4")
+ .SetModelProviderName("openai")
+ .SetInstructions("You are a helpful research assistant for {{companyName}}.")
+ .Build();
+
+try
+{
+ var agentConfig = aiClient.AgentConfig(
+ agentKey,
+ context,
+ defaultValue,
+ variables: new Dictionary
+ {
+ ["companyName"] = "LaunchDarkly"
+ });
+
+ if (!agentConfig.Enabled)
+ {
+ Console.WriteLine($"Agent config '{agentKey}' is disabled.");
+ return;
+ }
+
+ Console.WriteLine();
+ Console.WriteLine($"Resolved agent model: {agentConfig.Model.Name} (provider: {agentConfig.Provider.Name})");
+ Console.WriteLine();
+ Console.WriteLine("Agent instructions (Mustache variables interpolated):");
+ Console.WriteLine(agentConfig.Instructions);
+
+ if (agentConfig.Tools.Count > 0)
+ {
+ Console.WriteLine();
+ Console.WriteLine("Configured tools (map to your provider's function-calling API):");
+ foreach (var (name, tool) in agentConfig.Tools)
+ {
+ Console.WriteLine($" {name}: {tool.Description} (type: {tool.Type})");
+ }
+ }
+
+ var tracker = agentConfig.CreateTracker();
+
+ // To keep this example provider-agnostic, the operation below is a
+ // synthetic stand-in for an actual model call — see the getting-started/*
+ // examples for end-to-end integrations with OpenAI or AWS Bedrock. The
+ // tracker API is exercised exactly the same way regardless of provider.
+ Console.WriteLine();
+ Console.WriteLine("Running a synthetic agent invocation wrapped in TrackMetricsOf...");
+
+ var result = await tracker.TrackMetricsOf(
+ SyntheticMetrics,
+ async () =>
+ {
+ await Task.Delay(150);
+ return new SyntheticResult(
+ Content: "[simulated agent response]",
+ InputTokens: 80,
+ OutputTokens: 120,
+ ToolsInvoked: new[] { "search", "calculator" });
+ });
+
+ // Record each tool invocation on the tracker so it appears in the
+ // metric summary. In a real integration these names would come from
+ // the provider's tool-use response.
+ foreach (var toolName in result.ToolsInvoked)
+ {
+ tracker.TrackToolCall(toolName);
+ }
+
+ Console.WriteLine();
+ Console.WriteLine("Simulated agent response:");
+ Console.WriteLine(result.Content);
+
+ PrintSummary(tracker.Summary);
+}
+catch (Exception ex)
+{
+ Console.Error.WriteLine($"Error: {ex.Message}");
+}
+finally
+{
+ ldClient.FlushAndWait(TimeSpan.FromSeconds(5));
+ ldClient.Dispose();
+}
+
+static AiMetrics SyntheticMetrics(SyntheticResult result) => new(
+ success: true,
+ tokens: new Usage(
+ Total: result.InputTokens + result.OutputTokens,
+ Input: result.InputTokens,
+ Output: result.OutputTokens));
+
+static void PrintSummary(MetricSummary summary)
+{
+ Console.WriteLine();
+ Console.WriteLine("Done! The tracker captured the following metrics:");
+ Console.WriteLine($" Duration: {summary.DurationMs}ms");
+ Console.WriteLine($" Success: {summary.Success}");
+ if (summary.Tokens is { } tokens)
+ {
+ Console.WriteLine($" Input tokens: {tokens.Input}");
+ Console.WriteLine($" Output tokens: {tokens.Output}");
+ Console.WriteLine($" Total tokens: {tokens.Total}");
+ }
+}
+
+internal sealed record SyntheticResult(
+ string Content,
+ int InputTokens,
+ int OutputTokens,
+ IReadOnlyList ToolsInvoked);
diff --git a/server-sdk-ai/features/agent-config/README.md b/server-sdk-ai/features/agent-config/README.md
new file mode 100644
index 0000000..0ef5aa8
--- /dev/null
+++ b/server-sdk-ai/features/agent-config/README.md
@@ -0,0 +1,34 @@
+# Agent Config
+
+This example focuses on the LaunchDarkly AI SDK itself — it is **provider-agnostic** and does not call OpenAI, Bedrock, Anthropic, or any other model. The actual agent invocation is replaced with a synthetic operation so you can see the SDK's tracking surface clearly. For end-to-end provider integrations see the [getting-started examples](../../frameworks).
+
+It demonstrates:
+
+- Retrieving agent instructions and tools from LaunchDarkly via `LdAiClient.AgentConfig`
+- Using the builder pattern (`LdAiAgentConfigDefault.New()`) to declare a fallback default
+- Interpolating Mustache placeholders in agent instructions
+- Enumerating the tools attached to the agent (so they can be mapped to your provider's function-calling API)
+- Tracking metrics via `tracker.TrackMetricsOf` and recording individual tool invocations via `tracker.TrackToolCall`
+
+## Prerequisites
+
+- .NET 8.0 SDK
+- A LaunchDarkly account and a server-side SDK key
+
+## Setup
+
+1. [Create an AI Config](https://launchdarkly.com/docs/home/ai-configs/create) of type **Agent** in LaunchDarkly with the key `sample-agent`. Set the instructions (you may include Mustache placeholders such as `{{companyName}}`), choose a provider and model.
+2. Copy `.env.example` to `.env` and fill in your SDK key:
+ ```sh
+ cp .env.example .env
+ ```
+3. Restore dependencies:
+ ```sh
+ dotnet restore
+ ```
+
+## Run
+
+```sh
+dotnet run
+```
diff --git a/server-sdk-ai/features/completion-config/.env.example b/server-sdk-ai/features/completion-config/.env.example
new file mode 100644
index 0000000..be0ac8c
--- /dev/null
+++ b/server-sdk-ai/features/completion-config/.env.example
@@ -0,0 +1,5 @@
+# Your LaunchDarkly server-side SDK key
+LAUNCHDARKLY_SDK_KEY=
+
+# Override to use a different AI Config key
+LAUNCHDARKLY_COMPLETION_KEY=sample-completion
diff --git a/server-sdk-ai/features/completion-config/CompletionConfig.csproj b/server-sdk-ai/features/completion-config/CompletionConfig.csproj
new file mode 100644
index 0000000..3ec26a6
--- /dev/null
+++ b/server-sdk-ai/features/completion-config/CompletionConfig.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
diff --git a/server-sdk-ai/features/completion-config/NOTICE b/server-sdk-ai/features/completion-config/NOTICE
new file mode 100644
index 0000000..1b97a9b
--- /dev/null
+++ b/server-sdk-ai/features/completion-config/NOTICE
@@ -0,0 +1,4 @@
+LaunchDarkly .NET Server AI SDK Completion Config example
+Copyright 2026 Catamorphic, Co.
+
+This product includes software developed at LaunchDarkly (https://launchdarkly.com/).
diff --git a/server-sdk-ai/features/completion-config/Program.cs b/server-sdk-ai/features/completion-config/Program.cs
new file mode 100644
index 0000000..f6a1393
--- /dev/null
+++ b/server-sdk-ai/features/completion-config/Program.cs
@@ -0,0 +1,163 @@
+using DotNetEnv;
+using LaunchDarkly.Sdk;
+using LaunchDarkly.Sdk.Server;
+using LaunchDarkly.Sdk.Server.Ai;
+using LaunchDarkly.Sdk.Server.Ai.Adapters;
+using LaunchDarkly.Sdk.Server.Ai.Config;
+using LaunchDarkly.Sdk.Server.Ai.Tracking;
+
+Env.TraversePath().Load();
+
+var sdkKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_SDK_KEY");
+if (string.IsNullOrEmpty(sdkKey))
+{
+ Console.Error.WriteLine(
+ "LaunchDarkly SDK key is required: set the LAUNCHDARKLY_SDK_KEY environment variable and try again.");
+ return;
+}
+
+// Set completionKey to the AI config key you want to evaluate.
+var completionKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_COMPLETION_KEY")
+ ?? "sample-completion";
+
+var ldClient = new LdClient(Configuration.Builder(sdkKey).Build());
+if (!ldClient.Initialized)
+{
+ Console.Error.WriteLine(
+ "*** SDK failed to initialize. Please check your internet connection and SDK credential for any typo.");
+ ldClient.Dispose();
+ return;
+}
+Console.WriteLine("*** SDK successfully initialized!");
+
+var aiClient = new LdAiClient(new LdClientAdapter(ldClient));
+
+// Set up the evaluation context. This context should appear on your
+// LaunchDarkly contexts dashboard soon after you run the demo.
+var context = Context.Builder(ContextKind.Of("user"), "example-user-key")
+ .Name("Sandy")
+ .Build();
+
+// Build a fallback default config using the builder pattern. The default is
+// used when LaunchDarkly is unreachable or the AI Config is missing/disabled.
+var defaultValue = LdAiCompletionConfigDefault.New()
+ .Enable()
+ .SetModelName("gpt-4")
+ .SetModelProviderName("openai")
+ .SetModelParam("temperature", LdValue.Of(0.7))
+ .SetModelParam("maxTokens", LdValue.Of(4096))
+ .AddMessage("You are a helpful assistant for {{companyName}}.", LdAiConfigTypes.Role.System)
+ .Build();
+
+try
+{
+ // The variables dictionary supplies values for Mustache placeholders such
+ // as {{companyName}} that appear in message content.
+ var config = aiClient.CompletionConfig(
+ completionKey,
+ context,
+ defaultValue,
+ variables: new Dictionary
+ {
+ ["companyName"] = "LaunchDarkly"
+ });
+
+ if (!config.Enabled)
+ {
+ Console.WriteLine($"AI config '{completionKey}' is disabled.");
+ return;
+ }
+
+ Console.WriteLine();
+ Console.WriteLine($"Resolved model: {config.Model.Name} (provider: {config.Provider.Name})");
+
+ // Read typed model parameters set on the AI Config. Falls back to a
+ // sensible default if the parameter is absent or null.
+ var temperature = config.Model.Parameters.TryGetValue("temperature", out var tempVal) && !tempVal.IsNull
+ ? tempVal.AsFloat
+ : 0.5f;
+ var maxTokens = config.Model.Parameters.TryGetValue("maxTokens", out var maxVal) && !maxVal.IsNull
+ ? maxVal.AsInt
+ : 4096;
+
+ Console.WriteLine($" temperature: {temperature}");
+ Console.WriteLine($" maxTokens: {maxTokens}");
+
+ if (config.Messages.Count > 0)
+ {
+ Console.WriteLine();
+ Console.WriteLine("Resolved messages (Mustache variables interpolated):");
+ foreach (var m in config.Messages)
+ {
+ Console.WriteLine($" [{m.Role}] {m.Content}");
+ }
+ }
+
+ if (config.Tools.Count > 0)
+ {
+ Console.WriteLine();
+ Console.WriteLine("Available tools:");
+ foreach (var (name, tool) in config.Tools)
+ {
+ Console.WriteLine($" {name}: {tool.Description} (type: {tool.Type})");
+ }
+ }
+
+ var tracker = config.CreateTracker();
+
+ // To keep this example provider-agnostic, the operation below is a
+ // synthetic stand-in for an actual model call — see the getting-started/*
+ // examples for end-to-end integrations with OpenAI or AWS Bedrock. The
+ // tracker API is exercised exactly the same way regardless of provider.
+ Console.WriteLine();
+ Console.WriteLine("Running a synthetic model call wrapped in TrackMetricsOf...");
+
+ var result = await tracker.TrackMetricsOf(
+ SyntheticMetrics,
+ async () =>
+ {
+ await Task.Delay(150);
+ return new SyntheticResult(
+ Content: "[simulated model response]",
+ InputTokens: 50,
+ OutputTokens: 100);
+ });
+
+ Console.WriteLine();
+ Console.WriteLine("Simulated model response:");
+ Console.WriteLine(result.Content);
+
+ PrintSummary(tracker.Summary);
+}
+catch (Exception ex)
+{
+ Console.Error.WriteLine($"Error: {ex.Message}");
+}
+finally
+{
+ ldClient.FlushAndWait(TimeSpan.FromSeconds(5));
+ ldClient.Dispose();
+}
+
+static AiMetrics SyntheticMetrics(SyntheticResult result) => new(
+ success: true,
+ tokens: new Usage(
+ Total: result.InputTokens + result.OutputTokens,
+ Input: result.InputTokens,
+ Output: result.OutputTokens));
+
+static void PrintSummary(MetricSummary summary)
+{
+ Console.WriteLine();
+ Console.WriteLine("Done! The tracker captured the following metrics:");
+ Console.WriteLine($" Duration: {summary.DurationMs}ms");
+ Console.WriteLine($" Success: {summary.Success}");
+ if (summary.Tokens is { } tokens)
+ {
+ Console.WriteLine($" Input tokens: {tokens.Input}");
+ Console.WriteLine($" Output tokens: {tokens.Output}");
+ Console.WriteLine($" Total tokens: {tokens.Total}");
+ }
+}
+
+internal sealed record SyntheticResult(string Content, int InputTokens, int OutputTokens);
diff --git a/server-sdk-ai/features/completion-config/README.md b/server-sdk-ai/features/completion-config/README.md
new file mode 100644
index 0000000..134fd80
--- /dev/null
+++ b/server-sdk-ai/features/completion-config/README.md
@@ -0,0 +1,34 @@
+# Completion Config with Default Values
+
+This example focuses on the LaunchDarkly AI SDK itself — it is **provider-agnostic** and does not call OpenAI, Bedrock, Anthropic, or any other model. The actual model call is replaced with a synthetic operation so you can see the SDK's tracking surface clearly. For end-to-end provider integrations see the [getting-started examples](../../frameworks).
+
+It demonstrates:
+
+- Building a fallback default config with `LdAiCompletionConfigDefault.New()`
+- Using Mustache template variables to interpolate prompt content
+- Extracting typed model parameters (`temperature`, `maxTokens`) from the resolved config
+- Enumerating tools configured on the AI Config
+- Wrapping any provider call in `tracker.TrackMetricsOf` and reading the resulting `MetricSummary`
+
+## Prerequisites
+
+- .NET 8.0 SDK
+- A LaunchDarkly account and a server-side SDK key
+
+## Setup
+
+1. [Create an AI Config](https://launchdarkly.com/docs/home/ai-configs/create) in LaunchDarkly with the key `sample-completion`. Add a system message that contains a Mustache variable (for example `You are a helpful assistant for {{companyName}}.`). Optionally set `temperature` and `maxTokens` model parameters.
+2. Copy `.env.example` to `.env` and fill in your SDK key:
+ ```sh
+ cp .env.example .env
+ ```
+3. Restore dependencies:
+ ```sh
+ dotnet restore
+ ```
+
+## Run
+
+```sh
+dotnet run
+```
diff --git a/server-sdk-ai/features/judge-config/.env.example b/server-sdk-ai/features/judge-config/.env.example
new file mode 100644
index 0000000..60e0cb4
--- /dev/null
+++ b/server-sdk-ai/features/judge-config/.env.example
@@ -0,0 +1,5 @@
+# Your LaunchDarkly server-side SDK key
+LAUNCHDARKLY_SDK_KEY=
+
+# Override to use a different AI Config key
+LAUNCHDARKLY_JUDGE_KEY=sample-judge
diff --git a/server-sdk-ai/features/judge-config/JudgeConfig.csproj b/server-sdk-ai/features/judge-config/JudgeConfig.csproj
new file mode 100644
index 0000000..3ec26a6
--- /dev/null
+++ b/server-sdk-ai/features/judge-config/JudgeConfig.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
diff --git a/server-sdk-ai/features/judge-config/NOTICE b/server-sdk-ai/features/judge-config/NOTICE
new file mode 100644
index 0000000..401df22
--- /dev/null
+++ b/server-sdk-ai/features/judge-config/NOTICE
@@ -0,0 +1,4 @@
+LaunchDarkly .NET Server AI SDK Judge Config example
+Copyright 2026 Catamorphic, Co.
+
+This product includes software developed at LaunchDarkly (https://launchdarkly.com/).
diff --git a/server-sdk-ai/features/judge-config/Program.cs b/server-sdk-ai/features/judge-config/Program.cs
new file mode 100644
index 0000000..c103bad
--- /dev/null
+++ b/server-sdk-ai/features/judge-config/Program.cs
@@ -0,0 +1,165 @@
+using DotNetEnv;
+using LaunchDarkly.Sdk;
+using LaunchDarkly.Sdk.Server;
+using LaunchDarkly.Sdk.Server.Ai;
+using LaunchDarkly.Sdk.Server.Ai.Adapters;
+using LaunchDarkly.Sdk.Server.Ai.Config;
+using LaunchDarkly.Sdk.Server.Ai.Tracking;
+
+Env.TraversePath().Load();
+
+var sdkKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_SDK_KEY");
+if (string.IsNullOrEmpty(sdkKey))
+{
+ Console.Error.WriteLine(
+ "LaunchDarkly SDK key is required: set the LAUNCHDARKLY_SDK_KEY environment variable and try again.");
+ return;
+}
+
+// Set judgeKey to the AI config key you want to evaluate.
+var judgeKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_JUDGE_KEY")
+ ?? "sample-judge";
+
+var ldClient = new LdClient(Configuration.Builder(sdkKey).Build());
+if (!ldClient.Initialized)
+{
+ Console.Error.WriteLine(
+ "*** SDK failed to initialize. Please check your internet connection and SDK credential for any typo.");
+ ldClient.Dispose();
+ return;
+}
+Console.WriteLine("*** SDK successfully initialized!");
+
+var aiClient = new LdAiClient(new LdClientAdapter(ldClient));
+
+// Set up the evaluation context. This context should appear on your
+// LaunchDarkly contexts dashboard soon after you run the demo.
+var context = Context.Builder(ContextKind.Of("user"), "example-user-key")
+ .Name("Sandy")
+ .Build();
+
+// Default judge config used as a fallback when LaunchDarkly is unreachable.
+var defaultValue = LdAiJudgeConfigDefault.New()
+ .Enable()
+ .SetModelName("gpt-4")
+ .SetModelProviderName("openai")
+ .SetEvaluationMetricKey("quality")
+ .AddMessage(
+ "You are an evaluator. Score the assistant response from 0.0 (poor) to " +
+ "1.0 (excellent) based on quality and relevance.",
+ LdAiConfigTypes.Role.System)
+ .AddMessage("Input given to the assistant: {{message_history}}", LdAiConfigTypes.Role.User)
+ .AddMessage("Response to evaluate: {{response_to_evaluate}}", LdAiConfigTypes.Role.User)
+ .Build();
+
+// Sample input/output to evaluate. In a real application these would come
+// from a previous completion call (typically a tracker resumed via
+// LdAiClient.CreateTracker with a resumption token).
+const string messageHistory = "How can you help me?";
+const string outputText = "I can answer any question except questions about LaunchDarkly.";
+
+try
+{
+ var judgeConfig = aiClient.JudgeConfig(
+ judgeKey,
+ context,
+ defaultValue,
+ variables: new Dictionary
+ {
+ ["message_history"] = messageHistory,
+ ["response_to_evaluate"] = outputText
+ });
+
+ if (!judgeConfig.Enabled)
+ {
+ Console.WriteLine($"Judge config '{judgeKey}' is disabled.");
+ return;
+ }
+
+ Console.WriteLine();
+ Console.WriteLine($"Resolved judge model: {judgeConfig.Model.Name} (provider: {judgeConfig.Provider.Name})");
+ Console.WriteLine($"Evaluation metric key: {judgeConfig.EvaluationMetricKey}");
+
+ if (judgeConfig.Messages.Count > 0)
+ {
+ Console.WriteLine();
+ Console.WriteLine("Resolved judge prompt (Mustache variables interpolated):");
+ foreach (var m in judgeConfig.Messages)
+ {
+ Console.WriteLine($" [{m.Role}] {m.Content}");
+ }
+ }
+
+ var tracker = judgeConfig.CreateTracker();
+
+ // To keep this example provider-agnostic, we wrap a synthetic evaluation in
+ // TrackMetricsOf and report a hardcoded score. In a real integration the
+ // operation would call your model provider with the judge's resolved
+ // messages and parse the score from the response — see the
+ // getting-started/* examples for end-to-end provider integrations.
+ Console.WriteLine();
+ Console.WriteLine("Running a synthetic judge evaluation wrapped in TrackMetricsOf...");
+
+ var evaluation = await tracker.TrackMetricsOf(
+ SyntheticMetrics,
+ async () =>
+ {
+ await Task.Delay(150);
+ return new SyntheticJudgeResult(
+ Score: 0.8,
+ Reasoning: "Response was clear but explicitly refused to help with the requested topic.",
+ InputTokens: 120,
+ OutputTokens: 60);
+ });
+
+ // Emit the judge result back to LaunchDarkly. The event is silently
+ // dropped when Sampled or Success is false.
+ tracker.TrackJudgeResult(new JudgeResult(
+ metricKey: judgeConfig.EvaluationMetricKey,
+ score: evaluation.Score,
+ sampled: true,
+ success: true,
+ judgeConfigKey: judgeKey));
+
+ Console.WriteLine();
+ Console.WriteLine($"Simulated score: {evaluation.Score}");
+ Console.WriteLine($"Reasoning: {evaluation.Reasoning}");
+
+ PrintSummary(tracker.Summary);
+}
+catch (Exception ex)
+{
+ Console.Error.WriteLine($"Error: {ex.Message}");
+}
+finally
+{
+ ldClient.FlushAndWait(TimeSpan.FromSeconds(5));
+ ldClient.Dispose();
+}
+
+static AiMetrics SyntheticMetrics(SyntheticJudgeResult result) => new(
+ success: true,
+ tokens: new Usage(
+ Total: result.InputTokens + result.OutputTokens,
+ Input: result.InputTokens,
+ Output: result.OutputTokens));
+
+static void PrintSummary(MetricSummary summary)
+{
+ Console.WriteLine();
+ Console.WriteLine("Done! The tracker captured the following metrics:");
+ Console.WriteLine($" Duration: {summary.DurationMs}ms");
+ Console.WriteLine($" Success: {summary.Success}");
+ if (summary.Tokens is { } tokens)
+ {
+ Console.WriteLine($" Input tokens: {tokens.Input}");
+ Console.WriteLine($" Output tokens: {tokens.Output}");
+ Console.WriteLine($" Total tokens: {tokens.Total}");
+ }
+}
+
+internal sealed record SyntheticJudgeResult(
+ double Score,
+ string Reasoning,
+ int InputTokens,
+ int OutputTokens);
diff --git a/server-sdk-ai/features/judge-config/README.md b/server-sdk-ai/features/judge-config/README.md
new file mode 100644
index 0000000..a5027cf
--- /dev/null
+++ b/server-sdk-ai/features/judge-config/README.md
@@ -0,0 +1,34 @@
+# Judge Config
+
+This example focuses on the LaunchDarkly AI SDK itself — it is **provider-agnostic** and does not call OpenAI, Bedrock, Anthropic, or any other model. The actual judge evaluation is replaced with a synthetic operation that returns a hardcoded score so you can see the SDK's tracking surface clearly. For end-to-end provider integrations see the [getting-started examples](../../frameworks).
+
+It demonstrates:
+
+- Retrieving a judge configuration (prompt messages and evaluation metric key) via `LdAiClient.JudgeConfig`
+- Using the builder pattern (`LdAiJudgeConfigDefault.New()`) to declare a fallback default
+- Passing template variables for the input under evaluation and the response to evaluate
+- Wrapping the judge invocation in `tracker.TrackMetricsOf`
+- Emitting the judge result via `tracker.TrackJudgeResult` using the metric key from the config
+
+## Prerequisites
+
+- .NET 8.0 SDK
+- A LaunchDarkly account and a server-side SDK key
+
+## Setup
+
+1. [Create an AI Config](https://launchdarkly.com/docs/home/ai-configs/create) of type **Judge** in LaunchDarkly with the key `sample-judge`. Set an evaluation metric key (for example `quality`), choose a provider and model, and supply judge prompts that may reference Mustache variables such as `{{input}}` and `{{response_to_evaluate}}`.
+2. Copy `.env.example` to `.env` and fill in your SDK key:
+ ```sh
+ cp .env.example .env
+ ```
+3. Restore dependencies:
+ ```sh
+ dotnet restore
+ ```
+
+## Run
+
+```sh
+dotnet run
+```
diff --git a/server-sdk-ai/frameworks/bedrock/converse/.env.example b/server-sdk-ai/frameworks/bedrock/converse/.env.example
new file mode 100644
index 0000000..b402924
--- /dev/null
+++ b/server-sdk-ai/frameworks/bedrock/converse/.env.example
@@ -0,0 +1,10 @@
+# Your LaunchDarkly server-side SDK key
+LAUNCHDARKLY_SDK_KEY=
+
+# AWS credentials (or use the default AWS credential chain)
+AWS_ACCESS_KEY_ID=
+AWS_SECRET_ACCESS_KEY=
+AWS_DEFAULT_REGION=us-east-1
+
+# Override to use a different AI Config key
+LAUNCHDARKLY_COMPLETION_KEY=sample-completion
diff --git a/server-sdk-ai/frameworks/bedrock/converse/BedrockConverse.csproj b/server-sdk-ai/frameworks/bedrock/converse/BedrockConverse.csproj
new file mode 100644
index 0000000..e311a16
--- /dev/null
+++ b/server-sdk-ai/frameworks/bedrock/converse/BedrockConverse.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/server-sdk-ai/frameworks/bedrock/converse/NOTICE b/server-sdk-ai/frameworks/bedrock/converse/NOTICE
new file mode 100644
index 0000000..92cc136
--- /dev/null
+++ b/server-sdk-ai/frameworks/bedrock/converse/NOTICE
@@ -0,0 +1,4 @@
+LaunchDarkly .NET Server AI SDK Bedrock Converse example
+Copyright 2026 Catamorphic, Co.
+
+This product includes software developed at LaunchDarkly (https://launchdarkly.com/).
diff --git a/server-sdk-ai/frameworks/bedrock/converse/Program.cs b/server-sdk-ai/frameworks/bedrock/converse/Program.cs
new file mode 100644
index 0000000..edf046e
--- /dev/null
+++ b/server-sdk-ai/frameworks/bedrock/converse/Program.cs
@@ -0,0 +1,179 @@
+using System.Net;
+using Amazon.BedrockRuntime;
+using Amazon.BedrockRuntime.Model;
+using DotNetEnv;
+using LaunchDarkly.Sdk;
+using LaunchDarkly.Sdk.Server;
+using LaunchDarkly.Sdk.Server.Ai;
+using LaunchDarkly.Sdk.Server.Ai.Adapters;
+using LaunchDarkly.Sdk.Server.Ai.Config;
+using LaunchDarkly.Sdk.Server.Ai.Tracking;
+
+Env.TraversePath().Load();
+
+var sdkKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_SDK_KEY");
+if (string.IsNullOrEmpty(sdkKey))
+{
+ Console.Error.WriteLine(
+ "LaunchDarkly SDK key is required: set the LAUNCHDARKLY_SDK_KEY environment variable and try again.");
+ return;
+}
+
+// Set completionKey to the AI config key you want to evaluate.
+var completionKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_COMPLETION_KEY")
+ ?? "sample-completion";
+
+var ldClient = new LdClient(Configuration.Builder(sdkKey).Build());
+if (!ldClient.Initialized)
+{
+ Console.Error.WriteLine(
+ "*** SDK failed to initialize. Please check your internet connection and SDK credential for any typo.");
+ ldClient.Dispose();
+ return;
+}
+Console.WriteLine("*** SDK successfully initialized!");
+
+var aiClient = new LdAiClient(new LdClientAdapter(ldClient));
+
+// Set up the evaluation context. This context should appear on your
+// LaunchDarkly contexts dashboard soon after you run the demo.
+var context = Context.Builder(ContextKind.Of("user"), "example-user-key")
+ .Name("Sandy")
+ .Build();
+
+// The AWS SDK reads credentials and the default region from environment
+// variables, the shared credentials file, or IAM role metadata.
+var bedrockClient = new AmazonBedrockRuntimeClient();
+
+try
+{
+ // Pass a defaultValue for improved resiliency when the AI config is
+ // unavailable or LaunchDarkly is unreachable; omit for a disabled default.
+ // Example:
+ // var defaultValue = LdAiCompletionConfigDefault.New()
+ // .Enable()
+ // .SetModelName("anthropic.claude-3-sonnet-20240229-v1:0")
+ // .SetModelProviderName("bedrock")
+ // .AddMessage("You are a helpful assistant.", LdAiConfigTypes.Role.System)
+ // .Build();
+ var config = aiClient.CompletionConfig(
+ completionKey,
+ context,
+ variables: new Dictionary
+ {
+ ["myUserVariable"] = "Testing Variable"
+ });
+
+ if (!config.Enabled)
+ {
+ Console.WriteLine(
+ $"AI config '{completionKey}' is disabled. Verify the config key exists in " +
+ "your LaunchDarkly project and is not targeting a disabled variation.");
+ return;
+ }
+
+ var tracker = config.CreateTracker();
+
+ var (chatMessages, systemMessages) = MapMessages(config.Messages);
+
+ var sampleQuestion = "What can you help me with?";
+ chatMessages.Add(new Message
+ {
+ Role = ConversationRole.User,
+ Content = new List { new() { Text = sampleQuestion } }
+ });
+
+ var modelId = string.IsNullOrEmpty(config.Model.Name) ? "no-model" : config.Model.Name;
+
+ Console.WriteLine();
+ Console.WriteLine($"Sending sample question to {modelId}: \"{sampleQuestion}\"");
+ Console.WriteLine("Waiting for response...");
+
+ var response = await tracker.TrackMetricsOf(
+ ExtractBedrockMetrics,
+ async () => await bedrockClient.ConverseAsync(new ConverseRequest
+ {
+ ModelId = modelId,
+ Messages = chatMessages,
+ System = systemMessages,
+ InferenceConfig = BuildInferenceConfig(config.Model.Parameters)
+ }));
+
+ var aiResponse = response.Output?.Message?.Content?.FirstOrDefault()?.Text ?? "";
+ Console.WriteLine();
+ Console.WriteLine("Model response:");
+ Console.WriteLine(aiResponse);
+
+ PrintSummary(tracker.Summary);
+}
+catch (Exception ex)
+{
+ // In production, sanitize before logging — provider errors may include credentials.
+ Console.Error.WriteLine($"Error: {ex.Message}");
+}
+finally
+{
+ ldClient.FlushAndWait(TimeSpan.FromSeconds(5));
+ ldClient.Dispose();
+}
+
+static AiMetrics ExtractBedrockMetrics(ConverseResponse res) => new(
+ success: res.HttpStatusCode == HttpStatusCode.OK,
+ tokens: res.Usage is null
+ ? null
+ : new Usage(
+ Total: res.Usage.TotalTokens,
+ Input: res.Usage.InputTokens,
+ Output: res.Usage.OutputTokens));
+
+static (List ChatMessages, List SystemMessages) MapMessages(
+ IReadOnlyList sdkMessages)
+{
+ var chat = new List();
+ var system = new List();
+ foreach (var m in sdkMessages)
+ {
+ if (m.Role == LdAiConfigTypes.Role.System)
+ {
+ system.Add(new SystemContentBlock { Text = m.Content });
+ continue;
+ }
+
+ chat.Add(new Message
+ {
+ Role = m.Role == LdAiConfigTypes.Role.Assistant
+ ? ConversationRole.Assistant
+ : ConversationRole.User,
+ Content = new List { new() { Text = m.Content } }
+ });
+ }
+ return (chat, system);
+}
+
+static InferenceConfiguration BuildInferenceConfig(IReadOnlyDictionary parameters)
+{
+ var config = new InferenceConfiguration();
+ if (parameters.TryGetValue("temperature", out var temp) && !temp.IsNull)
+ {
+ config.Temperature = temp.AsFloat;
+ }
+ if (parameters.TryGetValue("maxTokens", out var maxTokens) && !maxTokens.IsNull)
+ {
+ config.MaxTokens = maxTokens.AsInt;
+ }
+ return config;
+}
+
+static void PrintSummary(MetricSummary summary)
+{
+ Console.WriteLine();
+ Console.WriteLine("Done! The AI config was evaluated and the following metrics were tracked:");
+ Console.WriteLine($" Duration: {summary.DurationMs}ms");
+ Console.WriteLine($" Success: {summary.Success}");
+ if (summary.Tokens is { } tokens)
+ {
+ Console.WriteLine($" Input tokens: {tokens.Input}");
+ Console.WriteLine($" Output tokens: {tokens.Output}");
+ Console.WriteLine($" Total tokens: {tokens.Total}");
+ }
+}
diff --git a/server-sdk-ai/frameworks/bedrock/converse/README.md b/server-sdk-ai/frameworks/bedrock/converse/README.md
new file mode 100644
index 0000000..883a73d
--- /dev/null
+++ b/server-sdk-ai/frameworks/bedrock/converse/README.md
@@ -0,0 +1,28 @@
+# AWS Bedrock Converse Example
+
+This example demonstrates how to use the LaunchDarkly AI SDK for .NET with the [AWS Bedrock Converse API](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html).
+
+## Prerequisites
+
+- .NET 8.0 SDK
+- A LaunchDarkly account and a server-side SDK key
+- An AWS account with [Bedrock model access enabled](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html)
+- AWS credentials available to the AWS SDK (environment variables, the shared credentials file, or an IAM role)
+
+## Setup
+
+1. [Create an AI Config](https://launchdarkly.com/docs/home/ai-configs/create) in LaunchDarkly with the key `sample-completion`. Select Bedrock as the provider, set the model name to a Bedrock model ID (for example `anthropic.claude-3-sonnet-20240229-v1:0`), and add a system message.
+2. Copy `.env.example` to `.env` and fill in your keys:
+ ```sh
+ cp .env.example .env
+ ```
+3. Restore dependencies:
+ ```sh
+ dotnet restore
+ ```
+
+## Run
+
+```sh
+dotnet run
+```
diff --git a/server-sdk-ai/frameworks/openai/chat-completions/.env.example b/server-sdk-ai/frameworks/openai/chat-completions/.env.example
new file mode 100644
index 0000000..f7dc420
--- /dev/null
+++ b/server-sdk-ai/frameworks/openai/chat-completions/.env.example
@@ -0,0 +1,8 @@
+# Your LaunchDarkly server-side SDK key
+LAUNCHDARKLY_SDK_KEY=
+
+# Your OpenAI API key
+OPENAI_API_KEY=
+
+# Override to use a different AI Config key
+LAUNCHDARKLY_COMPLETION_KEY=sample-completion
diff --git a/server-sdk-ai/frameworks/openai/chat-completions/NOTICE b/server-sdk-ai/frameworks/openai/chat-completions/NOTICE
new file mode 100644
index 0000000..87678f3
--- /dev/null
+++ b/server-sdk-ai/frameworks/openai/chat-completions/NOTICE
@@ -0,0 +1,4 @@
+LaunchDarkly .NET Server AI SDK OpenAI Chat Completions example
+Copyright 2026 Catamorphic, Co.
+
+This product includes software developed at LaunchDarkly (https://launchdarkly.com/).
diff --git a/server-sdk-ai/frameworks/openai/chat-completions/OpenAiChatCompletions.csproj b/server-sdk-ai/frameworks/openai/chat-completions/OpenAiChatCompletions.csproj
new file mode 100644
index 0000000..7c15241
--- /dev/null
+++ b/server-sdk-ai/frameworks/openai/chat-completions/OpenAiChatCompletions.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
diff --git a/server-sdk-ai/frameworks/openai/chat-completions/Program.cs b/server-sdk-ai/frameworks/openai/chat-completions/Program.cs
new file mode 100644
index 0000000..6664a47
--- /dev/null
+++ b/server-sdk-ai/frameworks/openai/chat-completions/Program.cs
@@ -0,0 +1,139 @@
+using DotNetEnv;
+using LaunchDarkly.Sdk;
+using LaunchDarkly.Sdk.Server;
+using LaunchDarkly.Sdk.Server.Ai;
+using LaunchDarkly.Sdk.Server.Ai.Adapters;
+using LaunchDarkly.Sdk.Server.Ai.Config;
+using LaunchDarkly.Sdk.Server.Ai.Tracking;
+using OpenAI.Chat;
+
+Env.TraversePath().Load();
+
+var sdkKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_SDK_KEY");
+if (string.IsNullOrEmpty(sdkKey))
+{
+ Console.Error.WriteLine(
+ "LaunchDarkly SDK key is required: set the LAUNCHDARKLY_SDK_KEY environment variable and try again.");
+ return;
+}
+
+var openAiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY");
+if (string.IsNullOrEmpty(openAiKey))
+{
+ Console.Error.WriteLine(
+ "OpenAI API key is required: set the OPENAI_API_KEY environment variable and try again.");
+ return;
+}
+
+// Set completionKey to the AI config key you want to evaluate.
+var completionKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_COMPLETION_KEY")
+ ?? "sample-completion";
+
+var ldClient = new LdClient(Configuration.Builder(sdkKey).Build());
+if (!ldClient.Initialized)
+{
+ Console.Error.WriteLine(
+ "*** SDK failed to initialize. Please check your internet connection and SDK credential for any typo.");
+ ldClient.Dispose();
+ return;
+}
+Console.WriteLine("*** SDK successfully initialized!");
+
+var aiClient = new LdAiClient(new LdClientAdapter(ldClient));
+
+// Set up the evaluation context. This context should appear on your
+// LaunchDarkly contexts dashboard soon after you run the demo.
+var context = Context.Builder(ContextKind.Of("user"), "example-user-key")
+ .Name("Sandy")
+ .Build();
+
+try
+{
+ // Pass a defaultValue for improved resiliency when the AI config is
+ // unavailable or LaunchDarkly is unreachable; omit for a disabled default.
+ // Example:
+ // var defaultValue = LdAiCompletionConfigDefault.New()
+ // .Enable()
+ // .SetModelName("gpt-4")
+ // .SetModelProviderName("openai")
+ // .AddMessage("You are a helpful assistant.", LdAiConfigTypes.Role.System)
+ // .Build();
+ var config = aiClient.CompletionConfig(
+ completionKey,
+ context,
+ variables: new Dictionary
+ {
+ ["myUserVariable"] = "Testing Variable"
+ });
+
+ if (!config.Enabled)
+ {
+ Console.WriteLine(
+ $"AI config '{completionKey}' is disabled. Verify the config key exists in " +
+ "your LaunchDarkly project and is not targeting a disabled variation.");
+ return;
+ }
+
+ var tracker = config.CreateTracker();
+
+ var modelName = string.IsNullOrEmpty(config.Model.Name) ? "gpt-4" : config.Model.Name;
+ var chatClient = new ChatClient(modelName, openAiKey);
+
+ var sampleQuestion = "What can you help me with?";
+ var messages = config.Messages
+ .Select(ToOpenAiMessage)
+ .Append(new UserChatMessage(sampleQuestion))
+ .ToList();
+
+ Console.WriteLine();
+ Console.WriteLine($"Sending sample question to {modelName}: \"{sampleQuestion}\"");
+ Console.WriteLine("Waiting for response...");
+
+ var completion = await tracker.TrackMetricsOf(
+ result => new AiMetrics(
+ success: true,
+ tokens: new Usage(
+ Total: result.Value.Usage.TotalTokenCount,
+ Input: result.Value.Usage.InputTokenCount,
+ Output: result.Value.Usage.OutputTokenCount)),
+ async () => await chatClient.CompleteChatAsync(messages));
+
+ var aiResponse = completion.Value.Content[0].Text;
+ Console.WriteLine();
+ Console.WriteLine("Model response:");
+ Console.WriteLine(aiResponse);
+
+ PrintSummary(tracker.Summary);
+}
+catch (Exception ex)
+{
+ // In production, sanitize before logging — provider errors may include credentials.
+ Console.Error.WriteLine($"Error: {ex.Message}");
+}
+finally
+{
+ ldClient.FlushAndWait(TimeSpan.FromSeconds(5));
+ ldClient.Dispose();
+}
+
+static ChatMessage ToOpenAiMessage(LdAiConfigTypes.Message m) => m.Role switch
+{
+ LdAiConfigTypes.Role.System => new SystemChatMessage(m.Content),
+ LdAiConfigTypes.Role.Assistant => new AssistantChatMessage(m.Content),
+ LdAiConfigTypes.Role.User => new UserChatMessage(m.Content),
+ _ => new UserChatMessage(m.Content)
+};
+
+static void PrintSummary(MetricSummary summary)
+{
+ Console.WriteLine();
+ Console.WriteLine("Done! The AI config was evaluated and the following metrics were tracked:");
+ Console.WriteLine($" Duration: {summary.DurationMs}ms");
+ Console.WriteLine($" Success: {summary.Success}");
+ if (summary.Tokens is { } tokens)
+ {
+ Console.WriteLine($" Input tokens: {tokens.Input}");
+ Console.WriteLine($" Output tokens: {tokens.Output}");
+ Console.WriteLine($" Total tokens: {tokens.Total}");
+ }
+}
diff --git a/server-sdk-ai/frameworks/openai/chat-completions/README.md b/server-sdk-ai/frameworks/openai/chat-completions/README.md
new file mode 100644
index 0000000..9375d0f
--- /dev/null
+++ b/server-sdk-ai/frameworks/openai/chat-completions/README.md
@@ -0,0 +1,27 @@
+# OpenAI Chat Completions Example
+
+This example demonstrates how to use the LaunchDarkly AI SDK for .NET with the [OpenAI Chat Completions API](https://platform.openai.com/docs/api-reference/chat).
+
+## Prerequisites
+
+- .NET 8.0 SDK
+- A LaunchDarkly account and a server-side SDK key
+- An [OpenAI API key](https://platform.openai.com/api-keys)
+
+## Setup
+
+1. [Create an AI Config](https://launchdarkly.com/docs/home/ai-configs/create) in LaunchDarkly with the key `sample-completion`. Select OpenAI as the provider, a model such as `gpt-4`, and add a system message.
+2. Copy `.env.example` to `.env` and fill in your keys:
+ ```sh
+ cp .env.example .env
+ ```
+3. Restore dependencies:
+ ```sh
+ dotnet restore
+ ```
+
+## Run
+
+```sh
+dotnet run
+```