From 8aacb445ad75b04054cf00345b0254c2edd6b258 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:23:18 +0000 Subject: [PATCH 1/6] [AIC-1636] fix: Improve usage reporting Co-Authored-By: unknown <> --- pkgs/sdk/server-ai/src/Config/LdAiConfig.cs | 2 +- .../server-ai/src/Interfaces/ILdAiClient.cs | 13 +++++ pkgs/sdk/server-ai/src/LdAiClient.cs | 49 ++++++++++++++++++- pkgs/sdk/server-ai/test/LdAiClientTest.cs | 43 ++++++++++++---- 4 files changed, 94 insertions(+), 13 deletions(-) diff --git a/pkgs/sdk/server-ai/src/Config/LdAiConfig.cs b/pkgs/sdk/server-ai/src/Config/LdAiConfig.cs index ea53d6eb..7fe18702 100644 --- a/pkgs/sdk/server-ai/src/Config/LdAiConfig.cs +++ b/pkgs/sdk/server-ai/src/Config/LdAiConfig.cs @@ -79,7 +79,7 @@ internal ModelConfiguration(string name, IReadOnlyDictionary pa /// /// Builder for constructing an LdAiConfig instance, which can be passed as the default - /// value to the AI Client's method. + /// value to the AI Client's method. /// public class Builder { diff --git a/pkgs/sdk/server-ai/src/Interfaces/ILdAiClient.cs b/pkgs/sdk/server-ai/src/Interfaces/ILdAiClient.cs index 2d8009cb..0d3f30d2 100644 --- a/pkgs/sdk/server-ai/src/Interfaces/ILdAiClient.cs +++ b/pkgs/sdk/server-ai/src/Interfaces/ILdAiClient.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using LaunchDarkly.Sdk.Server.Ai.Config; @@ -24,6 +25,18 @@ public interface ILdAiClient /// the default config, if unable to retrieve from LaunchDarkly /// the list of variables used when interpolating the prompt /// an AI Config tracker + public ILdAiConfigTracker CompletionConfig(string key, Context context, LdAiConfig defaultValue, + IReadOnlyDictionary variables = null); + + /// + /// Retrieves a LaunchDarkly AI Config identified by the given key. + /// + /// the AI Config key + /// the context + /// the default config, if unable to retrieve from LaunchDarkly + /// the list of variables used when interpolating the prompt + /// an AI Config tracker + [Obsolete("Use CompletionConfig instead.")] public ILdAiConfigTracker Config(string key, Context context, LdAiConfig defaultValue, IReadOnlyDictionary variables = null); } diff --git a/pkgs/sdk/server-ai/src/LdAiClient.cs b/pkgs/sdk/server-ai/src/LdAiClient.cs index f7528ab7..d8604365 100644 --- a/pkgs/sdk/server-ai/src/LdAiClient.cs +++ b/pkgs/sdk/server-ai/src/LdAiClient.cs @@ -20,6 +20,24 @@ public sealed class LdAiClient : ILdAiClient private readonly ILaunchDarklyClient _client; private readonly ILogger _logger; + /// + /// The name of the AI SDK package. + /// + internal const string AiSdkName = "LaunchDarkly.ServerSdk.Ai"; + + /// + /// The version of the AI SDK package. + /// + internal const string AiSdkVersion = "0.9.1"; // x-release-please-version + + /// + /// The implementation language. + /// + internal const string AiSdkLanguage = "dotnet"; + + private const string TrackSdkInfo = "$ld:ai:sdk:info"; + private const string TrackUsageCompletionConfig = "$ld:ai:usage:completion-config"; + /// /// Constructs a new LaunchDarkly AI client. Please note, the client library is an alpha release and is /// not considered ready for production use. @@ -36,6 +54,17 @@ public LdAiClient(ILaunchDarklyClient client) { _client = client ?? throw new ArgumentNullException(nameof(client)); _logger = _client.GetLogger(); + + _client.Track( + TrackSdkInfo, + Context.Builder(ContextKind.Of("ld_ai"), "ld-internal-tracking").Anonymous(true).Build(), + LdValue.ObjectFrom(new Dictionary + { + { "aiSdkName", LdValue.Of(AiSdkName) }, + { "aiSdkVersion", LdValue.Of(AiSdkVersion) }, + { "aiSdkLanguage", LdValue.Of(AiSdkLanguage) } + }), + 1); } @@ -44,10 +73,11 @@ public LdAiClient(ILaunchDarklyClient client) private const string LdContextVariable = "ldctx"; /// - public ILdAiConfigTracker Config(string key, Context context, LdAiConfig defaultValue, + public ILdAiConfigTracker CompletionConfig(string key, Context context, LdAiConfig defaultValue, IReadOnlyDictionary variables = null) { - _client.Track("$ld:ai:config:function:single", context, LdValue.Of(key), 1); + _client.Track(TrackUsageCompletionConfig, context, LdValue.Of(key), 1); + var result = _client.JsonVariation(key, context, defaultValue.ToLdValue()); @@ -97,6 +127,21 @@ public ILdAiConfigTracker Config(string key, Context context, LdAiConfig default } + /// + /// Retrieves a LaunchDarkly AI Config identified by the given key. + /// + /// the AI Config key + /// the context + /// the default config, if unable to retrieve from LaunchDarkly + /// the list of variables used when interpolating the prompt + /// an AI Config tracker + [Obsolete("Use CompletionConfig instead.")] + public ILdAiConfigTracker Config(string key, Context context, LdAiConfig defaultValue, + IReadOnlyDictionary variables = null) + { + return CompletionConfig(key, context, defaultValue, variables); + } + private static IDictionary AddSingleKindContextAttributes(Context context) { diff --git a/pkgs/sdk/server-ai/test/LdAiClientTest.cs b/pkgs/sdk/server-ai/test/LdAiClientTest.cs index f650c2b1..2c298dc9 100644 --- a/pkgs/sdk/server-ai/test/LdAiClientTest.cs +++ b/pkgs/sdk/server-ai/test/LdAiClientTest.cs @@ -16,7 +16,7 @@ public void CanInstantiateWithServerSideClient() { var client = new LdClientAdapter(new LdClient(Configuration.Builder("key").Offline(true).Build())); var aiClient = new LdAiClient(client); - var result= aiClient.Config("foo", Context.New("key"), LdAiConfig.Disabled); + var result= aiClient.CompletionConfig("foo", Context.New("key"), LdAiConfig.Disabled); Assert.False(result.Config.Enabled); } @@ -44,13 +44,13 @@ public void ReturnsDefaultConfigWhenGivenInvalidVariation() var defaultConfig = LdAiConfig.New().AddMessage("Hello").Build(); - var tracker = client.Config("foo", Context.New(ContextKind.Default, "key"), defaultConfig); + var tracker = client.CompletionConfig("foo", Context.New(ContextKind.Default, "key"), defaultConfig); Assert.Equal(defaultConfig, tracker.Config); } [Fact] - public void ConfigMethodCallsTrackWithCorrectParameters() + public void CompletionConfigMethodCallsTrackWithCorrectParameters() { var mockClient = new Mock(); var context = Context.New(ContextKind.Default, "user-key"); @@ -85,10 +85,10 @@ public void ConfigMethodCallsTrackWithCorrectParameters() var client = new LdAiClient(mockClient.Object); var defaultConfig = LdAiConfig.New().Build(); - var tracker = client.Config(configKey, context, defaultConfig); + var tracker = client.CompletionConfig(configKey, context, defaultConfig); mockClient.Verify(c => c.Track( - "$ld:ai:config:function:single", + "$ld:ai:usage:completion-config", context, LdValue.Of(configKey), 1), Times.Once); @@ -96,6 +96,29 @@ public void ConfigMethodCallsTrackWithCorrectParameters() Assert.NotNull(tracker); } + [Fact] + public void ConstructorTracksSdkInfo() + { + var mockClient = new Mock(); + var mockLogger = new Mock(); + mockClient.Setup(x => x.GetLogger()).Returns(mockLogger.Object); + + var client = new LdAiClient(mockClient.Object); + Assert.NotNull(client); + + mockClient.Verify(c => c.Track( + "$ld:ai:sdk:info", + It.Is(ctx => + ctx.Kind == ContextKind.Of("ld_ai") && + ctx.Key == "ld-internal-tracking" && + ctx.Anonymous), + It.Is(v => + v.Get("aiSdkName").AsString == LdAiClient.AiSdkName && + v.Get("aiSdkVersion").AsString == LdAiClient.AiSdkVersion && + v.Get("aiSdkLanguage").AsString == LdAiClient.AiSdkLanguage), + 1), Times.Once); + } + private const string MetaDisabledExplicitly = """ { "_ldMeta": {"variationKey": "1", "enabled": false}, @@ -142,7 +165,7 @@ public void ConfigNotEnabledReturnsDisabledInstance(string json) // All the JSON inputs here are considered disabled, either due to lack of the 'enabled' property, // or if present, it is set to false. Therefore, if the default was returned, we'd see the assertion fail // (since calling LdAiConfig.New() constructs an enabled config by default.) - var tracker = client.Config("foo", Context.New(ContextKind.Default, "key"), + var tracker = client.CompletionConfig("foo", Context.New(ContextKind.Default, "key"), LdAiConfig.New().AddMessage("foo").Build()); Assert.False(tracker.Config.Enabled); @@ -162,7 +185,7 @@ public void CanSetAllDefaultValueFields() var client = new LdAiClient(mockClient.Object); - var tracker = client.Config("foo", Context.New(ContextKind.Default, "key"), + var tracker = client.CompletionConfig("foo", Context.New(ContextKind.Default, "key"), LdAiConfig.New(). AddMessage("foo"). SetModelParam("foo", LdValue.Of("bar")). @@ -207,7 +230,7 @@ public void ConfigEnabledReturnsInstance() var client = new LdAiClient(mockClient.Object); // We shouldn't get this default. - var tracker = client.Config("foo", context, + var tracker = client.CompletionConfig("foo", context, LdAiConfig.New().AddMessage("Goodbye!").Build()); Assert.Collection(tracker.Config.Messages, @@ -259,7 +282,7 @@ public void ModelParametersAreParsed() var client = new LdAiClient(mockClient.Object); // We shouldn't get this default. - var tracker = client.Config("foo", context, + var tracker = client.CompletionConfig("foo", context, LdAiConfig.New().AddMessage("Goodbye!").Build()); Assert.Equal("model-foo", tracker.Config.Model.Name); @@ -296,7 +319,7 @@ public void ProviderConfigIsParsed() var client = new LdAiClient(mockClient.Object); // We shouldn't get this default. - var tracker = client.Config("foo", context, + var tracker = client.CompletionConfig("foo", context, LdAiConfig.New().AddMessage("Goodbye!").Build()); Assert.Equal("amazing-provider", tracker.Config.Provider.Name); From c2863d7342c3d57e3451a392fa46456b34645ce2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:26:07 +0000 Subject: [PATCH 2/6] [AIC-1636] fix: Make SDK info constants public for test visibility Co-Authored-By: unknown <> --- pkgs/sdk/server-ai/src/LdAiClient.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkgs/sdk/server-ai/src/LdAiClient.cs b/pkgs/sdk/server-ai/src/LdAiClient.cs index d8604365..0353bc67 100644 --- a/pkgs/sdk/server-ai/src/LdAiClient.cs +++ b/pkgs/sdk/server-ai/src/LdAiClient.cs @@ -23,17 +23,17 @@ public sealed class LdAiClient : ILdAiClient /// /// The name of the AI SDK package. /// - internal const string AiSdkName = "LaunchDarkly.ServerSdk.Ai"; + public const string AiSdkName = "LaunchDarkly.ServerSdk.Ai"; /// /// The version of the AI SDK package. /// - internal const string AiSdkVersion = "0.9.1"; // x-release-please-version + public const string AiSdkVersion = "0.9.1"; // x-release-please-version /// /// The implementation language. /// - internal const string AiSdkLanguage = "dotnet"; + public const string AiSdkLanguage = "dotnet"; private const string TrackSdkInfo = "$ld:ai:sdk:info"; private const string TrackUsageCompletionConfig = "$ld:ai:usage:completion-config"; From 66fdd1098e4bbae0895f5e2a2336c8873d45f4dd Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:28:06 +0000 Subject: [PATCH 3/6] [AIC-1636] refactor: Extract SDK info into dedicated SdkInfo class Co-Authored-By: unknown <> --- pkgs/sdk/server-ai/src/LdAiClient.cs | 21 +++------------------ pkgs/sdk/server-ai/src/SdkInfo.cs | 22 ++++++++++++++++++++++ pkgs/sdk/server-ai/test/LdAiClientTest.cs | 6 +++--- 3 files changed, 28 insertions(+), 21 deletions(-) create mode 100644 pkgs/sdk/server-ai/src/SdkInfo.cs diff --git a/pkgs/sdk/server-ai/src/LdAiClient.cs b/pkgs/sdk/server-ai/src/LdAiClient.cs index 0353bc67..8cbb82ac 100644 --- a/pkgs/sdk/server-ai/src/LdAiClient.cs +++ b/pkgs/sdk/server-ai/src/LdAiClient.cs @@ -20,21 +20,6 @@ public sealed class LdAiClient : ILdAiClient private readonly ILaunchDarklyClient _client; private readonly ILogger _logger; - /// - /// The name of the AI SDK package. - /// - public const string AiSdkName = "LaunchDarkly.ServerSdk.Ai"; - - /// - /// The version of the AI SDK package. - /// - public const string AiSdkVersion = "0.9.1"; // x-release-please-version - - /// - /// The implementation language. - /// - public const string AiSdkLanguage = "dotnet"; - private const string TrackSdkInfo = "$ld:ai:sdk:info"; private const string TrackUsageCompletionConfig = "$ld:ai:usage:completion-config"; @@ -60,9 +45,9 @@ public LdAiClient(ILaunchDarklyClient client) Context.Builder(ContextKind.Of("ld_ai"), "ld-internal-tracking").Anonymous(true).Build(), LdValue.ObjectFrom(new Dictionary { - { "aiSdkName", LdValue.Of(AiSdkName) }, - { "aiSdkVersion", LdValue.Of(AiSdkVersion) }, - { "aiSdkLanguage", LdValue.Of(AiSdkLanguage) } + { "aiSdkName", LdValue.Of(SdkInfo.Name) }, + { "aiSdkVersion", LdValue.Of(SdkInfo.Version) }, + { "aiSdkLanguage", LdValue.Of(SdkInfo.Language) } }), 1); } diff --git a/pkgs/sdk/server-ai/src/SdkInfo.cs b/pkgs/sdk/server-ai/src/SdkInfo.cs new file mode 100644 index 00000000..9e968175 --- /dev/null +++ b/pkgs/sdk/server-ai/src/SdkInfo.cs @@ -0,0 +1,22 @@ +namespace LaunchDarkly.Sdk.Server.Ai; + +/// +/// Contains metadata about the AI SDK, such as its name, version, and implementation language. +/// +public static class SdkInfo +{ + /// + /// The name of the AI SDK package. + /// + public const string Name = "LaunchDarkly.ServerSdk.Ai"; + + /// + /// The version of the AI SDK package. + /// + public const string Version = "0.9.1"; // x-release-please-version + + /// + /// The implementation language. + /// + public const string Language = "dotnet"; +} diff --git a/pkgs/sdk/server-ai/test/LdAiClientTest.cs b/pkgs/sdk/server-ai/test/LdAiClientTest.cs index 2c298dc9..4d8b2639 100644 --- a/pkgs/sdk/server-ai/test/LdAiClientTest.cs +++ b/pkgs/sdk/server-ai/test/LdAiClientTest.cs @@ -113,9 +113,9 @@ public void ConstructorTracksSdkInfo() ctx.Key == "ld-internal-tracking" && ctx.Anonymous), It.Is(v => - v.Get("aiSdkName").AsString == LdAiClient.AiSdkName && - v.Get("aiSdkVersion").AsString == LdAiClient.AiSdkVersion && - v.Get("aiSdkLanguage").AsString == LdAiClient.AiSdkLanguage), + v.Get("aiSdkName").AsString == SdkInfo.Name && + v.Get("aiSdkVersion").AsString == SdkInfo.Version && + v.Get("aiSdkLanguage").AsString == SdkInfo.Language), 1), Times.Once); } From 38ab2dbf3b7593ea719b1e717b54a64e83a8bfbf Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:00:09 +0000 Subject: [PATCH 4/6] [AIC-1636] refactor: Split CompletionConfig into public entry + private Evaluate to track usage only once Co-Authored-By: unknown <> --- pkgs/sdk/server-ai/src/LdAiClient.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pkgs/sdk/server-ai/src/LdAiClient.cs b/pkgs/sdk/server-ai/src/LdAiClient.cs index 8cbb82ac..ffedb7d5 100644 --- a/pkgs/sdk/server-ai/src/LdAiClient.cs +++ b/pkgs/sdk/server-ai/src/LdAiClient.cs @@ -63,7 +63,17 @@ public ILdAiConfigTracker CompletionConfig(string key, Context context, LdAiConf { _client.Track(TrackUsageCompletionConfig, context, LdValue.Of(key), 1); + return Evaluate(key, context, defaultValue, variables); + } + /// + /// Internal evaluation method that retrieves and parses an AI Config without tracking usage. + /// This allows higher-level SDK entry methods to track their own usage events without + /// double-counting. + /// + private ILdAiConfigTracker Evaluate(string key, Context context, LdAiConfig defaultValue, + IReadOnlyDictionary variables = null) + { var result = _client.JsonVariation(key, context, defaultValue.ToLdValue()); var parsed = ParseConfig(result, key); @@ -87,7 +97,6 @@ public ILdAiConfigTracker CompletionConfig(string key, Context context, LdAiConf } } - var prompt = new List(); if (parsed.Messages != null) @@ -109,7 +118,6 @@ public ILdAiConfigTracker CompletionConfig(string key, Context context, LdAiConf } return new LdAiConfigTracker(_client, key, new LdAiConfig(parsed.Meta?.Enabled ?? false, prompt, parsed.Meta, parsed.Model, parsed.Provider), context); - } /// From a78a6032de016e45730f24fa740971e9c3779feb Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:38:42 +0000 Subject: [PATCH 5/6] [AIC-1636] fix: Add SdkInfo.cs to release-please extra-files for version auto-update Co-Authored-By: unknown <> --- release-please-config.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/release-please-config.json b/release-please-config.json index c929fb24..ed3d07e4 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -36,10 +36,11 @@ "package-name": "LaunchDarkly.ServerSdk.Ai", "bump-minor-pre-major": true, "extra-files": [ - "src/LaunchDarkly.ServerSdk.Ai.csproj" + "src/LaunchDarkly.ServerSdk.Ai.csproj", + "src/SdkInfo.cs" ] }, - "pkgs/sdk/client": { + "pkgs/sdk/client":{ "release-type": "simple", "package-name": "LaunchDarkly.ClientSdk", "extra-files": [ From 11e992f32d47eb43c27d735ae35cf78953620cec Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 22:47:42 +0000 Subject: [PATCH 6/6] [AIC-1636] docs: Update doc comments to use 'Completion Config' naming Co-Authored-By: unknown <> --- pkgs/sdk/server-ai/src/Interfaces/ILdAiClient.cs | 12 ++++++------ pkgs/sdk/server-ai/src/LdAiClient.cs | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pkgs/sdk/server-ai/src/Interfaces/ILdAiClient.cs b/pkgs/sdk/server-ai/src/Interfaces/ILdAiClient.cs index 0d3f30d2..2e5c32bd 100644 --- a/pkgs/sdk/server-ai/src/Interfaces/ILdAiClient.cs +++ b/pkgs/sdk/server-ai/src/Interfaces/ILdAiClient.cs @@ -11,7 +11,7 @@ public interface ILdAiClient { /// - /// Retrieves a LaunchDarkly AI Config identified by the given key. The return value + /// Retrieves a LaunchDarkly AI Completion Config identified by the given key. The return value /// is an , which makes the configuration available and /// provides convenience methods for generating events related to model usage. /// @@ -20,22 +20,22 @@ public interface ILdAiClient /// a prompt message. /// /// - /// the AI Config key + /// the AI Completion Config key /// the context /// the default config, if unable to retrieve from LaunchDarkly /// the list of variables used when interpolating the prompt - /// an AI Config tracker + /// an AI Completion Config tracker public ILdAiConfigTracker CompletionConfig(string key, Context context, LdAiConfig defaultValue, IReadOnlyDictionary variables = null); /// - /// Retrieves a LaunchDarkly AI Config identified by the given key. + /// Retrieves a LaunchDarkly AI Completion Config identified by the given key. /// - /// the AI Config key + /// the AI Completion Config key /// the context /// the default config, if unable to retrieve from LaunchDarkly /// the list of variables used when interpolating the prompt - /// an AI Config tracker + /// an AI Completion Config tracker [Obsolete("Use CompletionConfig instead.")] public ILdAiConfigTracker Config(string key, Context context, LdAiConfig defaultValue, IReadOnlyDictionary variables = null); diff --git a/pkgs/sdk/server-ai/src/LdAiClient.cs b/pkgs/sdk/server-ai/src/LdAiClient.cs index ffedb7d5..9690ad49 100644 --- a/pkgs/sdk/server-ai/src/LdAiClient.cs +++ b/pkgs/sdk/server-ai/src/LdAiClient.cs @@ -121,13 +121,13 @@ private ILdAiConfigTracker Evaluate(string key, Context context, LdAiConfig defa } /// - /// Retrieves a LaunchDarkly AI Config identified by the given key. + /// Retrieves a LaunchDarkly AI Completion Config identified by the given key. /// - /// the AI Config key + /// the AI Completion Config key /// the context /// the default config, if unable to retrieve from LaunchDarkly /// the list of variables used when interpolating the prompt - /// an AI Config tracker + /// an AI Completion Config tracker [Obsolete("Use CompletionConfig instead.")] public ILdAiConfigTracker Config(string key, Context context, LdAiConfig defaultValue, IReadOnlyDictionary variables = null)