Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions pkgs/sdk/server-ai/src/Config/ConfigFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public LdAiCompletionConfig BuildCompletionConfig(
return BuildCompletionFromDefault(key, defaultValue, mergedVars, trackerFactory, interpolate);
}

var (enabled, variationKey, version, mode) = ParseMeta(ldValue);
var (enabled, variationKey, version, mode, modelKey, modelVersion) = ParseMeta(ldValue);

if (mode != LdAiCompletionConfig.Mode)
{
Expand All @@ -70,6 +70,8 @@ public LdAiCompletionConfig BuildCompletionConfig(
enabled,
variationKey,
version,
modelKey,
modelVersion,
messages,
tools,
judgeConfiguration,
Expand All @@ -95,6 +97,8 @@ private LdAiCompletionConfig BuildCompletionFromDefault(
defaultValue.Enabled ?? true,
variationKey: "",
version: 1,
modelKey: null,
modelVersion: 1,
messages,
tools: ImmutableDictionary<string, LdAiConfigTypes.Tool>.Empty,
defaultValue.JudgeConfiguration,
Expand Down Expand Up @@ -123,7 +127,7 @@ public LdAiAgentConfig BuildAgentConfig(
return BuildAgentFromDefault(key, defaultValue, mergedVars, trackerFactory, interpolate);
}

var (enabled, variationKey, version, mode) = ParseMeta(ldValue);
var (enabled, variationKey, version, mode, modelKey, modelVersion) = ParseMeta(ldValue);

if (mode != LdAiAgentConfig.Mode)
{
Expand All @@ -146,6 +150,8 @@ public LdAiAgentConfig BuildAgentConfig(
enabled,
variationKey,
version,
modelKey,
modelVersion,
instructions,
tools,
model,
Expand All @@ -169,6 +175,8 @@ internal LdAiAgentConfig BuildAgentFromDefault(
defaultValue.Enabled ?? true,
variationKey: "",
version: 1,
modelKey: null,
modelVersion: 1,
instructions,
tools: ImmutableDictionary<string, LdAiConfigTypes.Tool>.Empty,
defaultValue.Model,
Expand Down Expand Up @@ -196,7 +204,7 @@ public LdAiJudgeConfig BuildJudgeConfig(
return BuildJudgeFromDefault(key, defaultValue, mergedVars, trackerFactory, interpolate);
}

var (enabled, variationKey, version, mode) = ParseMeta(ldValue);
var (enabled, variationKey, version, mode, modelKey, modelVersion) = ParseMeta(ldValue);

if (mode != LdAiJudgeConfig.Mode)
{
Expand All @@ -218,6 +226,8 @@ public LdAiJudgeConfig BuildJudgeConfig(
enabled,
variationKey,
version,
modelKey,
modelVersion,
messages,
evaluationMetricKey,
model,
Expand All @@ -240,6 +250,8 @@ private LdAiJudgeConfig BuildJudgeFromDefault(
defaultValue.Enabled ?? true,
variationKey: "",
version: 1,
modelKey: null,
modelVersion: 1,
messages,
defaultValue.EvaluationMetricKey,
defaultValue.Model,
Expand Down Expand Up @@ -305,10 +317,17 @@ private Func<LdAiConfig, ILdAiConfigTracker> TrackerFactoryFor(Context context,
context,
cfg.Model?.Name,
cfg.Provider?.Name,
cfg.ModelKey,
cfg.ModelVersion,
graphKey);
}

private static (bool Enabled, string VariationKey, int Version, string Mode) ParseMeta(LdValue value)
// _ldMeta carries modelKey/modelVersion alongside the variation-level fields (rather than
// the model object) so modelVersion can't be mistaken for the underlying LLM's own version.
private readonly record struct Meta(
bool Enabled, string VariationKey, int Version, string Mode, string ModelKey, int ModelVersion);

private static Meta ParseMeta(LdValue value)
{
var meta = value.Get("_ldMeta");
var enabled = meta.Get("enabled").AsBool;
Expand All @@ -318,7 +337,10 @@ private static (bool Enabled, string VariationKey, int Version, string Mode) Par
// Default to the completion mode when _ldMeta.mode is missing or non-string: legacy
// flags predate the mode tag and were always served as completion configs.
var mode = meta.Get("mode").AsString ?? LdAiCompletionConfig.Mode;
return (enabled, variationKey, version, mode);
var modelKey = meta.Get("modelKey").AsString;
var modelVersionValue = meta.Get("modelVersion");
var modelVersion = modelVersionValue.IsNull ? 1 : modelVersionValue.AsInt;
return new Meta(enabled, variationKey, version, mode, modelKey, modelVersion);
}

private static LdAiConfigTypes.ModelConfig ParseModel(LdValue modelValue)
Expand Down
4 changes: 3 additions & 1 deletion pkgs/sdk/server-ai/src/Config/LdAiAgentConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ internal LdAiAgentConfig(
bool enabled,
string variationKey,
int version,
string modelKey,
int modelVersion,
string instructions,
IReadOnlyDictionary<string, LdAiConfigTypes.Tool> tools,
LdAiConfigTypes.ModelConfig model,
LdAiConfigTypes.ProviderConfig provider,
LdAiConfigTypes.JudgeConfiguration judgeConfiguration,
Func<LdAiConfig, ILdAiConfigTracker> trackerFactory)
: base(key, enabled, variationKey, version, model, provider, trackerFactory)
: base(key, enabled, variationKey, version, modelKey, modelVersion, model, provider, trackerFactory)
{
Instructions = instructions;
Tools = tools?.ToImmutableDictionary() ?? ImmutableDictionary<string, LdAiConfigTypes.Tool>.Empty;
Expand Down
3 changes: 2 additions & 1 deletion pkgs/sdk/server-ai/src/Config/LdAiCompletionConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@ public sealed class LdAiCompletionConfig : LdAiConfig
public LdAiConfigTypes.JudgeConfiguration JudgeConfiguration { get; }

internal LdAiCompletionConfig(string key, bool enabled, string variationKey, int version,
string modelKey, int modelVersion,
IEnumerable<LdAiConfigTypes.Message> messages, IReadOnlyDictionary<string, LdAiConfigTypes.Tool> tools,
LdAiConfigTypes.JudgeConfiguration judgeConfiguration,
LdAiConfigTypes.ModelConfig model, LdAiConfigTypes.ProviderConfig provider,
Func<LdAiConfig, ILdAiConfigTracker> trackerFactory)
: base(key, enabled, variationKey, version, model, provider, trackerFactory)
: base(key, enabled, variationKey, version, modelKey, modelVersion, model, provider, trackerFactory)
{
Messages = messages?.ToList() ?? new List<LdAiConfigTypes.Message>();
Tools = tools ?? ImmutableDictionary<string, LdAiConfigTypes.Tool>.Empty;
Expand Down
20 changes: 18 additions & 2 deletions pkgs/sdk/server-ai/src/Config/LdAiConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,23 @@ public abstract class LdAiConfig
/// </summary>
internal int Version { get; }

/// <summary>
/// This field meant for internal LaunchDarkly usage. Not exposed on <see cref="Model"/> so
/// it can't be mistaken for the underlying LLM's own version (e.g. "GPT-5.4").
/// </summary>
internal string ModelKey { get; }

/// <summary>
/// This field meant for internal LaunchDarkly usage.
/// </summary>
internal int ModelVersion { get; }

/// <summary>
/// Factory that produces a tracker for the config. The factory is mode-agnostic — it
/// operates only on the shared fields (<see cref="Key"/>, <see cref="Model"/>,
/// <see cref="Provider"/>, <see cref="VariationKey"/>, <see cref="Version"/>), so the
/// same tracker class serves all config modes.
/// <see cref="Provider"/>, <see cref="VariationKey"/>, <see cref="Version"/>,
/// <see cref="ModelKey"/>, <see cref="ModelVersion"/>), so the same tracker class serves
/// all config modes.
/// </summary>
private readonly Func<LdAiConfig, ILdAiConfigTracker> _trackerFactory;

Expand All @@ -64,6 +76,8 @@ private protected LdAiConfig(
bool enabled,
string variationKey,
int version,
string modelKey,
int modelVersion,
LdAiConfigTypes.ModelConfig model,
LdAiConfigTypes.ProviderConfig provider,
Func<LdAiConfig, ILdAiConfigTracker> trackerFactory)
Expand All @@ -72,6 +86,8 @@ private protected LdAiConfig(
Enabled = enabled;
VariationKey = variationKey ?? "";
Version = version;
ModelKey = modelKey;
ModelVersion = modelVersion;
Model = model ?? new LdAiConfigTypes.ModelConfig("", new Dictionary<string, LdValue>(), new Dictionary<string, LdValue>());
Provider = provider ?? new LdAiConfigTypes.ProviderConfig("");
_trackerFactory = trackerFactory ?? throw new ArgumentNullException(nameof(trackerFactory));
Expand Down
5 changes: 4 additions & 1 deletion pkgs/sdk/server-ai/src/Config/LdAiConfigTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ public sealed record ModelConfig
/// </summary>
public readonly IReadOnlyDictionary<string, LdValue> Custom;

internal ModelConfig(string name, IReadOnlyDictionary<string, LdValue> parameters, IReadOnlyDictionary<string, LdValue> custom)
internal ModelConfig(
string name,
IReadOnlyDictionary<string, LdValue> parameters,
IReadOnlyDictionary<string, LdValue> custom)
{
Name = name;
// Materialize into an ImmutableDictionary so a consumer that downcasts to
Expand Down
4 changes: 3 additions & 1 deletion pkgs/sdk/server-ai/src/Config/LdAiJudgeConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ internal LdAiJudgeConfig(
bool enabled,
string variationKey,
int version,
string modelKey,
int modelVersion,
IEnumerable<LdAiConfigTypes.Message> messages,
string evaluationMetricKey,
LdAiConfigTypes.ModelConfig model,
LdAiConfigTypes.ProviderConfig provider,
Func<LdAiConfig, ILdAiConfigTracker> trackerFactory)
: base(key, enabled, variationKey, version, model, provider, trackerFactory)
: base(key, enabled, variationKey, version, modelKey, modelVersion, model, provider, trackerFactory)
{
Messages = messages?.ToList() ?? new List<LdAiConfigTypes.Message>();
EvaluationMetricKey = evaluationMetricKey;
Expand Down
13 changes: 10 additions & 3 deletions pkgs/sdk/server-ai/src/LdAiConfigTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public class LdAiConfigTracker : ILdAiConfigTracker
/// </summary>
internal LdAiConfigTracker(ILaunchDarklyClient client, string runId, string configKey,
string variationKey, int version, Context context, string modelName, string providerName,
string graphKey = null)
string modelKey = null, int modelVersion = 1, string graphKey = null)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
_configKey = configKey ?? throw new ArgumentNullException(nameof(configKey));
Expand All @@ -96,6 +96,7 @@ internal LdAiConfigTracker(ILaunchDarklyClient client, string runId, string conf
{ "version", LdValue.Of(_version) },
{ "modelName", LdValue.Of(_modelName) },
{ "providerName", LdValue.Of(_providerName) },
{ "modelVersion", LdValue.Of(modelVersion) },
};
if (!string.IsNullOrEmpty(_graphKey))
{
Expand All @@ -105,6 +106,10 @@ internal LdAiConfigTracker(ILaunchDarklyClient client, string runId, string conf
{
trackDataBuilder.Add("variationKey", LdValue.Of(_variationKey));
}
if (!string.IsNullOrEmpty(modelKey))
{
trackDataBuilder.Add("modelKey", LdValue.Of(modelKey));
}
_trackData = LdValue.ObjectFrom(trackDataBuilder);

_resumptionToken = new Lazy<string>(BuildResumptionToken);
Expand All @@ -118,6 +123,7 @@ private string BuildResumptionToken()
// Utf8JsonWriter gives stable key ordering and avoids the runtime cost of
// anonymous-type reflection. The wire format omits empty optional fields so that
// resumption tokens round-trip exactly for configs that never carried them.
// modelName, providerName, modelKey, and modelVersion are not included.
using var stream = new MemoryStream();
using (var writer = new Utf8JsonWriter(stream))
{
Expand Down Expand Up @@ -426,7 +432,8 @@ private LdValue MergeTrackData(string key, LdValue value)
/// process or at a later time.
///
/// The reconstructed tracker will have empty model and provider names, as these are not
/// included in the resumption token.
/// included in the resumption token. modelKey and modelVersion are also not included;
/// modelVersion defaults to 1 on reconstructed trackers.
/// </summary>
/// <param name="token">the resumption token obtained from <see cref="ResumptionToken"/></param>
/// <param name="client">the LaunchDarkly client</param>
Expand Down Expand Up @@ -466,7 +473,7 @@ public static LdAiConfigTracker FromResumptionToken(string token, ILaunchDarklyC
}

return new LdAiConfigTracker(client, payload.RunId, payload.ConfigKey,
payload.VariationKey, payload.Version ?? 1, context, "", "", payload.GraphKey);
payload.VariationKey, payload.Version ?? 1, context, "", "", graphKey: payload.GraphKey);
}

private class ResumptionPayload
Expand Down
2 changes: 2 additions & 0 deletions pkgs/sdk/server-ai/test/AgentGraphDefinitionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ private static LdAiAgentConfig MakeAgentConfig(string key, bool enabled = true)
enabled: enabled,
variationKey: "v1",
version: 1,
modelKey: null,
modelVersion: 1,
instructions: null,
tools: new Dictionary<string, LdAiConfigTypes.Tool>(),
model: null,
Expand Down
87 changes: 87 additions & 0 deletions pkgs/sdk/server-ai/test/ConfigFactoryParserTest.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Collections.Generic;
using LaunchDarkly.Sdk.Server.Ai.Config;
using LaunchDarkly.Sdk.Server.Ai.Interfaces;
using Moq;
using Xunit;

namespace LaunchDarkly.Sdk.Server.Ai;
Expand Down Expand Up @@ -204,4 +206,89 @@ public void ParseAllFields_WithNoNewFields_ReturnsNullOrEmptyWithoutError()
Assert.Null(judgeConfig);
Assert.Null(evaluationMetricKey);
}

[Fact]
public void CreateTrackerDefaultsModelVersionAndOmitsModelKeyWhenAbsentFromFlag()
{
// modelKey/modelVersion are intentionally not exposed on Model (they'd read as
// properties of the LLM itself, e.g. a version like "5.4"); the only place they
// surface is the tracker's stamped event data, mirroring variationKey/version.
var mockClient = new Mock<ILaunchDarklyClient>();
var mockLogger = new Mock<ILogger>();
mockClient.Setup(x => x.GetLogger()).Returns(mockLogger.Object);
mockClient.Setup(x =>
x.JsonVariation("model-config-no-version", It.IsAny<Context>(), It.IsAny<LdValue>()))
.Returns(LdValue.ObjectFrom(new Dictionary<string, LdValue>
{
["_ldMeta"] = LdValue.ObjectFrom(new Dictionary<string, LdValue>
{
["enabled"] = LdValue.Of(true),
["variationKey"] = LdValue.Of("v1"),
["version"] = LdValue.Of(1)
}),
["model"] = LdValue.ObjectFrom(new Dictionary<string, LdValue>
{
["name"] = LdValue.Of("gpt-4")
}),
["provider"] = LdValue.ObjectFrom(new Dictionary<string, LdValue>
{
["name"] = LdValue.Of("openai")
}),
["messages"] = LdValue.ArrayOf()
}));

var client = new LdAiClient(mockClient.Object);
var context = Context.New("user-key");
var config = client.CompletionConfig("model-config-no-version", context);
var tracker = config.CreateTracker();
tracker.TrackSuccess();

mockClient.Verify(x => x.Track("$ld:ai:generation:success", context,
It.Is<LdValue>(d =>
d.Get("modelKey").IsNull &&
d.Get("modelVersion").AsInt == 1),
1.0f), Times.Once);
}

[Fact]
public void CreateTrackerStampsModelKeyAndVersionOnTrackData()
{
var mockClient = new Mock<ILaunchDarklyClient>();
var mockLogger = new Mock<ILogger>();
mockClient.Setup(x => x.GetLogger()).Returns(mockLogger.Object);
mockClient.Setup(x =>
x.JsonVariation("my-config-key", It.IsAny<Context>(), It.IsAny<LdValue>()))
.Returns(LdValue.ObjectFrom(new Dictionary<string, LdValue>
{
["_ldMeta"] = LdValue.ObjectFrom(new Dictionary<string, LdValue>
{
["enabled"] = LdValue.Of(true),
["variationKey"] = LdValue.Of("var-abc"),
["version"] = LdValue.Of(7),
["modelKey"] = LdValue.Of("my-model"),
["modelVersion"] = LdValue.Of(2)
}),
["model"] = LdValue.ObjectFrom(new Dictionary<string, LdValue>
{
["name"] = LdValue.Of("gpt-4")
}),
["provider"] = LdValue.ObjectFrom(new Dictionary<string, LdValue>
{
["name"] = LdValue.Of("openai")
}),
["messages"] = LdValue.ArrayOf()
}));

var client = new LdAiClient(mockClient.Object);
var context = Context.New("user-key");
var config = client.CompletionConfig("my-config-key", context);
var tracker = config.CreateTracker();
tracker.TrackSuccess();

mockClient.Verify(x => x.Track("$ld:ai:generation:success", context,
It.Is<LdValue>(d =>
d.Get("modelKey").AsString == "my-model" &&
d.Get("modelVersion").AsInt == 2),
1.0f), Times.Once);
}
}
2 changes: 1 addition & 1 deletion pkgs/sdk/server-ai/test/LdAiAgentGraphConfigTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ private static LdAiConfigTracker MakeTracker(ILaunchDarklyClient client, Context
string graphKey = null)
{
return new LdAiConfigTracker(client, Guid.NewGuid().ToString(), "config-key",
"v1", 1, context, "model", "provider", graphKey);
"v1", 1, context, "model", "provider", graphKey: graphKey);
}

// Test 1: Tracker created with graphKey → events include graphKey in data
Expand Down
4 changes: 3 additions & 1 deletion pkgs/sdk/server-ai/test/LdAiClientTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,9 @@ public void CreateTrackerFromResumptionTokenSetsEmptyModelAndProvider()
d.Get("variationKey").AsString == "var-1" &&
d.Get("version").AsInt == 2 &&
d.Get("modelName").AsString == "" &&
d.Get("providerName").AsString == ""),
d.Get("providerName").AsString == "" &&
d.Get("modelVersion").AsInt == 1 &&
d.Get("modelKey").IsNull),
1.0f), Times.Once);
}

Expand Down
Loading
Loading